From 842c3edca118e4a3a8e9bced9d0a152e4bba14b7 Mon Sep 17 00:00:00 2001 From: Nick Partridge Date: Tue, 8 Jun 2021 17:26:53 -0500 Subject: [PATCH] build(workspaces): implement src as workspace package (#1198) Key changes: - move `./src/**` to `./packages/elastic-charts/**` - correct all paths due to file sturcture changes - fix bad docs paths and config options - create top-level mono package, move lib package - simplify lib tsconfig.json files - fix errors in linking package, cleanup paths - fix linting config to reflect file structure changes - update semantic release to point at new lib package - add lerna to facilitate running workspace scripts --- packages/osd-charts/.gitignore | 6 + packages/osd-charts/.npmignore | 1 + packages/osd-charts/api-extractor.jsonc | 366 + packages/osd-charts/api/charts.api.md | 2340 ++ packages/osd-charts/package.json | 67 + packages/osd-charts/scripts/concat_sass.js | 51 + packages/osd-charts/scripts/move_txt_files.js | 25 + packages/osd-charts/src/_eui_imports.scss | 3 + .../goal_chart/layout/config/config.ts | 57 + .../goal_chart/layout/types/config_types.ts | 45 + .../layout/types/viewmodel_types.ts | 116 + .../goal_chart/layout/viewmodel/viewmodel.ts | 107 + .../renderer/canvas/canvas_renderers.ts | 345 + .../renderer/canvas/connected_component.tsx | 190 + .../chart_types/goal_chart/specs/constants.ts | 29 + .../src/chart_types/goal_chart/specs/index.ts | 94 + .../goal_chart/state/chart_state.tsx | 149 + .../goal_chart/state/selectors/geometries.ts | 41 + .../selectors/get_chart_type_description.ts | 28 + .../goal_chart/state/selectors/goal_spec.ts | 30 + .../state/selectors/is_tooltip_visible.ts | 37 + .../selectors/on_element_click_caller.ts | 53 + .../state/selectors/on_element_out_caller.ts | 51 + .../state/selectors/on_element_over_caller.ts | 54 + .../state/selectors/picked_shapes.ts | 62 + .../goal_chart/state/selectors/scenegraph.ts | 41 + .../goal_chart/state/selectors/tooltip.ts | 63 + .../heatmap/layout/config/config.ts | 121 + .../heatmap/layout/types/config_types.ts | 118 + .../heatmap/layout/types/viewmodel_types.ts | 133 + .../heatmap/layout/viewmodel/viewmodel.ts | 431 + .../renderer/canvas/canvas_renderers.ts | 170 + .../renderer/canvas/connected_component.tsx | 172 + .../heatmap/renderer/dom/highlighter.tsx | 154 + .../renderer/dom/highlighter_brush.tsx | 62 + .../src/chart_types/heatmap/specs/heatmap.ts | 94 + .../src/chart_types/heatmap/specs/index.ts | 20 + .../heatmap/specs/scale_defaults.ts | 27 + .../chart_types/heatmap/state/chart_state.tsx | 143 + .../selectors/compute_chart_dimensions.ts | 117 + .../heatmap/state/selectors/compute_legend.ts | 56 + .../heatmap/state/selectors/geometries.ts | 76 + .../heatmap/state/selectors/get_brush_area.ts | 58 + .../get_brushed_highlighted_shapes.ts | 54 + .../state/selectors/get_color_scale.ts | 95 + .../state/selectors/get_cursor_pointer.ts | 33 + .../state/selectors/get_debug_state.ts | 92 + .../state/selectors/get_grid_full_height.ts | 93 + .../state/selectors/get_heatmap_config.ts | 34 + .../selectors/get_heatmap_container_size.ts | 60 + .../state/selectors/get_heatmap_spec.ts | 33 + .../state/selectors/get_heatmap_table.ts | 97 + .../state/selectors/get_highlighted_area.ts | 52 + .../selectors/get_legend_items_labels.ts | 37 + .../state/selectors/get_picked_cells.ts | 49 + .../state/selectors/get_tooltip_anchor.ts | 52 + .../selectors/get_x_axis_right_overflow.ts | 62 + .../heatmap/state/selectors/heatmap_spec.ts | 30 + .../state/selectors/is_brush_available.ts | 36 + .../heatmap/state/selectors/is_brushing.ts | 30 + .../state/selectors/is_tooltip_visible.ts | 37 + .../state/selectors/on_brush_end_caller.ts | 72 + .../selectors/on_element_click_caller.ts | 79 + .../state/selectors/on_element_out_caller.ts | 66 + .../state/selectors/on_element_over_caller.ts | 92 + .../heatmap/state/selectors/picked_shapes.ts | 39 + .../heatmap/state/selectors/scenegraph.ts | 63 + .../heatmap/state/selectors/tooltip.ts | 114 + packages/osd-charts/src/chart_types/index.ts | 35 + .../partition_chart/layout/config.ts | 341 + .../layout/types/config_types.ts | 152 + .../layout/types/viewmodel_types.ts | 251 + .../layout/utils/circline_geometry.ts | 240 + .../layout/utils/group_by_rollup.ts | 299 + .../layout/utils/highlighted_geoms.ts | 98 + .../partition_chart/layout/utils/legend.ts | 99 + .../layout/utils/legend_labels.ts | 64 + .../partition_chart/layout/utils/sunburst.ts | 50 + .../partition_chart/layout/utils/treemap.ts | 188 + .../layout/viewmodel/fill_text_layout.test.ts | 300 + .../layout/viewmodel/fill_text_layout.ts | 543 + .../viewmodel/hierarchy_of_arrays.test.ts | 47 + .../layout/viewmodel/hierarchy_of_arrays.ts | 139 + .../layout/viewmodel/link_text_layout.ts | 202 + .../layout/viewmodel/picked_shapes.ts | 68 + .../layout/viewmodel/scenegraph.ts | 83 + .../layout/viewmodel/tooltip_info.ts | 73 + .../layout/viewmodel/viewmodel.ts | 525 + .../partition_chart/partition.test.tsx | 359 + .../partition_chart/renderer/_index.scss | 9 + .../canvas/canvas_linear_renderers.ts | 119 + .../renderer/canvas/canvas_renderers.ts | 317 + .../renderer/canvas/partition.tsx | 241 + .../renderer/dom/highlighter.tsx | 312 + .../renderer/dom/highlighter_hover.tsx | 56 + .../renderer/dom/highlighter_legend.tsx | 56 + .../renderer/dom/layered_partition_chart.tsx | 38 + .../partition_chart/specs/index.ts | 110 + .../partition_chart/state/chart_state.tsx | 144 + .../get_legend_items_extra.test.ts.snap | 7 + .../state/selectors/compute_legend.ts | 44 + .../state/selectors/drilldown_active.ts | 28 + .../state/selectors/geometries.ts | 247 + .../selectors/get_chart_type_description.ts | 28 + .../state/selectors/get_debug_state.test.ts | 143 + .../state/selectors/get_debug_state.ts | 72 + .../state/selectors/get_highlighted_shapes.ts | 40 + .../selectors/get_legend_items_extra.test.ts | 137 + .../state/selectors/get_legend_items_extra.ts | 38 + .../selectors/get_legend_items_labels.test.ts | 200 + .../selectors/get_legend_items_labels.ts | 34 + .../state/selectors/get_partition_specs.ts | 32 + .../state/selectors/is_tooltip_visible.ts | 38 + .../selectors/on_element_click_caller.ts | 54 + .../state/selectors/on_element_out_caller.ts | 53 + .../state/selectors/on_element_over_caller.ts | 54 + .../state/selectors/partition_spec.ts | 35 + .../state/selectors/picked_shapes.test.ts | 276 + .../state/selectors/picked_shapes.ts | 41 + .../state/selectors/tooltip.ts | 43 + .../partition_chart/state/selectors/tree.ts | 116 + packages/osd-charts/src/chart_types/specs.ts | 35 + .../wordcloud/layout/config/config.ts | 53 + .../wordcloud/layout/types/config_types.ts | 37 + .../wordcloud/layout/types/viewmodel_types.ts | 164 + .../wordcloud/layout/viewmodel/viewmodel.ts | 84 + .../renderer/svg/connected_component.tsx | 319 + .../src/chart_types/wordcloud/specs/index.ts | 90 + .../wordcloud/state/chart_state.tsx | 122 + .../wordcloud/state/selectors/geometries.ts | 41 + .../selectors/on_element_click_caller.ts | 53 + .../state/selectors/on_element_out_caller.ts | 51 + .../state/selectors/on_element_over_caller.ts | 54 + .../state/selectors/picked_shapes.ts | 61 + .../wordcloud/state/selectors/scenegraph.ts | 35 + .../state/selectors/wordcloud_spec.ts | 30 + .../line/dimensions.integration.test.ts | 164 + .../annotations/line/dimensions.test.ts | 741 + .../xy_chart/annotations/line/dimensions.ts | 415 + .../xy_chart/annotations/line/line.test.tsx | 176 + .../annotations/line/tooltip.test.tsx | 195 + .../xy_chart/annotations/line/types.ts | 36 + .../rect/dimensions.integration.test.ts | 347 + .../annotations/rect/dimensions.test.ts | 105 + .../xy_chart/annotations/rect/dimensions.ts | 244 + .../xy_chart/annotations/rect/tooltip.test.ts | 59 + .../xy_chart/annotations/rect/tooltip.ts | 100 + .../xy_chart/annotations/rect/types.ts | 35 + .../xy_chart/annotations/tooltip.ts | 87 + .../chart_types/xy_chart/annotations/types.ts | 98 + .../xy_chart/annotations/utils.test.ts | 96 + .../chart_types/xy_chart/annotations/utils.ts | 173 + .../chart_types/xy_chart/axes/axes_sizes.ts | 116 + .../xy_chart/crosshair/crosshair_line.test.ts | 31 + .../crosshair_utils.linear_snap.test.ts | 1590 + .../crosshair_utils.ordinal_snap.test.ts | 231 + .../xy_chart/crosshair/crosshair_utils.ts | 214 + .../src/chart_types/xy_chart/domains/nice.ts | 23 + .../src/chart_types/xy_chart/domains/types.ts | 50 + .../xy_chart/domains/x_domain.test.ts | 876 + .../chart_types/xy_chart/domains/x_domain.ts | 222 + .../xy_chart/domains/y_domain.test.ts | 551 + .../chart_types/xy_chart/domains/y_domain.ts | 243 + .../xy_chart/legend/legend.test.ts | 445 + .../src/chart_types/xy_chart/legend/legend.ts | 190 + .../chart_types/xy_chart/renderer/_index.scss | 1 + .../renderer/canvas/annotations/index.ts | 64 + .../renderer/canvas/annotations/lines.ts | 50 + .../renderer/canvas/annotations/rect.ts | 57 + .../xy_chart/renderer/canvas/areas.ts | 124 + .../renderer/canvas/axes/global_title.ts | 134 + .../xy_chart/renderer/canvas/axes/index.ts | 176 + .../xy_chart/renderer/canvas/axes/line.ts | 51 + .../renderer/canvas/axes/panel_title.ts | 146 + .../xy_chart/renderer/canvas/axes/tick.ts | 83 + .../renderer/canvas/axes/tick_label.ts | 120 + .../xy_chart/renderer/canvas/bars.ts | 96 + .../xy_chart/renderer/canvas/bubbles.ts | 78 + .../xy_chart/renderer/canvas/grids.ts | 53 + .../xy_chart/renderer/canvas/lines.ts | 95 + .../xy_chart/renderer/canvas/panels/panels.ts | 44 + .../xy_chart/renderer/canvas/points.ts | 116 + .../renderer/canvas/primitives/arc.ts | 53 + .../renderer/canvas/primitives/line.ts | 72 + .../renderer/canvas/primitives/path.ts | 116 + .../renderer/canvas/primitives/rect.ts | 101 + .../renderer/canvas/primitives/shapes.ts | 48 + .../renderer/canvas/primitives/text.ts | 188 + .../renderer/canvas/primitives/utils.ts | 49 + .../xy_chart/renderer/canvas/renderers.ts | 238 + .../renderer/canvas/styles/area.test.ts | 120 + .../xy_chart/renderer/canvas/styles/area.ts | 50 + .../renderer/canvas/styles/bar.test.ts | 185 + .../xy_chart/renderer/canvas/styles/bar.ts | 64 + .../renderer/canvas/styles/line.test.ts | 96 + .../xy_chart/renderer/canvas/styles/line.ts | 45 + .../xy_chart/renderer/canvas/utils/debug.ts | 95 + .../renderer/canvas/utils/panel_transform.ts | 57 + .../xy_chart/renderer/canvas/values/bar.ts | 422 + .../xy_chart/renderer/canvas/xy_chart.tsx | 273 + .../xy_chart/renderer/dom/_crosshair.scss | 8 + .../xy_chart/renderer/dom/_highlighter.scss | 22 + .../xy_chart/renderer/dom/_index.scss | 4 + .../xy_chart/renderer/dom/_screen_reader.scss | 8 + .../dom/annotations/_annotations.scss | 33 + .../renderer/dom/annotations/_index.scss | 1 + .../dom/annotations/annotation_tooltip.tsx | 93 + .../renderer/dom/annotations/annotations.tsx | 195 + .../renderer/dom/annotations/index.ts | 21 + .../renderer/dom/annotations/line_marker.tsx | 143 + .../dom/annotations/tooltip_content.tsx | 76 + .../xy_chart/renderer/dom/crosshair.tsx | 154 + .../xy_chart/renderer/dom/highlighter.tsx | 152 + .../xy_chart/renderer/shapes_paths.ts | 69 + .../chart_types/xy_chart/rendering/area.ts | 156 + .../chart_types/xy_chart/rendering/bars.ts | 296 + .../chart_types/xy_chart/rendering/bubble.ts | 81 + .../xy_chart/rendering/constants.ts | 21 + .../chart_types/xy_chart/rendering/line.ts | 120 + .../xy_chart/rendering/point_style.ts | 45 + .../chart_types/xy_chart/rendering/points.ts | 267 + .../rendering/rendering.areas.test.ts | 1003 + .../rendering/rendering.bands.test.ts | 436 + .../xy_chart/rendering/rendering.bars.test.ts | 862 + .../rendering/rendering.bubble.test.ts | 793 + .../rendering/rendering.lines.test.ts | 781 + .../xy_chart/rendering/rendering.test.ts | 599 + .../chart_types/xy_chart/rendering/utils.ts | 241 + .../xy_chart/scales/get_api_scales.ts | 42 + .../xy_chart/scales/scale_defaults.ts | 38 + .../xy_chart/specs/area_series.tsx | 57 + .../src/chart_types/xy_chart/specs/axis.tsx | 46 + .../chart_types/xy_chart/specs/bar_series.tsx | 57 + .../xy_chart/specs/bubble_series.tsx | 57 + .../xy_chart/specs/histogram_bar_series.tsx | 57 + .../src/chart_types/xy_chart/specs/index.ts | 27 + .../xy_chart/specs/line_annotation.test.tsx | 65 + .../xy_chart/specs/line_annotation.tsx | 51 + .../xy_chart/specs/line_series.tsx | 56 + .../xy_chart/specs/rect_annotation.tsx | 48 + .../state/chart_state.accessibility.test.ts | 161 + .../state/chart_state.interactions.test.ts | 1113 + .../xy_chart/state/chart_state.specs.test.ts | 157 + .../xy_chart/state/chart_state.test.ts | 176 + .../state/chart_state.timescales.test.ts | 280 + .../state/chart_state.tooltip.test.ts | 85 + .../xy_chart/state/chart_state.tsx | 167 + .../state/selectors/compute_annotations.ts | 63 + .../selectors/compute_axes_geometries.ts | 85 + .../compute_axis_ticks_dimensions.ts | 93 + .../selectors/compute_chart_dimensions.ts | 50 + .../selectors/compute_chart_transform.ts | 33 + .../state/selectors/compute_grid_lines.ts | 35 + .../state/selectors/compute_legend.ts | 72 + .../state/selectors/compute_panels.ts | 38 + .../selectors/compute_per_panel_axes_geoms.ts | 86 + .../state/selectors/compute_series_domains.ts | 52 + .../selectors/compute_series_geometries.ts | 66 + .../compute_small_multiple_scales.ts | 61 + .../state/selectors/count_bars_in_cluster.ts | 63 + .../selectors/get_annotation_tooltip_state.ts | 173 + .../selectors/get_api_scale_configs.test.ts | 94 + .../state/selectors/get_api_scale_configs.ts | 118 + .../state/selectors/get_axis_styles.ts | 56 + .../state/selectors/get_bar_paddings.ts | 31 + .../state/selectors/get_brush_area.test.ts | 317 + .../state/selectors/get_brush_area.ts | 167 + .../selectors/get_chart_type_description.ts | 33 + .../state/selectors/get_computed_scales.ts | 30 + .../state/selectors/get_cursor_band.ts | 158 + .../state/selectors/get_cursor_line.ts | 34 + .../state/selectors/get_cursor_pointer.ts | 68 + .../state/selectors/get_debug_state.ts | 297 + .../get_elements_at_cursor_pos.test.ts | 98 + .../selectors/get_elements_at_cursor_pos.ts | 83 + .../state/selectors/get_geometries_index.ts | 30 + .../selectors/get_geometries_index_keys.ts | 30 + .../state/selectors/get_grid_lines.ts | 35 + .../state/selectors/get_highlighted_series.ts | 41 + .../state/selectors/get_highlighted_values.ts | 32 + .../selectors/get_legend_items_labels.ts | 37 + ...get_oriented_projected_pointer_position.ts | 49 + .../get_projected_pointer_position.ts | 103 + .../selectors/get_projected_scaled_values.ts | 54 + .../state/selectors/get_series_color_map.ts | 48 + .../state/selectors/get_si_dataseries_map.ts | 36 + .../state/selectors/get_specs.test.ts | 36 + .../xy_chart/state/selectors/get_specs.ts | 61 + .../state/selectors/get_tooltip_position.ts | 70 + .../state/selectors/get_tooltip_snap.ts | 39 + .../state/selectors/get_tooltip_type.ts | 30 + ...t_tooltip_values_highlighted_geoms.test.ts | 60 + .../get_tooltip_values_highlighted_geoms.ts | 234 + .../state/selectors/has_single_series.ts | 30 + .../is_annotation_tooltip_visible.ts | 29 + .../state/selectors/is_brush_available.ts | 40 + .../xy_chart/state/selectors/is_brushing.ts | 38 + .../state/selectors/is_chart_animatable.ts | 38 + .../state/selectors/is_chart_empty.ts | 29 + .../selectors/is_histogram_mode_enabled.ts | 29 + .../selectors/is_tooltip_snap_enabled.ts | 35 + .../state/selectors/is_tooltip_visible.ts | 71 + .../state/selectors/merge_y_custom_domains.ts | 84 + .../state/selectors/on_brush_end_caller.ts | 232 + .../state/selectors/on_click_caller.ts | 100 + .../state/selectors/on_element_out_caller.ts | 84 + .../state/selectors/on_element_over_caller.ts | 97 + .../state/selectors/on_pointer_move_caller.ts | 140 + .../utils/__snapshots__/utils.test.ts.snap | 786 + .../xy_chart/state/utils/common.test.ts | 288 + .../xy_chart/state/utils/common.ts | 77 + .../xy_chart/state/utils/get_last_value.ts | 69 + .../chart_types/xy_chart/state/utils/spec.ts | 58 + .../chart_types/xy_chart/state/utils/types.ts | 90 + .../xy_chart/state/utils/utils.test.ts | 845 + .../chart_types/xy_chart/state/utils/utils.ts | 577 + .../xy_chart/tooltip/tooltip.test.ts | 383 + .../chart_types/xy_chart/tooltip/tooltip.ts | 111 + .../__snapshots__/dimensions.test.ts.snap | 46 + .../utils/__snapshots__/series.test.ts.snap | 28877 ++++++++++++++++ .../xy_chart/utils/axis_type_utils.test.ts | 60 + .../xy_chart/utils/axis_type_utils.ts | 61 + .../xy_chart/utils/axis_utils.test.ts | 1896 + .../chart_types/xy_chart/utils/axis_utils.ts | 940 + .../xy_chart/utils/default_series_sort_fn.ts | 62 + .../xy_chart/utils/dimensions.test.ts | 192 + .../chart_types/xy_chart/utils/dimensions.ts | 83 + .../chart_types/xy_chart/utils/fill_series.ts | 80 + .../xy_chart/utils/fit_function.test.ts | 1053 + .../xy_chart/utils/fit_function.ts | 276 + .../xy_chart/utils/fit_function_utils.ts | 51 + .../xy_chart/utils/grid_lines.test.ts | 55 + .../chart_types/xy_chart/utils/grid_lines.ts | 151 + .../xy_chart/utils/group_data_series.ts | 51 + .../utils/indexed_geometry_linear_map.ts | 55 + .../xy_chart/utils/indexed_geometry_map.ts | 113 + .../utils/indexed_geometry_spatial_map.ts | 135 + .../xy_chart/utils/interactions.test.ts | 68 + .../xy_chart/utils/interactions.ts | 58 + .../utils/nonstacked_series_utils.test.ts | 312 + .../src/chart_types/xy_chart/utils/panel.ts | 30 + .../chart_types/xy_chart/utils/panel_utils.ts | 60 + .../chart_types/xy_chart/utils/scales.test.ts | 145 + .../src/chart_types/xy_chart/utils/scales.ts | 162 + .../chart_types/xy_chart/utils/series.test.ts | 1020 + .../src/chart_types/xy_chart/utils/series.ts | 629 + .../src/chart_types/xy_chart/utils/specs.ts | 989 + .../stacked_percent_series_utils.test.ts | 268 + .../utils/stacked_series_utils.test.ts | 360 + .../xy_chart/utils/stacked_series_utils.ts | 179 + .../src/chart_types/xy_chart/utils/texture.ts | 115 + .../osd-charts/src/common/__mocks__/calcs.ts | 23 + .../__mocks__/color_library_wrappers.ts | 28 + .../src/common/__mocks__/fill_text_layout.ts | 22 + .../src/common/__mocks__/link_text_layout.ts | 22 + packages/osd-charts/src/common/category.ts | 33 + .../osd-charts/src/common/color_calcs.test.ts | 198 + packages/osd-charts/src/common/color_calcs.ts | 211 + .../src/common/color_library_wrappers.test.ts | 220 + .../src/common/color_library_wrappers.ts | 135 + .../osd-charts/src/common/config_objects.ts | 97 + packages/osd-charts/src/common/constants.ts | 46 + .../src/common/event_handler_selectors.ts | 121 + .../osd-charts/src/common/fill_text_color.ts | 93 + packages/osd-charts/src/common/geometry.ts | 109 + packages/osd-charts/src/common/iterables.ts | 27 + packages/osd-charts/src/common/legend.ts | 53 + packages/osd-charts/src/common/math.ts | 23 + packages/osd-charts/src/common/predicate.ts | 64 + packages/osd-charts/src/common/series_id.ts | 42 + packages/osd-charts/src/common/text_utils.ts | 189 + .../__snapshots__/chart.test.tsx.snap | 127 + .../osd-charts/src/components/_container.scss | 34 + .../osd-charts/src/components/_global.scss | 16 + .../osd-charts/src/components/_index.scss | 11 + .../src/components/_unavailable_chart.scss | 9 + .../accessibility/accessibility.test.tsx | 46 + .../components/accessibility/description.tsx | 28 + .../src/components/accessibility/index.ts | 21 + .../src/components/accessibility/label.tsx | 29 + .../accessibility/screen_reader_summary.tsx | 66 + .../src/components/accessibility/types.tsx | 37 + .../src/components/brush/_brush.scss | 10 + .../src/components/brush/_index.scss | 1 + .../osd-charts/src/components/brush/brush.tsx | 189 + .../src/components/chart.snap.test.ts | 31 + .../osd-charts/src/components/chart.test.tsx | 60 + packages/osd-charts/src/components/chart.tsx | 191 + .../src/components/chart_background.tsx | 53 + .../src/components/chart_container.tsx | 249 + .../src/components/chart_resizer.tsx | 123 + .../src/components/chart_status.tsx | 80 + .../error_boundary/error_boundary.tsx | 62 + .../src/components/error_boundary/errors.ts | 40 + .../src/components/error_boundary/index.tsx | 23 + .../src/components/icons/_icon.scss | 15 + .../src/components/icons/_index.scss | 1 + .../src/components/icons/assets/alert.tsx | 34 + .../src/components/icons/assets/dot.tsx | 38 + .../src/components/icons/assets/empty.tsx | 27 + .../src/components/icons/assets/eye.tsx | 31 + .../components/icons/assets/eye_closed.tsx | 31 + .../src/components/icons/assets/list.tsx | 31 + .../icons/assets/question_in_circle.tsx | 31 + .../osd-charts/src/components/icons/icon.tsx | 85 + packages/osd-charts/src/components/index.ts | 21 + .../legend/__snapshots__/legend.test.tsx.snap | 280 + .../src/components/legend/_index.scss | 3 + .../src/components/legend/_legend.scss | 42 + .../src/components/legend/_legend_item.scss | 106 + .../src/components/legend/_variables.scss | 3 + .../src/components/legend/color.tsx | 70 + .../src/components/legend/extra.tsx | 33 + .../src/components/legend/label.tsx | 55 + .../src/components/legend/legend.test.tsx | 310 + .../src/components/legend/legend.tsx | 176 + .../src/components/legend/legend_item.tsx | 238 + .../src/components/legend/position_style.ts | 104 + .../src/components/legend/style_utils.ts | 98 + .../osd-charts/src/components/legend/utils.ts | 40 + .../osd-charts/src/components/no_results.tsx | 33 + .../src/components/portal/_index.scss | 1 + .../src/components/portal/_portal.scss | 15 + .../osd-charts/src/components/portal/index.ts | 22 + .../src/components/portal/tooltip_portal.tsx | 236 + .../osd-charts/src/components/portal/types.ts | 129 + .../osd-charts/src/components/portal/utils.ts | 129 + .../src/components/tooltip/_index.scss | 1 + .../src/components/tooltip/_tooltip.scss | 69 + .../tooltip/get_tooltip_settings.ts | 37 + .../src/components/tooltip/index.ts | 21 + .../src/components/tooltip/tooltip.tsx | 251 + .../src/components/tooltip/types.ts | 45 + packages/osd-charts/src/geoms/types.ts | 102 + packages/osd-charts/src/index.ts | 106 + .../src/mocks/annotations/annotations.ts | 113 + packages/osd-charts/src/mocks/canvas.ts | 31 + packages/osd-charts/src/mocks/geometries.ts | 201 + .../hierarchical/cpu_profile_tree_mock.json | 7541 ++++ .../src/mocks/hierarchical/dimension_codes.ts | 309 + .../src/mocks/hierarchical/index.ts | 33 + .../src/mocks/hierarchical/many_pie.ts | 252 + .../src/mocks/hierarchical/mini_sunburst.ts | 38 + .../mocks/hierarchical/observability_tree.ts | 25 + .../src/mocks/hierarchical/palettes.ts | 544 + .../osd-charts/src/mocks/hierarchical/pie.ts | 32 + .../src/mocks/hierarchical/sunburst.ts | 168 + packages/osd-charts/src/mocks/index.ts | 23 + packages/osd-charts/src/mocks/scale/index.ts | 20 + packages/osd-charts/src/mocks/scale/scale.ts | 49 + packages/osd-charts/src/mocks/series/data.ts | 180 + packages/osd-charts/src/mocks/series/index.ts | 23 + .../osd-charts/src/mocks/series/series.ts | 196 + .../src/mocks/series/series_identifiers.ts | 53 + packages/osd-charts/src/mocks/series/utils.ts | 52 + packages/osd-charts/src/mocks/specs/index.ts | 21 + packages/osd-charts/src/mocks/specs/specs.ts | 373 + packages/osd-charts/src/mocks/store/index.ts | 21 + packages/osd-charts/src/mocks/store/store.ts | 61 + packages/osd-charts/src/mocks/theme.ts | 125 + packages/osd-charts/src/mocks/utils.ts | 95 + packages/osd-charts/src/mocks/xy/domains.ts | 78 + .../osd-charts/src/renderers/canvas/index.ts | 127 + packages/osd-charts/src/reset_dark.scss | 6 + packages/osd-charts/src/reset_light.scss | 6 + packages/osd-charts/src/scales/constants.ts | 44 + packages/osd-charts/src/scales/index.ts | 82 + .../osd-charts/src/scales/scale_band.test.ts | 149 + packages/osd-charts/src/scales/scale_band.ts | 157 + .../src/scales/scale_continuous.test.ts | 572 + .../osd-charts/src/scales/scale_continuous.ts | 566 + .../osd-charts/src/scales/scale_time.test.ts | 267 + packages/osd-charts/src/scales/scales.test.ts | 237 + packages/osd-charts/src/scales/types.ts | 47 + .../src/solvers/monotonic_hill_climb.ts | 80 + .../screenspace_marker_scale_compressor.ts | 65 + packages/osd-charts/src/specs/constants.ts | 177 + packages/osd-charts/src/specs/group_by.ts | 69 + packages/osd-charts/src/specs/index.ts | 36 + .../osd-charts/src/specs/settings.test.tsx | 208 + packages/osd-charts/src/specs/settings.tsx | 721 + .../osd-charts/src/specs/small_multiples.ts | 96 + .../src/specs/specs_parser.test.tsx | 184 + .../osd-charts/src/specs/specs_parser.tsx | 59 + .../osd-charts/src/state/actions/chart.ts | 33 + .../src/state/actions/chart_settings.ts | 36 + .../osd-charts/src/state/actions/colors.ts | 64 + .../src/state/actions/dom_element.ts | 61 + .../osd-charts/src/state/actions/events.ts | 36 + .../osd-charts/src/state/actions/index.ts | 40 + packages/osd-charts/src/state/actions/key.ts | 41 + .../osd-charts/src/state/actions/legend.ts | 72 + .../osd-charts/src/state/actions/mouse.ts | 79 + .../osd-charts/src/state/actions/specs.ts | 73 + .../osd-charts/src/state/actions/z_index.ts | 34 + packages/osd-charts/src/state/chart_state.ts | 439 + .../src/state/reducers/interactions.ts | 214 + .../selectors/get_accessibility_config.ts | 78 + .../get_chart_container_dimensions.ts | 53 + .../src/state/selectors/get_chart_id.ts | 23 + .../src/state/selectors/get_chart_rotation.ts | 30 + .../src/state/selectors/get_chart_theme.ts | 43 + .../selectors/get_chart_type_components.ts | 33 + .../selectors/get_chart_type_description.ts | 29 + .../src/state/selectors/get_debug_state.ts | 33 + .../selectors/get_deselected_data_series.ts | 23 + .../selectors/get_internal_brush_area.ts | 29 + .../selectors/get_internal_cursor_pointer.ts | 26 + .../selectors/get_internal_is_brushing.ts | 28 + .../get_internal_is_brushing_available.ts | 28 + .../selectors/get_internal_is_intialized.ts | 57 + .../get_internal_is_tooltip_visible.ts | 30 + .../get_internal_main_projection_area.ts | 29 + .../get_internal_projection_container_area.ts | 29 + .../get_internal_tooltip_anchor_position.ts | 29 + .../selectors/get_internal_tooltip_info.ts | 28 + .../src/state/selectors/get_last_click.ts | 25 + .../src/state/selectors/get_last_drag.ts | 25 + .../selectors/get_legend_config_selector.ts | 60 + .../src/state/selectors/get_legend_items.ts | 31 + .../selectors/get_legend_items_labels.ts | 30 + .../selectors/get_legend_items_values.ts | 32 + .../src/state/selectors/get_legend_size.ts | 110 + .../selectors/get_settings_specs.test.ts | 57 + .../src/state/selectors/get_settings_specs.ts | 42 + .../selectors/get_small_multiples_spec.ts | 43 + .../selectors/get_tooltip_header_formatter.ts | 37 + .../selectors/has_external_pointer_event.ts | 25 + .../src/state/selectors/is_chart_empty.ts | 27 + .../selectors/is_external_tooltip_visible.ts | 52 + .../src/state/spec_factory.test.tsx | 94 + packages/osd-charts/src/state/spec_factory.ts | 75 + packages/osd-charts/src/state/types.ts | 131 + packages/osd-charts/src/state/utils.test.ts | 30 + packages/osd-charts/src/state/utils.ts | 58 + packages/osd-charts/src/theme_dark.scss | 7 + packages/osd-charts/src/theme_light.scss | 7 + packages/osd-charts/src/theme_only_dark.scss | 6 + packages/osd-charts/src/theme_only_light.scss | 6 + .../osd-charts/src/utils/__mocks__/common.ts | 48 + packages/osd-charts/src/utils/accessor.ts | 117 + .../src/utils/bbox/bbox_calculator.ts | 36 + .../bbox/canvas_text_bbox_calculator.test.ts | 29 + .../utils/bbox/canvas_text_bbox_calculator.ts | 60 + .../utils/bbox/dom_text_bbox_calculator.ts | 54 + .../utils/bbox/svg_text_bbox_calculator.ts | 58 + .../osd-charts/src/utils/chart_size.test.ts | 81 + packages/osd-charts/src/utils/chart_size.ts | 53 + packages/osd-charts/src/utils/common.test.ts | 1158 + packages/osd-charts/src/utils/common.tsx | 656 + packages/osd-charts/src/utils/curves.test.ts | 49 + packages/osd-charts/src/utils/curves.ts | 76 + .../osd-charts/src/utils/d3-delaunay/index.ts | 1543 + .../osd-charts/src/utils/data/date_time.ts | 31 + .../osd-charts/src/utils/data/formatters.ts | 52 + .../src/utils/data/formatters.tz.test.ts | 139 + .../src/utils/data/formatters.tz.test.utc.ts | 71 + .../utils/data_generators/data_generator.ts | 96 + .../src/utils/data_generators/simple_noise.ts | 63 + .../src/utils/data_samples/4_time_series.json | 4694 +++ .../src/utils/data_samples/babynames.ts | 1186 + .../data_samples/test_anomaly_swim_lane.ts | 732 + .../src/utils/data_samples/test_dataset.ts | 175 + .../utils/data_samples/test_dataset_github.ts | 842 + .../utils/data_samples/test_dataset_kibana.ts | 1426 + .../utils/data_samples/test_dataset_random.ts | 930 + .../utils/data_samples/test_dataset_tsvb.ts | 1075 + packages/osd-charts/src/utils/dimensions.ts | 85 + packages/osd-charts/src/utils/domain.test.ts | 244 + packages/osd-charts/src/utils/domain.ts | 134 + packages/osd-charts/src/utils/events.ts | 56 + .../osd-charts/src/utils/fast_deep_equal.ts | 91 + packages/osd-charts/src/utils/geometry.ts | 176 + packages/osd-charts/src/utils/ids.test.ts | 57 + packages/osd-charts/src/utils/ids.ts | 27 + packages/osd-charts/src/utils/legend.ts | 29 + packages/osd-charts/src/utils/logger.ts | 91 + packages/osd-charts/src/utils/point.ts | 29 + packages/osd-charts/src/utils/series_sort.ts | 73 + .../osd-charts/src/utils/themes/colors.ts | 73 + .../osd-charts/src/utils/themes/dark_theme.ts | 208 + .../src/utils/themes/light_theme.ts | 208 + .../src/utils/themes/merge_utils.ts | 103 + .../osd-charts/src/utils/themes/theme.test.ts | 463 + packages/osd-charts/src/utils/themes/theme.ts | 549 + .../src/utils/themes/theme_common.ts | 52 + packages/osd-charts/tsconfig.check.json | 12 + packages/osd-charts/tsconfig.json | 11 + packages/osd-charts/tsconfig.nocomments.json | 9 + 589 files changed, 126259 insertions(+) create mode 100644 packages/osd-charts/.gitignore create mode 100644 packages/osd-charts/.npmignore create mode 100644 packages/osd-charts/api-extractor.jsonc create mode 100644 packages/osd-charts/api/charts.api.md create mode 100644 packages/osd-charts/package.json create mode 100644 packages/osd-charts/scripts/concat_sass.js create mode 100644 packages/osd-charts/scripts/move_txt_files.js create mode 100644 packages/osd-charts/src/_eui_imports.scss create mode 100644 packages/osd-charts/src/chart_types/goal_chart/layout/config/config.ts create mode 100644 packages/osd-charts/src/chart_types/goal_chart/layout/types/config_types.ts create mode 100644 packages/osd-charts/src/chart_types/goal_chart/layout/types/viewmodel_types.ts create mode 100644 packages/osd-charts/src/chart_types/goal_chart/layout/viewmodel/viewmodel.ts create mode 100644 packages/osd-charts/src/chart_types/goal_chart/renderer/canvas/canvas_renderers.ts create mode 100644 packages/osd-charts/src/chart_types/goal_chart/renderer/canvas/connected_component.tsx create mode 100644 packages/osd-charts/src/chart_types/goal_chart/specs/constants.ts create mode 100644 packages/osd-charts/src/chart_types/goal_chart/specs/index.ts create mode 100644 packages/osd-charts/src/chart_types/goal_chart/state/chart_state.tsx create mode 100644 packages/osd-charts/src/chart_types/goal_chart/state/selectors/geometries.ts create mode 100644 packages/osd-charts/src/chart_types/goal_chart/state/selectors/get_chart_type_description.ts create mode 100644 packages/osd-charts/src/chart_types/goal_chart/state/selectors/goal_spec.ts create mode 100644 packages/osd-charts/src/chart_types/goal_chart/state/selectors/is_tooltip_visible.ts create mode 100644 packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_click_caller.ts create mode 100644 packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_out_caller.ts create mode 100644 packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_over_caller.ts create mode 100644 packages/osd-charts/src/chart_types/goal_chart/state/selectors/picked_shapes.ts create mode 100644 packages/osd-charts/src/chart_types/goal_chart/state/selectors/scenegraph.ts create mode 100644 packages/osd-charts/src/chart_types/goal_chart/state/selectors/tooltip.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/layout/config/config.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/layout/types/config_types.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/layout/types/viewmodel_types.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/renderer/canvas/canvas_renderers.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx create mode 100644 packages/osd-charts/src/chart_types/heatmap/renderer/dom/highlighter.tsx create mode 100644 packages/osd-charts/src/chart_types/heatmap/renderer/dom/highlighter_brush.tsx create mode 100644 packages/osd-charts/src/chart_types/heatmap/specs/heatmap.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/specs/index.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/specs/scale_defaults.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/chart_state.tsx create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/compute_chart_dimensions.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/compute_legend.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/geometries.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/get_brush_area.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/get_color_scale.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/get_cursor_pointer.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/get_debug_state.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/get_grid_full_height.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/get_heatmap_config.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/get_heatmap_container_size.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/get_heatmap_spec.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/get_heatmap_table.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/get_highlighted_area.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/get_legend_items_labels.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/get_picked_cells.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/get_tooltip_anchor.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/get_x_axis_right_overflow.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/heatmap_spec.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/is_brush_available.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/is_brushing.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/is_tooltip_visible.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/on_brush_end_caller.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/on_element_click_caller.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/on_element_out_caller.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/on_element_over_caller.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/picked_shapes.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/scenegraph.ts create mode 100644 packages/osd-charts/src/chart_types/heatmap/state/selectors/tooltip.ts create mode 100644 packages/osd-charts/src/chart_types/index.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/config.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/types/config_types.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/types/viewmodel_types.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/utils/circline_geometry.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/utils/group_by_rollup.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/utils/highlighted_geoms.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/utils/legend.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/utils/legend_labels.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/utils/sunburst.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/utils/treemap.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.test.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/hierarchy_of_arrays.test.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/hierarchy_of_arrays.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/link_text_layout.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/picked_shapes.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/scenegraph.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/tooltip_info.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/partition.test.tsx create mode 100644 packages/osd-charts/src/chart_types/partition_chart/renderer/_index.scss create mode 100644 packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/canvas_linear_renderers.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/partition.tsx create mode 100644 packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter.tsx create mode 100644 packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter_hover.tsx create mode 100644 packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter_legend.tsx create mode 100644 packages/osd-charts/src/chart_types/partition_chart/renderer/dom/layered_partition_chart.tsx create mode 100644 packages/osd-charts/src/chart_types/partition_chart/specs/index.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/chart_state.tsx create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/__snapshots__/get_legend_items_extra.test.ts.snap create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/drilldown_active.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/geometries.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_chart_type_description.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_debug_state.test.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_debug_state.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_highlighted_shapes.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_extra.test.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_extra.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.test.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_partition_specs.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/is_tooltip_visible.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_out_caller.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_over_caller.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/partition_spec.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/picked_shapes.test.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/picked_shapes.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/tooltip.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/tree.ts create mode 100644 packages/osd-charts/src/chart_types/specs.ts create mode 100644 packages/osd-charts/src/chart_types/wordcloud/layout/config/config.ts create mode 100644 packages/osd-charts/src/chart_types/wordcloud/layout/types/config_types.ts create mode 100644 packages/osd-charts/src/chart_types/wordcloud/layout/types/viewmodel_types.ts create mode 100644 packages/osd-charts/src/chart_types/wordcloud/layout/viewmodel/viewmodel.ts create mode 100644 packages/osd-charts/src/chart_types/wordcloud/renderer/svg/connected_component.tsx create mode 100644 packages/osd-charts/src/chart_types/wordcloud/specs/index.ts create mode 100644 packages/osd-charts/src/chart_types/wordcloud/state/chart_state.tsx create mode 100644 packages/osd-charts/src/chart_types/wordcloud/state/selectors/geometries.ts create mode 100644 packages/osd-charts/src/chart_types/wordcloud/state/selectors/on_element_click_caller.ts create mode 100644 packages/osd-charts/src/chart_types/wordcloud/state/selectors/on_element_out_caller.ts create mode 100644 packages/osd-charts/src/chart_types/wordcloud/state/selectors/on_element_over_caller.ts create mode 100644 packages/osd-charts/src/chart_types/wordcloud/state/selectors/picked_shapes.ts create mode 100644 packages/osd-charts/src/chart_types/wordcloud/state/selectors/scenegraph.ts create mode 100644 packages/osd-charts/src/chart_types/wordcloud/state/selectors/wordcloud_spec.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/annotations/line/dimensions.integration.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/annotations/line/dimensions.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/annotations/line/dimensions.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/annotations/line/line.test.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/annotations/line/tooltip.test.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/annotations/line/types.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/annotations/rect/dimensions.integration.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/annotations/rect/dimensions.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/annotations/rect/dimensions.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/annotations/rect/tooltip.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/annotations/rect/tooltip.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/annotations/rect/types.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/annotations/tooltip.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/annotations/types.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/annotations/utils.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/annotations/utils.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/axes/axes_sizes.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/crosshair/crosshair_line.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/crosshair/crosshair_utils.linear_snap.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/crosshair/crosshair_utils.ordinal_snap.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/crosshair/crosshair_utils.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/domains/nice.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/domains/types.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/domains/x_domain.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/domains/x_domain.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/domains/y_domain.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/domains/y_domain.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/legend/legend.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/legend/legend.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/_index.scss create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/annotations/index.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/areas.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/global_title.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/index.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/line.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/panel_title.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/bars.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/bubbles.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/grids.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/lines.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/panels/panels.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/points.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/arc.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/line.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/shapes.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/text.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/utils.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/renderers.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/area.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/area.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/bar.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/line.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/line.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/utils/debug.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/utils/panel_transform.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/values/bar.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_crosshair.scss create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_highlighter.scss create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_index.scss create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_screen_reader.scss create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/_annotations.scss create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/_index.scss create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/annotation_tooltip.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/annotations.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/index.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/line_marker.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/tooltip_content.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/dom/crosshair.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/renderer/shapes_paths.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/rendering/area.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/rendering/bars.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/rendering/bubble.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/rendering/constants.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/rendering/line.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/rendering/point_style.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/rendering/points.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.areas.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.bands.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.bars.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.bubble.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.lines.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/rendering/utils.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/scales/get_api_scales.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/scales/scale_defaults.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/specs/area_series.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/specs/axis.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/specs/bar_series.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/specs/bubble_series.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/specs/histogram_bar_series.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/specs/index.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/specs/line_annotation.test.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/specs/line_annotation.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/specs/line_series.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/specs/rect_annotation.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/chart_state.accessibility.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/chart_state.interactions.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/chart_state.specs.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/chart_state.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/chart_state.timescales.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/chart_state.tooltip.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/chart_state.tsx create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_annotations.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_axes_geometries.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_axis_ticks_dimensions.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_chart_dimensions.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_chart_transform.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_grid_lines.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_legend.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_panels.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_per_panel_axes_geoms.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_small_multiple_scales.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/count_bars_in_cluster.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_api_scale_configs.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_api_scale_configs.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_axis_styles.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_bar_paddings.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_brush_area.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_brush_area.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_chart_type_description.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_computed_scales.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_cursor_line.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_cursor_pointer.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_debug_state.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_elements_at_cursor_pos.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_elements_at_cursor_pos.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_geometries_index.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_geometries_index_keys.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_grid_lines.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_highlighted_series.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_highlighted_values.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_oriented_projected_pointer_position.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_projected_pointer_position.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_projected_scaled_values.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_si_dataseries_map.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_specs.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_specs.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_position.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_snap.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_type.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/has_single_series.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_annotation_tooltip_visible.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_brush_available.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_brushing.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_chart_animatable.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_chart_empty.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_histogram_mode_enabled.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_tooltip_snap_enabled.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_tooltip_visible.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/merge_y_custom_domains.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_brush_end_caller.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_click_caller.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_element_out_caller.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_element_over_caller.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_pointer_move_caller.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/utils/__snapshots__/utils.test.ts.snap create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/utils/common.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/utils/common.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/utils/get_last_value.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/utils/spec.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/utils/types.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/utils/utils.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/state/utils/utils.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/tooltip/tooltip.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/tooltip/tooltip.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/__snapshots__/dimensions.test.ts.snap create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/__snapshots__/series.test.ts.snap create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/axis_type_utils.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/axis_type_utils.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/axis_utils.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/axis_utils.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/default_series_sort_fn.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/dimensions.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/dimensions.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/fill_series.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/fit_function.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/fit_function.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/fit_function_utils.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/grid_lines.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/grid_lines.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/group_data_series.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/indexed_geometry_linear_map.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/indexed_geometry_map.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/indexed_geometry_spatial_map.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/interactions.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/interactions.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/panel.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/panel_utils.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/scales.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/scales.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/series.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/series.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/specs.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/stacked_series_utils.test.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/stacked_series_utils.ts create mode 100644 packages/osd-charts/src/chart_types/xy_chart/utils/texture.ts create mode 100644 packages/osd-charts/src/common/__mocks__/calcs.ts create mode 100644 packages/osd-charts/src/common/__mocks__/color_library_wrappers.ts create mode 100644 packages/osd-charts/src/common/__mocks__/fill_text_layout.ts create mode 100644 packages/osd-charts/src/common/__mocks__/link_text_layout.ts create mode 100644 packages/osd-charts/src/common/category.ts create mode 100644 packages/osd-charts/src/common/color_calcs.test.ts create mode 100644 packages/osd-charts/src/common/color_calcs.ts create mode 100644 packages/osd-charts/src/common/color_library_wrappers.test.ts create mode 100644 packages/osd-charts/src/common/color_library_wrappers.ts create mode 100644 packages/osd-charts/src/common/config_objects.ts create mode 100644 packages/osd-charts/src/common/constants.ts create mode 100644 packages/osd-charts/src/common/event_handler_selectors.ts create mode 100644 packages/osd-charts/src/common/fill_text_color.ts create mode 100644 packages/osd-charts/src/common/geometry.ts create mode 100644 packages/osd-charts/src/common/iterables.ts create mode 100644 packages/osd-charts/src/common/legend.ts create mode 100644 packages/osd-charts/src/common/math.ts create mode 100644 packages/osd-charts/src/common/predicate.ts create mode 100644 packages/osd-charts/src/common/series_id.ts create mode 100644 packages/osd-charts/src/common/text_utils.ts create mode 100644 packages/osd-charts/src/components/__snapshots__/chart.test.tsx.snap create mode 100644 packages/osd-charts/src/components/_container.scss create mode 100644 packages/osd-charts/src/components/_global.scss create mode 100644 packages/osd-charts/src/components/_index.scss create mode 100644 packages/osd-charts/src/components/_unavailable_chart.scss create mode 100644 packages/osd-charts/src/components/accessibility/accessibility.test.tsx create mode 100644 packages/osd-charts/src/components/accessibility/description.tsx create mode 100644 packages/osd-charts/src/components/accessibility/index.ts create mode 100644 packages/osd-charts/src/components/accessibility/label.tsx create mode 100644 packages/osd-charts/src/components/accessibility/screen_reader_summary.tsx create mode 100644 packages/osd-charts/src/components/accessibility/types.tsx create mode 100644 packages/osd-charts/src/components/brush/_brush.scss create mode 100644 packages/osd-charts/src/components/brush/_index.scss create mode 100644 packages/osd-charts/src/components/brush/brush.tsx create mode 100644 packages/osd-charts/src/components/chart.snap.test.ts create mode 100644 packages/osd-charts/src/components/chart.test.tsx create mode 100644 packages/osd-charts/src/components/chart.tsx create mode 100644 packages/osd-charts/src/components/chart_background.tsx create mode 100644 packages/osd-charts/src/components/chart_container.tsx create mode 100644 packages/osd-charts/src/components/chart_resizer.tsx create mode 100644 packages/osd-charts/src/components/chart_status.tsx create mode 100644 packages/osd-charts/src/components/error_boundary/error_boundary.tsx create mode 100644 packages/osd-charts/src/components/error_boundary/errors.ts create mode 100644 packages/osd-charts/src/components/error_boundary/index.tsx create mode 100644 packages/osd-charts/src/components/icons/_icon.scss create mode 100644 packages/osd-charts/src/components/icons/_index.scss create mode 100644 packages/osd-charts/src/components/icons/assets/alert.tsx create mode 100644 packages/osd-charts/src/components/icons/assets/dot.tsx create mode 100644 packages/osd-charts/src/components/icons/assets/empty.tsx create mode 100644 packages/osd-charts/src/components/icons/assets/eye.tsx create mode 100644 packages/osd-charts/src/components/icons/assets/eye_closed.tsx create mode 100644 packages/osd-charts/src/components/icons/assets/list.tsx create mode 100644 packages/osd-charts/src/components/icons/assets/question_in_circle.tsx create mode 100644 packages/osd-charts/src/components/icons/icon.tsx create mode 100644 packages/osd-charts/src/components/index.ts create mode 100644 packages/osd-charts/src/components/legend/__snapshots__/legend.test.tsx.snap create mode 100644 packages/osd-charts/src/components/legend/_index.scss create mode 100644 packages/osd-charts/src/components/legend/_legend.scss create mode 100644 packages/osd-charts/src/components/legend/_legend_item.scss create mode 100644 packages/osd-charts/src/components/legend/_variables.scss create mode 100644 packages/osd-charts/src/components/legend/color.tsx create mode 100644 packages/osd-charts/src/components/legend/extra.tsx create mode 100644 packages/osd-charts/src/components/legend/label.tsx create mode 100644 packages/osd-charts/src/components/legend/legend.test.tsx create mode 100644 packages/osd-charts/src/components/legend/legend.tsx create mode 100644 packages/osd-charts/src/components/legend/legend_item.tsx create mode 100644 packages/osd-charts/src/components/legend/position_style.ts create mode 100644 packages/osd-charts/src/components/legend/style_utils.ts create mode 100644 packages/osd-charts/src/components/legend/utils.ts create mode 100644 packages/osd-charts/src/components/no_results.tsx create mode 100644 packages/osd-charts/src/components/portal/_index.scss create mode 100644 packages/osd-charts/src/components/portal/_portal.scss create mode 100644 packages/osd-charts/src/components/portal/index.ts create mode 100644 packages/osd-charts/src/components/portal/tooltip_portal.tsx create mode 100644 packages/osd-charts/src/components/portal/types.ts create mode 100644 packages/osd-charts/src/components/portal/utils.ts create mode 100644 packages/osd-charts/src/components/tooltip/_index.scss create mode 100644 packages/osd-charts/src/components/tooltip/_tooltip.scss create mode 100644 packages/osd-charts/src/components/tooltip/get_tooltip_settings.ts create mode 100644 packages/osd-charts/src/components/tooltip/index.ts create mode 100644 packages/osd-charts/src/components/tooltip/tooltip.tsx create mode 100644 packages/osd-charts/src/components/tooltip/types.ts create mode 100644 packages/osd-charts/src/geoms/types.ts create mode 100644 packages/osd-charts/src/index.ts create mode 100644 packages/osd-charts/src/mocks/annotations/annotations.ts create mode 100644 packages/osd-charts/src/mocks/canvas.ts create mode 100644 packages/osd-charts/src/mocks/geometries.ts create mode 100644 packages/osd-charts/src/mocks/hierarchical/cpu_profile_tree_mock.json create mode 100644 packages/osd-charts/src/mocks/hierarchical/dimension_codes.ts create mode 100644 packages/osd-charts/src/mocks/hierarchical/index.ts create mode 100644 packages/osd-charts/src/mocks/hierarchical/many_pie.ts create mode 100644 packages/osd-charts/src/mocks/hierarchical/mini_sunburst.ts create mode 100644 packages/osd-charts/src/mocks/hierarchical/observability_tree.ts create mode 100644 packages/osd-charts/src/mocks/hierarchical/palettes.ts create mode 100644 packages/osd-charts/src/mocks/hierarchical/pie.ts create mode 100644 packages/osd-charts/src/mocks/hierarchical/sunburst.ts create mode 100644 packages/osd-charts/src/mocks/index.ts create mode 100644 packages/osd-charts/src/mocks/scale/index.ts create mode 100644 packages/osd-charts/src/mocks/scale/scale.ts create mode 100644 packages/osd-charts/src/mocks/series/data.ts create mode 100644 packages/osd-charts/src/mocks/series/index.ts create mode 100644 packages/osd-charts/src/mocks/series/series.ts create mode 100644 packages/osd-charts/src/mocks/series/series_identifiers.ts create mode 100644 packages/osd-charts/src/mocks/series/utils.ts create mode 100644 packages/osd-charts/src/mocks/specs/index.ts create mode 100644 packages/osd-charts/src/mocks/specs/specs.ts create mode 100644 packages/osd-charts/src/mocks/store/index.ts create mode 100644 packages/osd-charts/src/mocks/store/store.ts create mode 100644 packages/osd-charts/src/mocks/theme.ts create mode 100644 packages/osd-charts/src/mocks/utils.ts create mode 100644 packages/osd-charts/src/mocks/xy/domains.ts create mode 100644 packages/osd-charts/src/renderers/canvas/index.ts create mode 100644 packages/osd-charts/src/reset_dark.scss create mode 100644 packages/osd-charts/src/reset_light.scss create mode 100644 packages/osd-charts/src/scales/constants.ts create mode 100644 packages/osd-charts/src/scales/index.ts create mode 100644 packages/osd-charts/src/scales/scale_band.test.ts create mode 100644 packages/osd-charts/src/scales/scale_band.ts create mode 100644 packages/osd-charts/src/scales/scale_continuous.test.ts create mode 100644 packages/osd-charts/src/scales/scale_continuous.ts create mode 100644 packages/osd-charts/src/scales/scale_time.test.ts create mode 100644 packages/osd-charts/src/scales/scales.test.ts create mode 100644 packages/osd-charts/src/scales/types.ts create mode 100644 packages/osd-charts/src/solvers/monotonic_hill_climb.ts create mode 100644 packages/osd-charts/src/solvers/screenspace_marker_scale_compressor.ts create mode 100644 packages/osd-charts/src/specs/constants.ts create mode 100644 packages/osd-charts/src/specs/group_by.ts create mode 100644 packages/osd-charts/src/specs/index.ts create mode 100644 packages/osd-charts/src/specs/settings.test.tsx create mode 100644 packages/osd-charts/src/specs/settings.tsx create mode 100644 packages/osd-charts/src/specs/small_multiples.ts create mode 100644 packages/osd-charts/src/specs/specs_parser.test.tsx create mode 100644 packages/osd-charts/src/specs/specs_parser.tsx create mode 100644 packages/osd-charts/src/state/actions/chart.ts create mode 100644 packages/osd-charts/src/state/actions/chart_settings.ts create mode 100644 packages/osd-charts/src/state/actions/colors.ts create mode 100644 packages/osd-charts/src/state/actions/dom_element.ts create mode 100644 packages/osd-charts/src/state/actions/events.ts create mode 100644 packages/osd-charts/src/state/actions/index.ts create mode 100644 packages/osd-charts/src/state/actions/key.ts create mode 100644 packages/osd-charts/src/state/actions/legend.ts create mode 100644 packages/osd-charts/src/state/actions/mouse.ts create mode 100644 packages/osd-charts/src/state/actions/specs.ts create mode 100644 packages/osd-charts/src/state/actions/z_index.ts create mode 100644 packages/osd-charts/src/state/chart_state.ts create mode 100644 packages/osd-charts/src/state/reducers/interactions.ts create mode 100644 packages/osd-charts/src/state/selectors/get_accessibility_config.ts create mode 100644 packages/osd-charts/src/state/selectors/get_chart_container_dimensions.ts create mode 100644 packages/osd-charts/src/state/selectors/get_chart_id.ts create mode 100644 packages/osd-charts/src/state/selectors/get_chart_rotation.ts create mode 100644 packages/osd-charts/src/state/selectors/get_chart_theme.ts create mode 100644 packages/osd-charts/src/state/selectors/get_chart_type_components.ts create mode 100644 packages/osd-charts/src/state/selectors/get_chart_type_description.ts create mode 100644 packages/osd-charts/src/state/selectors/get_debug_state.ts create mode 100644 packages/osd-charts/src/state/selectors/get_deselected_data_series.ts create mode 100644 packages/osd-charts/src/state/selectors/get_internal_brush_area.ts create mode 100644 packages/osd-charts/src/state/selectors/get_internal_cursor_pointer.ts create mode 100644 packages/osd-charts/src/state/selectors/get_internal_is_brushing.ts create mode 100644 packages/osd-charts/src/state/selectors/get_internal_is_brushing_available.ts create mode 100644 packages/osd-charts/src/state/selectors/get_internal_is_intialized.ts create mode 100644 packages/osd-charts/src/state/selectors/get_internal_is_tooltip_visible.ts create mode 100644 packages/osd-charts/src/state/selectors/get_internal_main_projection_area.ts create mode 100644 packages/osd-charts/src/state/selectors/get_internal_projection_container_area.ts create mode 100644 packages/osd-charts/src/state/selectors/get_internal_tooltip_anchor_position.ts create mode 100644 packages/osd-charts/src/state/selectors/get_internal_tooltip_info.ts create mode 100644 packages/osd-charts/src/state/selectors/get_last_click.ts create mode 100644 packages/osd-charts/src/state/selectors/get_last_drag.ts create mode 100644 packages/osd-charts/src/state/selectors/get_legend_config_selector.ts create mode 100644 packages/osd-charts/src/state/selectors/get_legend_items.ts create mode 100644 packages/osd-charts/src/state/selectors/get_legend_items_labels.ts create mode 100644 packages/osd-charts/src/state/selectors/get_legend_items_values.ts create mode 100644 packages/osd-charts/src/state/selectors/get_legend_size.ts create mode 100644 packages/osd-charts/src/state/selectors/get_settings_specs.test.ts create mode 100644 packages/osd-charts/src/state/selectors/get_settings_specs.ts create mode 100644 packages/osd-charts/src/state/selectors/get_small_multiples_spec.ts create mode 100644 packages/osd-charts/src/state/selectors/get_tooltip_header_formatter.ts create mode 100644 packages/osd-charts/src/state/selectors/has_external_pointer_event.ts create mode 100644 packages/osd-charts/src/state/selectors/is_chart_empty.ts create mode 100644 packages/osd-charts/src/state/selectors/is_external_tooltip_visible.ts create mode 100644 packages/osd-charts/src/state/spec_factory.test.tsx create mode 100644 packages/osd-charts/src/state/spec_factory.ts create mode 100644 packages/osd-charts/src/state/types.ts create mode 100644 packages/osd-charts/src/state/utils.test.ts create mode 100644 packages/osd-charts/src/state/utils.ts create mode 100644 packages/osd-charts/src/theme_dark.scss create mode 100644 packages/osd-charts/src/theme_light.scss create mode 100644 packages/osd-charts/src/theme_only_dark.scss create mode 100644 packages/osd-charts/src/theme_only_light.scss create mode 100644 packages/osd-charts/src/utils/__mocks__/common.ts create mode 100644 packages/osd-charts/src/utils/accessor.ts create mode 100644 packages/osd-charts/src/utils/bbox/bbox_calculator.ts create mode 100644 packages/osd-charts/src/utils/bbox/canvas_text_bbox_calculator.test.ts create mode 100644 packages/osd-charts/src/utils/bbox/canvas_text_bbox_calculator.ts create mode 100644 packages/osd-charts/src/utils/bbox/dom_text_bbox_calculator.ts create mode 100644 packages/osd-charts/src/utils/bbox/svg_text_bbox_calculator.ts create mode 100644 packages/osd-charts/src/utils/chart_size.test.ts create mode 100644 packages/osd-charts/src/utils/chart_size.ts create mode 100644 packages/osd-charts/src/utils/common.test.ts create mode 100644 packages/osd-charts/src/utils/common.tsx create mode 100644 packages/osd-charts/src/utils/curves.test.ts create mode 100644 packages/osd-charts/src/utils/curves.ts create mode 100644 packages/osd-charts/src/utils/d3-delaunay/index.ts create mode 100644 packages/osd-charts/src/utils/data/date_time.ts create mode 100644 packages/osd-charts/src/utils/data/formatters.ts create mode 100644 packages/osd-charts/src/utils/data/formatters.tz.test.ts create mode 100644 packages/osd-charts/src/utils/data/formatters.tz.test.utc.ts create mode 100644 packages/osd-charts/src/utils/data_generators/data_generator.ts create mode 100644 packages/osd-charts/src/utils/data_generators/simple_noise.ts create mode 100644 packages/osd-charts/src/utils/data_samples/4_time_series.json create mode 100644 packages/osd-charts/src/utils/data_samples/babynames.ts create mode 100644 packages/osd-charts/src/utils/data_samples/test_anomaly_swim_lane.ts create mode 100644 packages/osd-charts/src/utils/data_samples/test_dataset.ts create mode 100644 packages/osd-charts/src/utils/data_samples/test_dataset_github.ts create mode 100644 packages/osd-charts/src/utils/data_samples/test_dataset_kibana.ts create mode 100644 packages/osd-charts/src/utils/data_samples/test_dataset_random.ts create mode 100644 packages/osd-charts/src/utils/data_samples/test_dataset_tsvb.ts create mode 100644 packages/osd-charts/src/utils/dimensions.ts create mode 100644 packages/osd-charts/src/utils/domain.test.ts create mode 100644 packages/osd-charts/src/utils/domain.ts create mode 100644 packages/osd-charts/src/utils/events.ts create mode 100644 packages/osd-charts/src/utils/fast_deep_equal.ts create mode 100644 packages/osd-charts/src/utils/geometry.ts create mode 100644 packages/osd-charts/src/utils/ids.test.ts create mode 100644 packages/osd-charts/src/utils/ids.ts create mode 100644 packages/osd-charts/src/utils/legend.ts create mode 100644 packages/osd-charts/src/utils/logger.ts create mode 100644 packages/osd-charts/src/utils/point.ts create mode 100644 packages/osd-charts/src/utils/series_sort.ts create mode 100644 packages/osd-charts/src/utils/themes/colors.ts create mode 100644 packages/osd-charts/src/utils/themes/dark_theme.ts create mode 100644 packages/osd-charts/src/utils/themes/light_theme.ts create mode 100644 packages/osd-charts/src/utils/themes/merge_utils.ts create mode 100644 packages/osd-charts/src/utils/themes/theme.test.ts create mode 100644 packages/osd-charts/src/utils/themes/theme.ts create mode 100644 packages/osd-charts/src/utils/themes/theme_common.ts create mode 100644 packages/osd-charts/tsconfig.check.json create mode 100644 packages/osd-charts/tsconfig.json create mode 100644 packages/osd-charts/tsconfig.nocomments.json diff --git a/packages/osd-charts/.gitignore b/packages/osd-charts/.gitignore new file mode 100644 index 000000000000..bd5df212b8ef --- /dev/null +++ b/packages/osd-charts/.gitignore @@ -0,0 +1,6 @@ +# Copied files to include in final package +# remove when bundled other than tsc +LICENSE.txt +CHANGELOG.md +NOTICE.txt +README.md diff --git a/packages/osd-charts/.npmignore b/packages/osd-charts/.npmignore new file mode 100644 index 000000000000..514435808ab9 --- /dev/null +++ b/packages/osd-charts/.npmignore @@ -0,0 +1 @@ +src/mocks diff --git a/packages/osd-charts/api-extractor.jsonc b/packages/osd-charts/api-extractor.jsonc new file mode 100644 index 000000000000..e72bc020b65f --- /dev/null +++ b/packages/osd-charts/api-extractor.jsonc @@ -0,0 +1,366 @@ +/** + * Config file for API Extractor. For more info, please visit: https://api-extractor.com + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "newlineKind": "lf", + + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for + * standard settings to be shared across multiple projects. + * + * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains + * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be + * resolved using NodeJS require(). + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + // "extends": "./shared/api-extractor-base.json" + // "extends": "my-package/include/api-extractor-base.json" + + /** + * Determines the "" token that can be used with other config file settings. The project folder + * typically contains the tsconfig.json and package.json config files, but the path is user-defined. + * + * The path is resolved relative to the folder of the config file that contains the setting. + * + * The default value for "projectFolder" is the token "", which means the folder is determined by traversing + * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder + * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error + * will be reported. + * + * SUPPORTED TOKENS: + * DEFAULT VALUE: "" + */ + "projectFolder": "./", + + /** + * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor + * analyzes the symbols exported by this module. + * + * The file extension must be ".d.ts" and not ".ts". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + */ + "mainEntryPointFilePath": "/dist/index.d.ts", + + /** + * A list of NPM package names whose exports should be treated as part of this package. + * + * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", + * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part + * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly + * imports library2. To avoid this, we can specify: + * + * "bundledPackages": [ "library2" ], + * + * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been + * local files for library1. + */ + "bundledPackages": [], + + /** + * Determines how the TypeScript compiler engine will be invoked by API Extractor. + */ + "compiler": { + /** + * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * Note: This setting will be ignored if "overrideTsconfig" is used. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/tsconfig.json" + */ + "tsconfigFilePath": "/tsconfig.json", + /** + * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. + * The object must conform to the TypeScript tsconfig schema: + * + * http://json.schemastore.org/tsconfig + * + * If omitted, then the tsconfig.json file will be read from the "projectFolder". + * + * DEFAULT VALUE: no overrideTsconfig section + */ + // "overrideTsconfig": { + // . . . + // } + /** + * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended + * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when + * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses + * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. + * + * DEFAULT VALUE: false + */ + "skipLibCheck": false + }, + + /** + * Configures how the API report file (*.api.md) will be generated. + */ + "apiReport": { + /** + * (REQUIRED) Whether to generate an API report. + */ + "enabled": true, + + /** + * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce + * a full file path. + * + * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". + * + * SUPPORTED TOKENS: , + * DEFAULT VALUE: ".api.md" + */ + // "reportFileName": ".api.md", + + /** + * Specifies the folder where the API report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, + * e.g. for an API review. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/etc/" + */ + "reportFolder": "/api/", + + /** + * Specifies the folder where the temporary report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * After the temporary file is written to disk, it is compared with the file in the "reportFolder". + * If they are different, a production build will fail. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/" + */ + "reportTempFolder": "/tmp/" + }, + + /** + * Configures how the doc model file (*.api.json) will be generated. + */ + "docModel": { + /** + * (REQUIRED) Whether to generate a doc model file. + */ + "enabled": false + + /** + * The output path for the doc model file. The file extension should be ".api.json". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/.api.json" + */ + // "apiJsonFilePath": "/temp/.api.json" + }, + + /** + * Configures how the .d.ts rollup file will be generated. + */ + "dtsRollup": { + /** + * (REQUIRED) Whether to generate the .d.ts rollup file. + */ + "enabled": false, + + /** + * Specifies the output path for a .d.ts rollup file to be generated without any trimming. + * This file will include all declarations that are exported by the main entry point. + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/dist/.d.ts" + */ + "untrimmedFilePath": "/dist/.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. + * This file will include only declarations that are marked as "@public" or "@beta". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + "betaTrimmedFilePath": "/dist/-beta.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. + * This file will include only declarations that are marked as "@public". + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + "publicTrimmedFilePath": "/dist/-public.d.ts" + + /** + * When a declaration is trimmed, by default it will be replaced by a code comment such as + * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the + * declaration completely. + * + * DEFAULT VALUE: false + */ + // "omitTrimmingComments": true + }, + + /** + * Configures how the tsdoc-metadata.json file will be generated. + */ + "tsdocMetadata": { + /** + * Whether to generate the tsdoc-metadata.json file. + * + * DEFAULT VALUE: true + */ + "enabled": true + /** + * Specifies where the TSDoc metadata file should be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", + * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup + * falls back to "tsdoc-metadata.json" in the package folder. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" + }, + + /** + * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files + * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. + * To use the OS's default newline kind, specify "os". + * + * DEFAULT VALUE: "crlf" + */ + // "newlineKind": "crlf", + + /** + * Configures how API Extractor reports error and warning messages produced during analysis. + * + * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. + */ + "messages": { + /** + * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing + * the input .d.ts files. + * + * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "compilerMessageReporting": { + /** + * Configures the default routing for messages that don't match an explicit rule in this table. + */ + "default": { + /** + * Specifies whether the message should be written to the the tool's output log. Note that + * the "addToApiReportFile" property may supersede this option. + * + * Possible values: "error", "warning", "none" + * + * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail + * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes + * the "--local" option), the warning is displayed but the build will not fail. + * + * DEFAULT VALUE: "warning" + */ + "logLevel": "warning" + + /** + * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), + * then the message will be written inside that file; otherwise, the message is instead logged according to + * the "logLevel" option. + * + * DEFAULT VALUE: false + */ + // "addToApiReportFile": false + } + + // "TS2551": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by API Extractor during its analysis. + * + * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" + * + * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings + */ + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + // "addToApiReportFile": false + }, + "ae-extra-release-tag": { + "logLevel": "warning", + "addToApiReportFile": true + }, + "ae-missing-release-tag": { + "logLevel": "warning", + "addToApiReportFile": true + } + }, + + /** + * Configures handling of messages reported by the TSDoc parser when analyzing code comments. + * + * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + // "addToApiReportFile": false + } + + // "tsdoc-link-tag-unescaped-text": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + } + } +} diff --git a/packages/osd-charts/api/charts.api.md b/packages/osd-charts/api/charts.api.md new file mode 100644 index 000000000000..2d6a0e5c96a0 --- /dev/null +++ b/packages/osd-charts/api/charts.api.md @@ -0,0 +1,2340 @@ +## API Report File for "@elastic/charts" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { $Values } from 'utility-types'; +import { ComponentType } from 'react'; +import { default as React_2 } from 'react'; +import { ReactChild } from 'react'; +import { ReactNode } from 'react'; + +// @public +export type Accessor = AccessorObjectKey | AccessorArrayIndex; + +// @public +export type AccessorArrayIndex = number; + +// @public +export type AccessorFn = UnaryAccessorFn; + +// @public +export type AccessorObjectKey = string; + +// @public +export type AdditiveNumber = number; + +// @public (undocumented) +export const AGGREGATE_KEY = "value"; + +// @public (undocumented) +export function aggregateAccessor(n: ArrayEntry): number; + +// @public (undocumented) +export interface AngleFromTo { + // Warning: (ae-forgotten-export) The symbol "Radian" needs to be exported by the entry point index.d.ts + // + // (undocumented) + x0: Radian; + // (undocumented) + x1: Radian; +} + +// @public +export const AnnotationDomainType: Readonly<{ + XDomain: "xDomain"; + YDomain: "yDomain"; +}>; + +// @public +export type AnnotationDomainType = $Values; + +// @public (undocumented) +export type AnnotationId = string; + +// @public +export type AnnotationPortalSettings = TooltipPortalSettings<'chart'> & { + customTooltip?: CustomAnnotationTooltip; + customTooltipDetails?: AnnotationTooltipFormatter; +}; + +// @public (undocumented) +export type AnnotationSpec = LineAnnotationSpec | RectAnnotationSpec; + +// @public (undocumented) +export type AnnotationTooltipFormatter = (details?: string) => JSX.Element | null; + +// @public (undocumented) +export const AnnotationType: Readonly<{ + Line: "line"; + Rectangle: "rectangle"; + Text: "text"; +}>; + +// @public (undocumented) +export type AnnotationType = $Values; + +// @public (undocumented) +export interface ArcSeriesStyle { + // (undocumented) + arc: ArcStyle; +} + +// @public (undocumented) +export interface ArcStyle { + fill?: Color | ColorVariant; + opacity: number; + stroke?: Color | ColorVariant; + strokeWidth: number; + visible: boolean; +} + +// Warning: (ae-forgotten-export) The symbol "SpecRequiredProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "SpecOptionalProps" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export const AreaSeries: React_2.FunctionComponent; + +// @public +export type AreaSeriesSpec = BasicSeriesSpec & HistogramConfig & Postfixes & { + seriesType: typeof SeriesType.Area; + curve?: CurveType; + areaSeriesStyle?: RecursivePartial; + stackMode?: StackMode; + pointStyleAccessor?: PointStyleAccessor; + fit?: Exclude | FitConfig; +}; + +// @public (undocumented) +export interface AreaSeriesStyle { + // (undocumented) + area: AreaStyle; + // (undocumented) + line: LineStyle; + // (undocumented) + point: PointStyle; +} + +// @public (undocumented) +export interface AreaStyle { + fill?: Color | ColorVariant; + opacity: number; + texture?: TexturedStyles; + visible: boolean; +} + +// @public (undocumented) +export type ArrayEntry = [Key, ArrayNode]; + +// @public (undocumented) +export interface ArrayNode extends NodeDescriptor { + // (undocumented) + [CHILDREN_KEY]: HierarchyOfArrays; + // (undocumented) + [PARENT_KEY]: ArrayNode; + // (undocumented) + [PATH_KEY]: LegendPath; + // (undocumented) + [SORT_INDEX_KEY]: number; +} + +// Warning: (ae-forgotten-export) The symbol "SpecRequired" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "SpecOptionals" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export const Axis: React_2.FunctionComponent; + +// @public (undocumented) +export type AxisId = string; + +// @public +export interface AxisSpec extends Spec { + // (undocumented) + chartType: typeof ChartType.XYAxis; + domain?: YDomainRange; + gridLine?: Partial; + groupId: GroupId; + hide: boolean; + id: AxisId; + integersOnly?: boolean; + labelFormat?: TickFormatter; + position: Position; + showDuplicatedTicks?: boolean; + // @deprecated + showGridLines?: boolean; + showOverlappingLabels: boolean; + showOverlappingTicks: boolean; + // (undocumented) + specType: typeof SpecType.Axis; + style?: RecursivePartial>; + tickFormat?: TickFormatter; + ticks?: number; + title?: string; +} + +// @public (undocumented) +export interface AxisStyle { + // (undocumented) + axisLine: StrokeStyle & Visible; + // (undocumented) + axisPanelTitle: TextStyle & Visible; + // (undocumented) + axisTitle: TextStyle & Visible; + // (undocumented) + gridLine: { + horizontal: GridLineStyle; + vertical: GridLineStyle; + }; + // (undocumented) + tickLabel: TextStyle & Visible & { + rotation: number; + offset: TextOffset; + alignment: TextAlignment; + }; + // (undocumented) + tickLine: TickStyle; +} + +// @public +export interface BackgroundStyle { + color: string; +} + +// @public +export const BandedAccessorType: Readonly<{ + Y0: "y0"; + Y1: "y1"; +}>; + +// @public (undocumented) +export type BandedAccessorType = $Values; + +// @alpha (undocumented) +export type BandFillColorAccessor = (input: BandFillColorAccessorInput) => Color; + +// @alpha (undocumented) +export interface BandFillColorAccessorInput { + // (undocumented) + aboveBaseCount: number; + // (undocumented) + base: number; + // (undocumented) + belowBaseCount: number; + // (undocumented) + highestValue: number; + // (undocumented) + index: number; + // (undocumented) + lowestValue: number; + // (undocumented) + target: number; + // (undocumented) + value: number; +} + +// Warning: (ae-forgotten-export) The symbol "SpecRequiredProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "SpecOptionalProps" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export const BarSeries: React_2.FunctionComponent; + +// @public +export type BarSeriesSpec = BasicSeriesSpec & Postfixes & { + seriesType: typeof SeriesType.Bar; + enableHistogramMode?: boolean; + barSeriesStyle?: RecursivePartial; + stackMode?: StackMode; + styleAccessor?: BarStyleAccessor; + minBarHeight?: number; +}; + +// @public (undocumented) +export interface BarSeriesStyle { + // (undocumented) + displayValue: DisplayValueStyle; + // (undocumented) + rect: RectStyle; + // (undocumented) + rectBorder: RectBorderStyle; +} + +// @public +export type BarStyleAccessor = (datum: DataSeriesDatum, seriesIdentifier: XYChartSeriesIdentifier) => BarStyleOverride; + +// @public (undocumented) +export type BarStyleOverride = RecursivePartial | Color | null; + +// @public (undocumented) +export interface BaseAnnotationSpec extends Spec, AnnotationPortalSettings { + annotationType: T; + // (undocumented) + chartType: typeof ChartType.XYAxis; + dataValues: D[]; + groupId: GroupId; + hideTooltips?: boolean; + // (undocumented) + specType: typeof SpecType.Annotation; + style?: Partial; + zIndex?: number; +} + +// @public (undocumented) +export interface BasePointerEvent { + // (undocumented) + chartId: string; + // (undocumented) + type: PointerEventType; +} + +// @public (undocumented) +export type BasicListener = () => undefined | void; + +// @public (undocumented) +export type BasicSeriesSpec = SeriesSpec & SeriesAccessors & SeriesScales & { + markFormat?: TickFormatter; +}; + +// @public +export const BinAgg: Readonly<{ + Sum: "sum"; + None: "none"; +}>; + +// @public (undocumented) +export type BinAgg = $Values; + +// @public (undocumented) +export const BrushAxis: Readonly<{ + X: "x"; + Y: "y"; + Both: "both"; +}>; + +// @public (undocumented) +export type BrushAxis = $Values; + +// @public (undocumented) +export type BrushEndListener = (brushArea: XYBrushArea) => void; + +// Warning: (ae-forgotten-export) The symbol "SpecRequiredProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "SpecOptionalProps" needs to be exported by the entry point index.d.ts +// +// @alpha +export const BubbleSeries: React_2.FunctionComponent; + +// @alpha +export type BubbleSeriesSpec = BasicSeriesSpec & { + seriesType: typeof SeriesType.Bubble; + bubbleSeriesStyle?: RecursivePartial; + pointStyleAccessor?: PointStyleAccessor; +}; + +// @public (undocumented) +export interface BubbleSeriesStyle { + // (undocumented) + point: PointStyle; +} + +// @public (undocumented) +export type CategoryKey = string; + +// @public (undocumented) +export interface Cell { + // Warning: (ae-forgotten-export) The symbol "HeatmapCellDatum" needs to be exported by the entry point index.d.ts + // + // (undocumented) + datum: HeatmapCellDatum; + // Warning: (ae-forgotten-export) The symbol "Fill" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fill: Fill; + // (undocumented) + formatted: string; + // (undocumented) + height: number; + // Warning: (ae-forgotten-export) The symbol "Stroke" needs to be exported by the entry point index.d.ts + // + // (undocumented) + stroke: Stroke; + // (undocumented) + value: number; + // (undocumented) + visible: boolean; + // (undocumented) + width: number; + // (undocumented) + x: number; + // (undocumented) + y: number; + // (undocumented) + yIndex: number; +} + +// Warning: (ae-forgotten-export) The symbol "ChartProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ChartState" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export class Chart extends React_2.Component { + constructor(props: ChartProps); + // (undocumented) + componentDidMount(): void; + // (undocumented) + componentWillUnmount(): void; + // (undocumented) + static defaultProps: ChartProps; + // (undocumented) + dispatchExternalPointerEvent(event: PointerEvent_2): void; + // (undocumented) + getChartContainerRef: () => React_2.RefObject; + // (undocumented) + getPNGSnapshot(options?: { + backgroundColor: string; + pixelRatio: number; + }): { + blobOrDataUrl: any; + browser: 'IE11' | 'other'; + } | null; + // (undocumented) + render(): JSX.Element; + } + +// @public (undocumented) +export type ChartSize = number | string | ChartSizeArray | ChartSizeObject; + +// @public (undocumented) +export type ChartSizeArray = [number | string | undefined, number | string | undefined]; + +// @public (undocumented) +export interface ChartSizeObject { + // (undocumented) + height?: number | string; + // (undocumented) + width?: number | string; +} + +// @public +export const ChartType: Readonly<{ + Global: "global"; + Goal: "goal"; + Partition: "partition"; + XYAxis: "xy_axis"; + Heatmap: "heatmap"; + Wordcloud: "wordcloud"; +}>; + +// @public (undocumented) +export type ChartType = $Values; + +// @public (undocumented) +export const CHILDREN_KEY = "children"; + +// @public (undocumented) +export function childrenAccessor(n: ArrayEntry): HierarchyOfArrays; + +// @public (undocumented) +export type Color = string; + +// @public (undocumented) +export interface ColorConfig { + // (undocumented) + defaultVizColor: Color; + // (undocumented) + vizColors: Color[]; +} + +// @public +export const ColorVariant: Readonly<{ + Series: "__use__series__color__"; + None: "__use__empty__color__"; +}>; + +// @public (undocumented) +export type ColorVariant = $Values; + +// Warning: (ae-forgotten-export) The symbol "DomainBase" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "LowerBound" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UpperBound" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type CompleteBoundedDomain = DomainBase & LowerBound & UpperBound; + +// @public +export type ComponentWithAnnotationDatum = ComponentType; + +// @public (undocumented) +export type ContinuousDomain = [min: number, max: number]; + +// @public (undocumented) +export interface CrosshairStyle { + // (undocumented) + band: FillStyle & Visible; + // (undocumented) + crossLine: StrokeStyle & Visible & Partial; + // (undocumented) + line: StrokeStyle & Visible & Partial; +} + +// @public (undocumented) +export const CurveType: Readonly<{ + CURVE_CARDINAL: 0; + CURVE_NATURAL: 1; + CURVE_MONOTONE_X: 2; + CURVE_MONOTONE_Y: 3; + CURVE_BASIS: 4; + CURVE_CATMULL_ROM: 5; + CURVE_STEP: 6; + CURVE_STEP_AFTER: 7; + CURVE_STEP_BEFORE: 8; + LINEAR: 9; +}>; + +// @public (undocumented) +export type CurveType = $Values; + +// @public (undocumented) +export type CustomAnnotationTooltip = ComponentType<{ + header?: string; + details?: string; + datum: LineAnnotationDatum | RectAnnotationDatum; +}> | null; + +// @public +export type CustomTooltip = ComponentType; + +// @public (undocumented) +export type CustomXDomain = (DomainRange & Pick) | OrdinalDomain; + +// @public (undocumented) +export const DARK_THEME: Theme; + +// @public (undocumented) +export class DataGenerator { + // Warning: (ae-forgotten-export) The symbol "RandomNumberGenerator" needs to be exported by the entry point index.d.ts + constructor(frequency?: number, randomNumberGenerator?: RandomNumberGenerator); + // (undocumented) + generateBasicSeries(totalPoints?: number, offset?: number, amplitude?: number): { + x: number; + y: number; + }[]; + // (undocumented) + generateGroupedSeries(totalPoints?: number, totalGroups?: number, groupPrefix?: string): { + x: number; + y: number; + g: string; + }[]; + // (undocumented) + generateRandomGroupedSeries(totalPoints?: number, totalGroups?: number, groupPrefix?: string): { + x: number; + y: number; + z: number; + g: string; + }[]; + // (undocumented) + generateRandomSeries(totalPoints?: number, groupIndex?: number, groupPrefix?: string): { + x: number; + y: number; + z: number; + g: string; + }[]; + // (undocumented) + generateSimpleSeries(totalPoints?: number, groupIndex?: number, groupPrefix?: string): { + x: number; + y: number; + g: string; + }[]; + } + +// @public (undocumented) +export type DataName = CategoryKey; + +// @public (undocumented) +export interface DataSeriesDatum { + datum: T; + filled?: FilledValues; + initialY0: number | null; + initialY1: number | null; + mark: number | null; + x: number | string; + y0: number | null; + y1: number | null; +} + +// @public (undocumented) +export type Datum = any; + +// @public +export interface DebugState { + // Warning: (ae-forgotten-export) The symbol "DebugStateArea" needs to be exported by the entry point index.d.ts + // + // (undocumented) + areas?: DebugStateArea[]; + // Warning: (ae-forgotten-export) The symbol "DebugStateAxes" needs to be exported by the entry point index.d.ts + // + // (undocumented) + axes?: DebugStateAxes; + // Warning: (ae-forgotten-export) The symbol "DebugStateBar" needs to be exported by the entry point index.d.ts + // + // (undocumented) + bars?: DebugStateBar[]; + // Warning: (ae-forgotten-export) The symbol "HeatmapDebugState" needs to be exported by the entry point index.d.ts + heatmap?: HeatmapDebugState; + // Warning: (ae-forgotten-export) The symbol "DebugStateLegend" needs to be exported by the entry point index.d.ts + // + // (undocumented) + legend?: DebugStateLegend; + // Warning: (ae-forgotten-export) The symbol "DebugStateLine" needs to be exported by the entry point index.d.ts + // + // (undocumented) + lines?: DebugStateLine[]; + // Warning: (ae-forgotten-export) The symbol "PartitionDebugState" needs to be exported by the entry point index.d.ts + // + // (undocumented) + partition?: PartitionDebugState[]; +} + +// @public (undocumented) +export const DEFAULT_ANNOTATION_LINE_STYLE: LineAnnotationStyle; + +// @public (undocumented) +export const DEFAULT_ANNOTATION_RECT_STYLE: RectAnnotationStyle; + +// Warning: (ae-forgotten-export) The symbol "Margins" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export const DEFAULT_CHART_MARGINS: Margins; + +// @public (undocumented) +export const DEFAULT_CHART_PADDING: Margins; + +// @public (undocumented) +export const DEFAULT_GEOMETRY_STYLES: SharedGeometryStateStyle; + +// @public +export const DEFAULT_GLOBAL_ID = "__global__"; + +// @public (undocumented) +export const DEFAULT_MISSING_COLOR = "red"; + +// @public (undocumented) +export const DEFAULT_SETTINGS_SPEC: SettingsSpec; + +// @public +export const DEFAULT_TOOLTIP_SNAP = true; + +// @public +export const DEFAULT_TOOLTIP_TYPE: "vertical"; + +// @public (undocumented) +export type DefaultSettingsProps = 'id' | 'chartType' | 'specType' | 'rendering' | 'rotation' | 'resizeDebounce' | 'animateData' | 'debug' | 'tooltip' | 'theme' | 'hideDuplicateAxes' | 'brushAxis' | 'minBrushDelta' | 'externalPointerEvents' | 'showLegend' | 'showLegendExtra' | 'legendPosition' | 'legendMaxDepth' | 'ariaUseDefaultSummary' | 'ariaLabelHeadingLevel'; + +// @public (undocumented) +export const DEPTH_KEY = "depth"; + +// @public (undocumented) +export function depthAccessor(n: ArrayEntry): number; + +// @public +export const Direction: Readonly<{ + Ascending: "ascending"; + Descending: "descending"; +}>; + +// @public (undocumented) +export type Direction = $Values; + +// @public (undocumented) +export interface DisplayValueSpec { + hideClippedValue?: boolean; + isAlternatingValueLabel?: boolean; + isValueContainedInElement?: boolean; + showValueLabel?: boolean; + valueFormatter?: TickFormatter; +} + +// @public (undocumented) +export type DisplayValueStyle = Omit & { + offsetX: number; + offsetY: number; + fontSize: number | { + min: number; + max: number; + }; + fill: Color | { + color: Color; + borderColor?: Color; + borderWidth?: number; + } | { + textInvertible: boolean; + textContrast?: number | boolean; + textBorder?: number; + }; + alignment?: { + horizontal: Exclude; + vertical: Exclude; + }; +}; + +// @public +export const DomainPaddingUnit: Readonly<{ + Domain: "domain"; + Pixel: "pixel"; + DomainRatio: "domainRatio"; +}>; + +// @public +export type DomainPaddingUnit = $Values; + +// @public (undocumented) +export type DomainRange = LowerBoundedDomain | UpperBoundedDomain | CompleteBoundedDomain | UnboundedDomainWithInterval; + +// @public (undocumented) +export type ElementClickListener = (elements: Array) => void; + +// @public (undocumented) +export type ElementOverListener = (elements: Array) => void; + +// @public (undocumented) +export const entryKey: ([key]: ArrayEntry) => string; + +// @public (undocumented) +export const entryValue: ([, value]: ArrayEntry) => ArrayNode; + +// @alpha +export interface ExternalPointerEventsSettings { + tooltip: TooltipPortalSettings<'chart'> & { + visible?: boolean; + }; +} + +// @public (undocumented) +export interface FilledValues { + x?: number | string; + y0?: number; + y1?: number; +} + +// @public (undocumented) +export interface FillStyle { + fill: Color; +} + +// @public (undocumented) +export type FilterPredicate = (series: XYChartSeriesIdentifier) => boolean; + +// @public +export const Fit: Readonly<{ + None: "none"; + Carry: "carry"; + Lookahead: "lookahead"; + Nearest: "nearest"; + Average: "average"; + Linear: "linear"; + Zero: "zero"; + Explicit: "explicit"; +}>; + +// @public (undocumented) +export type Fit = $Values; + +// @public (undocumented) +export type FitConfig = { + type: Fit; + value?: number; + endValue?: number | 'nearest'; +}; + +// @public +export interface GeometryStateStyle { + opacity: number; +} + +// @public +export interface GeometryStyle { + opacity: number; +} + +// @public (undocumented) +export interface GeometryValue { + // (undocumented) + accessor: BandedAccessorType; + datum: any; + // (undocumented) + mark: number | null; + // (undocumented) + x: any; + // (undocumented) + y: any; +} + +// @public (undocumented) +export function getNodeName(node: ArrayNode): string; + +// Warning: (ae-forgotten-export) The symbol "SpecRequiredProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "SpecOptionalProps" needs to be exported by the entry point index.d.ts +// +// @alpha (undocumented) +export const Goal: React_2.FunctionComponent; + +// @alpha (undocumented) +export interface GoalSpec extends Spec { + // (undocumented) + actual: number; + // (undocumented) + bandFillColor: BandFillColorAccessor; + // (undocumented) + bands: number[]; + // (undocumented) + base: number; + // (undocumented) + centralMajor: string | BandFillColorAccessor; + // (undocumented) + centralMinor: string | BandFillColorAccessor; + // (undocumented) + chartType: typeof ChartType.Goal; + // Warning: (ae-forgotten-export) The symbol "Config" needs to be exported by the entry point index.d.ts + // + // (undocumented) + config: RecursivePartial; + // (undocumented) + labelMajor: string | BandFillColorAccessor; + // (undocumented) + labelMinor: string | BandFillColorAccessor; + // (undocumented) + specType: typeof SpecType.Series; + // Warning: (ae-forgotten-export) The symbol "GoalSubtype" needs to be exported by the entry point index.d.ts + // + // (undocumented) + subtype: GoalSubtype; + // (undocumented) + target: number; + // (undocumented) + ticks: number[]; + // (undocumented) + tickValueFormatter: BandFillColorAccessor; +} + +// @public (undocumented) +export interface GridLineStyle { + // (undocumented) + dash: number[]; + // (undocumented) + opacity: number; + // (undocumented) + stroke: Color; + // (undocumented) + strokeWidth: number; + // (undocumented) + visible: boolean; +} + +// @public (undocumented) +export interface GroupBrushExtent { + // (undocumented) + extent: [number, number]; + // (undocumented) + groupId: GroupId; +} + +// @alpha (undocumented) +export const GroupBy: React_2.FunctionComponent; + +// @public (undocumented) +export type GroupByAccessor = (spec: Spec, datum: any) => string | number; + +// @public +export type GroupByFormatter = (value: ReturnType) => string; + +// @alpha (undocumented) +export type GroupByProps = Pick; + +// Warning: (ae-forgotten-export) The symbol "Predicate" needs to be exported by the entry point index.d.ts +// +// @alpha (undocumented) +export type GroupBySort = Predicate; + +// @alpha (undocumented) +export interface GroupBySpec extends Spec { + by: GroupByAccessor; + format?: GroupByFormatter; + sort: GroupBySort; +} + +// @public (undocumented) +export type GroupId = string; + +// @alpha (undocumented) +export const Heatmap: React_2.FunctionComponent & Partial>>; + +// @public (undocumented) +export type HeatmapBrushEvent = { + cells: Cell[]; + x: (string | number)[]; + y: (string | number)[]; +}; + +// @public (undocumented) +export interface HeatmapConfig { + brushArea: { + visible: boolean; + fill: Color; + stroke: Color; + strokeWidth: number; + }; + brushMask: { + visible: boolean; + fill: Color; + }; + brushTool: { + visible: boolean; + fill: Color; + }; + // (undocumented) + cell: { + maxWidth: Pixels | 'fill'; + maxHeight: Pixels | 'fill'; + align: 'center'; + label: Font & { + fontSize: Pixels; + maxWidth: Pixels | 'fill'; + fill: string; + align: TextAlign; + baseline: TextBaseline; + visible: boolean; + }; + border: { + strokeWidth: Pixels; + stroke: Color; + }; + }; + // Warning: (ae-forgotten-export) The symbol "FontFamily" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fontFamily: FontFamily; + // (undocumented) + grid: { + cellWidth: { + min: Pixels; + max: Pixels | 'fill'; + }; + cellHeight: { + min: Pixels; + max: Pixels | 'fill'; + }; + stroke: { + color: string; + width: number; + }; + }; + // (undocumented) + height: Pixels; + // (undocumented) + margin: { + left: SizeRatio; + right: SizeRatio; + top: SizeRatio; + bottom: SizeRatio; + }; + // (undocumented) + maxColumnWidth: Pixels; + // (undocumented) + maxLegendHeight?: number; + // (undocumented) + maxRowHeight: Pixels; + // (undocumented) + onBrushEnd?: (brushArea: HeatmapBrushEvent) => void; + // (undocumented) + timeZone: string; + // Warning: (ae-forgotten-export) The symbol "Pixels" needs to be exported by the entry point index.d.ts + // + // (undocumented) + width: Pixels; + // Warning: (ae-forgotten-export) The symbol "Font" needs to be exported by the entry point index.d.ts + // + // (undocumented) + xAxisLabel: Font & { + name: string; + fontSize: Pixels; + width: Pixels | 'auto'; + fill: string; + align: TextAlign; + baseline: TextBaseline; + visible: boolean; + padding: number; + formatter: (value: string | number) => string; + }; + // (undocumented) + yAxisLabel: Font & { + name: string; + fontSize: Pixels; + width: Pixels | 'auto' | { + max: Pixels; + }; + fill: string; + baseline: TextBaseline; + visible: boolean; + padding: number | { + left?: number; + right?: number; + top?: number; + bottom?: number; + }; + formatter: (value: string | number) => string; + }; +} + +// @public (undocumented) +export type HeatmapElementEvent = [Cell, SeriesIdentifier]; + +// @alpha (undocumented) +export interface HeatmapSpec extends Spec { + // (undocumented) + chartType: typeof ChartType.Heatmap; + // (undocumented) + colors: Color[]; + // Warning: (ae-forgotten-export) The symbol "HeatmapScaleType" needs to be exported by the entry point index.d.ts + // + // (undocumented) + colorScale?: HeatmapScaleType; + // (undocumented) + config: RecursivePartial; + // (undocumented) + data: Datum[]; + // (undocumented) + highlightedData?: { + x: Array; + y: Array; + }; + // (undocumented) + name?: string; + // (undocumented) + ranges?: number[] | [number, number]; + // (undocumented) + specType: typeof SpecType.Series; + // (undocumented) + valueAccessor: Accessor | AccessorFn; + // (undocumented) + valueFormatter: (value: number) => string; + // (undocumented) + xAccessor: Accessor | AccessorFn; + // (undocumented) + xScaleType: SeriesScales['xScaleType']; + // (undocumented) + xSortPredicate: Predicate; + // (undocumented) + yAccessor: Accessor | AccessorFn; + // (undocumented) + ySortPredicate: Predicate; +} + +// @public (undocumented) +export type HierarchyOfArrays = Array; + +// Warning: (ae-forgotten-export) The symbol "SpecRequiredProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "SpecOptionalProps" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export const HistogramBarSeries: React_2.FunctionComponent; + +// @public +export type HistogramBarSeriesSpec = Omit & { + enableHistogramMode: true; +}; + +// @public (undocumented) +export interface HistogramConfig { + histogramModeAlignment?: HistogramModeAlignment; +} + +// @public (undocumented) +export type HistogramModeAlignment = 'start' | 'center' | 'end'; + +// @public (undocumented) +export const HistogramModeAlignments: Readonly<{ + Start: LineAlignSetting; + Center: LineAlignSetting; + End: LineAlignSetting; +}>; + +// @public (undocumented) +export const HorizontalAlignment: Readonly<{ + Center: "center"; + Right: "right"; + Left: "left"; + Near: "near"; + Far: "far"; +}>; + +// @public +export type HorizontalAlignment = $Values; + +// Warning: (ae-forgotten-export) The symbol "BinaryAccessorFn" needs to be exported by the entry point index.d.ts +// +// @public +export type IndexedAccessorFn = UnaryAccessorFn | BinaryAccessorFn; + +// @public (undocumented) +export const INPUT_KEY = "inputIndex"; + +// @public +export type IsAny = True | False extends (T extends never ? True : False) ? True : False; + +// @public +export type IsUnknown = unknown extends T ? IsAny : False; + +// @public (undocumented) +export type Key = CategoryKey; + +// @public (undocumented) +export type LabelAccessor = (value: PrimitiveValue) => string; + +// @public (undocumented) +export interface LayerValue { + depth: number; + groupByRollup: PrimitiveValue; + path: LegendPath; + smAccessorValue: ReturnType; + sortIndex: number; + value: number; +} + +// @public (undocumented) +export const LayoutDirection: Readonly<{ + Horizontal: "horizontal"; + Vertical: "vertical"; +}>; + +// @public (undocumented) +export type LayoutDirection = $Values; + +// @public +export type LegendAction = ComponentType; + +// @public +export interface LegendActionProps { + color: string; + label: string; + series: SeriesIdentifier[]; +} + +// @public (undocumented) +export type LegendColorPicker = ComponentType; + +// @public (undocumented) +export interface LegendColorPickerProps { + anchor: HTMLElement; + color: Color; + onChange: (color: Color | null) => void; + onClose: () => void; + seriesIdentifiers: SeriesIdentifier[]; +} + +// @public (undocumented) +export type LegendItemListener = (series: SeriesIdentifier[]) => void; + +// @public (undocumented) +export type LegendPath = LegendPathElement[]; + +// @public (undocumented) +export type LegendPathElement = { + index: number; + value: CategoryKey; +}; + +// @public +export type LegendPositionConfig = { + vAlign: typeof VerticalAlignment.Top | typeof VerticalAlignment.Bottom; + hAlign: typeof HorizontalAlignment.Left | typeof HorizontalAlignment.Right; + direction: LayoutDirection; + floating: boolean; + floatingColumns?: number; +}; + +// @public +export interface LegendSpec { + flatLegend?: boolean; + legendAction?: LegendAction; + // (undocumented) + legendColorPicker?: LegendColorPicker; + legendMaxDepth: number; + legendPosition: Position | LegendPositionConfig; + legendStrategy?: LegendStrategy; + // (undocumented) + onLegendItemClick?: LegendItemListener; + // (undocumented) + onLegendItemMinusClick?: LegendItemListener; + // (undocumented) + onLegendItemOut?: BasicListener; + // (undocumented) + onLegendItemOver?: LegendItemListener; + // (undocumented) + onLegendItemPlusClick?: LegendItemListener; + showLegend: boolean; + showLegendExtra: boolean; +} + +// @public (undocumented) +export const LegendStrategy: Readonly<{ + Node: "node"; + Path: "path"; + KeyInLayer: "keyInLayer"; + Key: "key"; + NodeWithDescendants: "nodeWithDescendants"; + PathWithDescendants: "pathWithDescendants"; +}>; + +// @public (undocumented) +export type LegendStrategy = $Values; + +// @public (undocumented) +export interface LegendStyle { + horizontalHeight: number; + margin: number; + spacingBuffer: number; + verticalWidth: number; +} + +// @public (undocumented) +export const LIGHT_THEME: Theme; + +// Warning: (ae-forgotten-export) The symbol "SpecRequiredProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "SpecOptionalProps" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export const LineAnnotation: React_2.FunctionComponent; + +// @public +export interface LineAnnotationDatum { + dataValue: any; + details?: string; + header?: string; +} + +// @public (undocumented) +export type LineAnnotationSpec = BaseAnnotationSpec & { + domainType: AnnotationDomainType; + marker?: ReactNode | ComponentWithAnnotationDatum; + markerBody?: ReactNode | ComponentWithAnnotationDatum; + markerDimensions?: { + width: number; + height: number; + }; + markerPosition?: Position; + hideLines?: boolean; + hideLinesTooltips?: boolean; + zIndex?: number; +}; + +// @public +export interface LineAnnotationStyle { + // @deprecated + details: TextStyle; + line: StrokeStyle & Opacity & Partial; +} + +// Warning: (ae-forgotten-export) The symbol "SpecRequiredProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "SpecOptionalProps" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export const LineSeries: React_2.FunctionComponent; + +// @public +export type LineSeriesSpec = BasicSeriesSpec & HistogramConfig & { + seriesType: typeof SeriesType.Line; + curve?: CurveType; + lineSeriesStyle?: RecursivePartial; + pointStyleAccessor?: PointStyleAccessor; + fit?: Exclude | FitConfig; +}; + +// @public (undocumented) +export interface LineSeriesStyle { + // (undocumented) + line: LineStyle; + // (undocumented) + point: PointStyle; +} + +// @public (undocumented) +export interface LineStyle { + dash?: number[]; + opacity: number; + stroke?: Color | ColorVariant; + strokeWidth: number; + visible: boolean; +} + +// @public (undocumented) +export const LogBase: Readonly<{ + Common: "common"; + Binary: "binary"; + Natural: "natural"; +}>; + +// @public +export type LogBase = $Values; + +// @public +export interface LogScaleOptions { + logBase?: LogBase; + logMinLimit?: number; +} + +// @public (undocumented) +export type LowerBoundedDomain = DomainBase & LowerBound; + +// @public +export type MarkBuffer = number | ((radius: number) => number); + +// @public (undocumented) +export function mergeWithDefaultAnnotationLine(config?: Partial): LineAnnotationStyle; + +// @public (undocumented) +export function mergeWithDefaultAnnotationRect(config?: Partial): RectAnnotationStyle; + +// @public +export function mergeWithDefaultTheme(theme: PartialTheme, defaultTheme?: Theme, axillaryThemes?: PartialTheme[]): Theme; + +// @public (undocumented) +export const MODEL_KEY = "parent"; + +// @public (undocumented) +export function niceTimeFormatByDay(days: number): "YYYY-MM-DD" | "MMMM DD" | "MM-DD HH:mm" | "HH:mm:ss"; + +// @public (undocumented) +export function niceTimeFormatter(domain: [number, number]): TickFormatter; + +// @public (undocumented) +export type NodeColorAccessor = (d: ShapeTreeNode, index: number, array: HierarchyOfArrays) => string; + +// @public (undocumented) +export interface NodeDescriptor { + // (undocumented) + [DEPTH_KEY]: number; + // (undocumented) + [INPUT_KEY]?: Array; + // (undocumented) + [STATISTICS_KEY]: Statistics; + // (undocumented) + [AGGREGATE_KEY]: number; +} + +// @public +export type NodeSorter = (a: ArrayEntry, b: ArrayEntry) => number; + +// @public (undocumented) +export type NonAny = number | boolean | string | symbol | null; + +// @public (undocumented) +export interface Opacity { + opacity: number; +} + +// @public +export interface OrderBy { + // (undocumented) + binAgg?: BinAgg; + // (undocumented) + direction?: Direction; +} + +// @public (undocumented) +export type OrdinalDomain = (number | string)[]; + +// @public (undocumented) +export type OutOfRoomCallback = (wordCount: number, renderedWordCount: number, renderedWords: string[]) => void; + +// Warning: (ae-forgotten-export) The symbol "PerSideDistance" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type Padding = PerSideDistance; + +// @public (undocumented) +export const PARENT_KEY = "parent"; + +// @public (undocumented) +export function parentAccessor(n: ArrayEntry): ArrayNode; + +// @public (undocumented) +export type PartialTheme = RecursivePartial; + +// Warning: (ae-forgotten-export) The symbol "SpecRequiredProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "SpecOptionalProps" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export const Partition: React_2.FunctionComponent; + +// Warning: (ae-forgotten-export) The symbol "StaticConfig" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export interface PartitionConfig extends StaticConfig { + // @alpha (undocumented) + animation: { + duration: TimeMs; + keyframes: Array; + }; +} + +// @public (undocumented) +export type PartitionElementEvent = [Array, SeriesIdentifier]; + +// Warning: (ae-forgotten-export) The symbol "LabelConfig" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export interface PartitionFillLabel extends LabelConfig { + // (undocumented) + clipText: boolean; +} + +// @public +export interface PartitionLayer { + // Warning: (ae-forgotten-export) The symbol "ExtendedFillLabelConfig" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fillLabel?: Partial; + // (undocumented) + groupByRollup: IndexedAccessorFn; + // (undocumented) + nodeLabel?: LabelAccessor; + // (undocumented) + shape?: { + fillColor: string | NodeColorAccessor; + }; + // (undocumented) + showAccessor?: ShowAccessor; + // (undocumented) + sortPredicate?: NodeSorter | null; +} + +// @public (undocumented) +export const PartitionLayout: Readonly<{ + sunburst: "sunburst"; + treemap: "treemap"; + icicle: "icicle"; + flame: "flame"; + mosaic: "mosaic"; +}>; + +// @public (undocumented) +export type PartitionLayout = $Values; + +// @public (undocumented) +export const PATH_KEY = "path"; + +// @public (undocumented) +export function pathAccessor(n: ArrayEntry): LegendPath; + +// @public +export const Placement: Readonly<{ + Top: "top"; + Bottom: "bottom"; + Left: "left"; + Right: "right"; + TopStart: "top-start"; + TopEnd: "top-end"; + BottomStart: "bottom-start"; + BottomEnd: "bottom-end"; + RightStart: "right-start"; + RightEnd: "right-end"; + LeftStart: "left-start"; + LeftEnd: "left-end"; + Auto: "auto"; + AutoStart: "auto-start"; + AutoEnd: "auto-end"; +}>; + +// @public +export type Placement = $Values; + +// @public (undocumented) +type PointerEvent_2 = PointerOverEvent | PointerOutEvent; + +export { PointerEvent_2 as PointerEvent } + +// @public (undocumented) +export const PointerEventType: Readonly<{ + Over: "Over"; + Out: "Out"; +}>; + +// @public (undocumented) +export type PointerEventType = $Values; + +// @public (undocumented) +export interface PointerOutEvent extends BasePointerEvent { + // (undocumented) + type: typeof PointerEventType.Out; +} + +// @public +export interface PointerOverEvent extends BasePointerEvent { + // (undocumented) + scale: ScaleContinuousType | ScaleOrdinalType; + // (undocumented) + type: typeof PointerEventType.Over; + // @alpha + unit?: string; + // (undocumented) + value: number | string | null; +} + +// @public (undocumented) +export type PointerUpdateListener = (event: PointerEvent_2) => void; + +// @public (undocumented) +export const PointShape: Readonly<{ + Circle: "circle"; + Square: "square"; + Diamond: "diamond"; + Plus: "plus"; + X: "x"; + Triangle: "triangle"; +}>; + +// @public (undocumented) +export type PointShape = $Values; + +// @public (undocumented) +export interface PointStyle { + fill?: Color | ColorVariant; + opacity: number; + radius: number; + shape?: PointShape; + stroke?: Color | ColorVariant; + strokeWidth: number; + visible: boolean; +} + +// @public +export type PointStyleAccessor = (datum: DataSeriesDatum, seriesIdentifier: XYChartSeriesIdentifier) => PointStyleOverride; + +// @public (undocumented) +export type PointStyleOverride = RecursivePartial | Color | null; + +// @public (undocumented) +export const Position: Readonly<{ + Top: "top"; + Bottom: "bottom"; + Left: "left"; + Right: "right"; +}>; + +// @public (undocumented) +export type Position = $Values; + +// @public (undocumented) +export interface Postfixes { + y0AccessorFormat?: string; + y1AccessorFormat?: string; +} + +// @public (undocumented) +export type PrimitiveValue = string | number | null; + +// @public +export type ProjectedValues = { + x: PrimitiveValue; + y: Array<{ + value: PrimitiveValue; + groupId: string; + }>; + smVerticalValue: PrimitiveValue; + smHorizontalValue: PrimitiveValue; +}; + +// @public +export type ProjectionClickListener = (values: ProjectedValues) => void; + +// @public +export type Ratio = number; + +// @public (undocumented) +export type RawTextGetter = (node: ShapeTreeNode) => string; + +// @public (undocumented) +export const RectAnnotation: React_2.FunctionComponent & Partial>>; + +// @public +export interface RectAnnotationDatum { + coordinates: { + x0?: PrimitiveValue; + x1?: PrimitiveValue; + y0?: PrimitiveValue; + y1?: PrimitiveValue; + }; + details?: string; +} + +// @public (undocumented) +export type RectAnnotationSpec = BaseAnnotationSpec & { + renderTooltip?: AnnotationTooltipFormatter; + zIndex?: number; +}; + +// @public (undocumented) +export type RectAnnotationStyle = StrokeStyle & FillStyle & Opacity & Partial; + +// @public (undocumented) +export interface RectBorderStyle { + stroke?: Color | ColorVariant; + strokeOpacity?: number; + strokeWidth: number; + visible: boolean; +} + +// @public (undocumented) +export interface RectStyle { + fill?: Color | ColorVariant; + opacity: number; + texture?: TexturedStyles; + widthPixel?: Pixels; + widthRatio?: Ratio; +} + +// @public +export type RecursivePartial = { + [P in keyof T]?: T[P] extends NonAny[] ? T[P] : T[P] extends ReadonlyArray ? T[P] : T[P] extends (infer U)[] ? RecursivePartial[] : T[P] extends ReadonlyArray ? ReadonlyArray> : T[P] extends Set ? Set> : T[P] extends Map ? Map> : T[P] extends NonAny ? T[P] : IsUnknown extends 1 ? T[P] : RecursivePartial; +}; + +// @alpha +export type RelativeBandsPadding = { + outer: Ratio; + inner: Ratio; +}; + +// @public +export type RenderChangeListener = (isRendered: boolean) => void; + +// @public (undocumented) +export type Rendering = 'canvas' | 'svg'; + +// @public (undocumented) +export type Rotation = 0 | 90 | -90 | 180; + +// @public (undocumented) +export type ScaleBandType = ScaleOrdinalType; + +// @public (undocumented) +export type ScaleContinuousType = typeof ScaleType.Linear | typeof ScaleType.Time | typeof ScaleType.Log | typeof ScaleType.Sqrt; + +// @public (undocumented) +export type ScaleOrdinalType = typeof ScaleType.Ordinal; + +// @public (undocumented) +export interface ScalesConfig { + barsPadding: number; + histogramPadding: number; +} + +// @public +export const ScaleType: Readonly<{ + Linear: "linear"; + Ordinal: "ordinal"; + Log: "log"; + Sqrt: "sqrt"; + Time: "time"; + Quantize: "quantize"; + Quantile: "quantile"; + Threshold: "threshold"; +}>; + +// @public +export type ScaleType = $Values; + +// @public (undocumented) +export interface SectorGeomSpecY { + // Warning: (ae-forgotten-export) The symbol "Distance" needs to be exported by the entry point index.d.ts + // + // (undocumented) + y0px: Distance; + // (undocumented) + y1px: Distance; +} + +// @public (undocumented) +export interface SeriesAccessors { + markSizeAccessor?: Accessor | AccessorFn; + splitSeriesAccessors?: (Accessor | AccessorFn)[]; + stackAccessors?: (Accessor | AccessorFn)[]; + xAccessor: Accessor | AccessorFn; + y0Accessors?: (Accessor | AccessorFn)[]; + yAccessors: (Accessor | AccessorFn)[]; +} + +// @public (undocumented) +export type SeriesColorAccessor = string | SeriesColorsArray | SeriesColorAccessorFn; + +// @public (undocumented) +export type SeriesColorAccessorFn = (seriesIdentifier: XYChartSeriesIdentifier) => string | null; + +// @public (undocumented) +export type SeriesColorsArray = string[]; + +// @public +export type SeriesIdentifier = { + specId: SpecId; + key: SeriesKey; +}; + +// @public +export type SeriesKey = CategoryKey; + +// @public (undocumented) +export type SeriesName = string | number | null; + +// @public (undocumented) +export type SeriesNameAccessor = string | SeriesNameFn | SeriesNameConfigOptions; + +// @public +export interface SeriesNameConfig { + accessor: string | number; + name?: string | number; + sortIndex?: number; + value?: string | number; +} + +// @public (undocumented) +export interface SeriesNameConfigOptions { + delimiter?: string; + names?: SeriesNameConfig[]; +} + +// @public +export type SeriesNameFn = (series: XYChartSeriesIdentifier, isTooltip: boolean) => SeriesName; + +// @public (undocumented) +export interface SeriesScales { + timeZone?: string; + xNice?: boolean; + xScaleType: XScaleType; + yNice?: boolean; + yScaleType: ScaleContinuousType; +} + +// @public (undocumented) +export interface SeriesSpec extends Spec { + // (undocumented) + chartType: typeof ChartType.XYAxis; + color?: SeriesColorAccessor; + data: Datum[]; + // (undocumented) + displayValueSettings?: DisplayValueSpec; + filterSeriesInTooltip?: FilterPredicate; + groupId: string; + hideInLegend?: boolean; + name?: SeriesNameAccessor; + seriesType: SeriesType; + // @deprecated + sortIndex?: number; + // (undocumented) + specType: typeof SpecType.Series; + tickFormat?: TickFormatter; + useDefaultGroupDomain?: boolean | string; + // Warning: (ae-forgotten-export) The symbol "AccessorFormat" needs to be exported by the entry point index.d.ts + y0AccessorFormat?: AccessorFormat; + y1AccessorFormat?: AccessorFormat; +} + +// @public (undocumented) +export type SeriesSpecs = Array; + +// @public (undocumented) +export const SeriesType: Readonly<{ + Area: "area"; + Bar: "bar"; + Line: "line"; + Bubble: "bubble"; +}>; + +// @public +export type SeriesType = $Values; + +// @public (undocumented) +export const Settings: React_2.FunctionComponent; + +// @public +export interface SettingsSpec extends Spec, LegendSpec { + allowBrushingLastHistogramBucket?: boolean; + // (undocumented) + animateData: boolean; + ariaDescribedBy?: string; + ariaDescription?: string; + ariaLabel?: string; + ariaLabelHeadingLevel: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p'; + ariaLabelledBy?: string; + ariaUseDefaultSummary: boolean; + baseTheme?: Theme; + brushAxis?: BrushAxis; + debug: boolean; + // @alpha + debugState?: boolean; + // @alpha + externalPointerEvents: ExternalPointerEventsSettings; + hideDuplicateAxes: boolean; + minBrushDelta?: number; + noResults?: ComponentType | ReactChild; + // (undocumented) + onBrushEnd?: BrushEndListener; + // (undocumented) + onElementClick?: ElementClickListener; + // (undocumented) + onElementOut?: BasicListener; + // (undocumented) + onElementOver?: ElementOverListener; + // (undocumented) + onPointerUpdate?: PointerUpdateListener; + onProjectionClick?: ProjectionClickListener; + // (undocumented) + onRenderChange?: RenderChangeListener; + orderOrdinalBinsBy?: OrderBy; + // (undocumented) + pointBuffer?: MarkBuffer; + // (undocumented) + rendering: Rendering; + // (undocumented) + resizeDebounce?: number; + // (undocumented) + rotation: Rotation; + roundHistogramBrushValues?: boolean; + theme?: PartialTheme | PartialTheme[]; + tooltip: TooltipSettings; + // (undocumented) + xDomain?: CustomXDomain; +} + +// @public (undocumented) +export type SettingsSpecProps = Partial>; + +// @public (undocumented) +export interface ShapeTreeNode extends TreeNode, SectorGeomSpecY { + // (undocumented) + dataName: DataName; + // (undocumented) + depth: number; + // (undocumented) + [MODEL_KEY]: ArrayNode; + // (undocumented) + path: LegendPath; + // (undocumented) + sortIndex: number; + // (undocumented) + value: number; + // (undocumented) + yMidPx: Distance; +} + +// @public (undocumented) +export interface SharedGeometryStateStyle { + // (undocumented) + default: GeometryStateStyle; + // (undocumented) + highlighted: GeometryStateStyle; + // (undocumented) + unhighlighted: GeometryStateStyle; +} + +// @public (undocumented) +export type ShowAccessor = (value: PrimitiveValue) => boolean; + +// @public +export interface SimplePadding { + // (undocumented) + inner: number; + // (undocumented) + outer: number; +} + +// @alpha (undocumented) +export const SmallMultiples: React_2.FunctionComponent; + +// @alpha (undocumented) +export type SmallMultiplesProps = Partial>; + +// @alpha (undocumented) +export interface SmallMultiplesSpec extends Spec { + splitHorizontally?: string; + splitVertically?: string; + splitZigzag?: string; + style?: Partial; +} + +// @alpha +export interface SmallMultiplesStyle { + horizontalPanelPadding: RelativeBandsPadding; + verticalPanelPadding: RelativeBandsPadding; +} + +// @public (undocumented) +export const SORT_INDEX_KEY = "sortIndex"; + +// @public (undocumented) +export type Sorter = (a: number, b: number) => number; + +// @public (undocumented) +export function sortIndexAccessor(n: ArrayEntry): number; + +// @public +export interface SortSeriesByConfig { + default?: SeriesCompareFn; + // Warning: (ae-forgotten-export) The symbol "SeriesCompareFn" needs to be exported by the entry point index.d.ts + legend?: SeriesCompareFn; + rendering?: SeriesCompareFn; + tooltip?: SeriesCompareFn; +} + +// @public (undocumented) +export interface Spec { + chartType: ChartType; + id: string; + specType: string; +} + +// @public (undocumented) +export type SpecId = string; + +// @public (undocumented) +export const SpecType: Readonly<{ + Series: "series"; + Axis: "axis"; + Annotation: "annotation"; + Settings: "settings"; + IndexOrder: "index_order"; + SmallMultiples: "small_multiples"; +}>; + +// @public (undocumented) +export type SpecType = $Values; + +// @public +export const StackMode: Readonly<{ + Percentage: "percentage"; + Wiggle: "wiggle"; + Silhouette: "silhouette"; +}>; + +// @public +export type StackMode = $Values; + +// @public (undocumented) +export interface Statistics { + // (undocumented) + globalAggregate: number; +} + +// @public (undocumented) +export const STATISTICS_KEY = "statistics"; + +// @public +export interface StrokeDashArray { + dash: number[]; +} + +// @public +export interface StrokeStyle { + stroke: C; + strokeWidth: number; +} + +// @public +export interface TextAlignment { + // (undocumented) + horizontal: HorizontalAlignment; + // (undocumented) + vertical: VerticalAlignment; +} + +// @public +export interface TextOffset { + reference: 'global' | 'local'; + x: number | string; + y: number | string; +} + +// @public (undocumented) +export interface TextStyle { + // (undocumented) + fill: Color; + // (undocumented) + fontFamily: string; + // (undocumented) + fontSize: number; + // (undocumented) + fontStyle?: string; + // (undocumented) + padding: number | SimplePadding; +} + +// @public (undocumented) +export interface TexturedPathStyles extends TexturedStylesBase { + path: string | Path2D; +} + +// @public (undocumented) +export interface TexturedShapeStyles extends TexturedStylesBase { + shape: TextureShape; +} + +// @public +export type TexturedStyles = TexturedPathStyles | TexturedShapeStyles; + +// @public (undocumented) +export interface TexturedStylesBase { + dash?: number[]; + fill?: Color | ColorVariant; + offset?: Partial & { + global?: boolean; + }; + opacity?: number; + rotation?: number; + shapeRotation?: number; + size?: number; + // Warning: (ae-forgotten-export) The symbol "Point" needs to be exported by the entry point index.d.ts + spacing?: Partial | number; + stroke?: Color | ColorVariant; + strokeWidth?: number; +} + +// @public (undocumented) +export const TextureShape: Readonly<{ + Line: "line"; + Circle: "circle"; + Square: "square"; + Diamond: "diamond"; + Plus: "plus"; + X: "x"; + Triangle: "triangle"; +}>; + +// @public (undocumented) +export type TextureShape = $Values; + +// @public (undocumented) +export interface Theme { + // (undocumented) + arcSeriesStyle: ArcSeriesStyle; + areaSeriesStyle: AreaSeriesStyle; + // (undocumented) + axes: AxisStyle; + background: BackgroundStyle; + barSeriesStyle: BarSeriesStyle; + bubbleSeriesStyle: BubbleSeriesStyle; + chartMargins: Margins; + chartPaddings: Margins; + // (undocumented) + colors: ColorConfig; + // (undocumented) + crosshair: CrosshairStyle; + // (undocumented) + legend: LegendStyle; + lineSeriesStyle: LineSeriesStyle; + markSizeRatio?: number; + // (undocumented) + scales: ScalesConfig; + // (undocumented) + sharedStyle: SharedGeometryStateStyle; +} + +// @public (undocumented) +export type TickFormatter = (value: V, options?: TickFormatterOptions) => string; + +// @public (undocumented) +export type TickFormatterOptions = { + timeZone?: string; +}; + +// @public (undocumented) +export type TickStyle = StrokeStyle & Visible & { + padding: number; + size: number; +}; + +// @public (undocumented) +export function timeFormatter(format: string): TickFormatter; + +// @public +export function toEntries, S>(array: T[], accessor: keyof T, staticValue: S): Record; + +// @public +export interface TooltipInfo { + header: TooltipValue | null; + values: TooltipValue[]; +} + +// @public +export interface TooltipPortalSettings { + boundary?: HTMLElement | B; + boundaryPadding?: Partial | number; + fallbackPlacements?: Placement[]; + offset?: number; + placement?: Placement; +} + +// @public +export type TooltipProps = TooltipPortalSettings<'chart'> & { + type?: TooltipType; + snap?: boolean; + headerFormatter?: TooltipValueFormatter; + unit?: string; + customTooltip?: CustomTooltip; + stickTo?: TooltipStickTo; +}; + +// @public +export type TooltipSettings = TooltipType | TooltipProps; + +// @public +export const TooltipStickTo: Readonly<{ + Top: "top"; + Bottom: "bottom"; + Middle: "middle"; + Left: "left"; + Right: "right"; + Center: "center"; + MousePosition: "MousePosition"; +}>; + +// @public (undocumented) +export type TooltipStickTo = $Values; + +// @public +export const TooltipType: Readonly<{ + VerticalCursor: "vertical"; + Crosshairs: "cross"; + Follow: "follow"; + None: "none"; +}>; + +// @public +export type TooltipType = $Values; + +// @public +export interface TooltipValue { + color: Color; + datum?: unknown; + formattedMarkValue?: string | null; + formattedValue: string; + isHighlighted: boolean; + isVisible: boolean; + label: string; + markValue?: number | null; + seriesIdentifier: SeriesIdentifier; + value: any; + valueAccessor?: Accessor; +} + +// @public +export type TooltipValueFormatter = (data: TooltipValue) => JSX.Element | string; + +// @public (undocumented) +export type TreeLevel = number; + +// @public (undocumented) +export interface TreeNode extends AngleFromTo { + // (undocumented) + fill?: Color; + // (undocumented) + x0: Radian; + // (undocumented) + x1: Radian; + // (undocumented) + y0: TreeLevel; + // (undocumented) + y1: TreeLevel; +} + +// @public +export interface UnaryAccessorFn { + // (undocumented) + (datum: Datum): Return; + fieldName?: string; +} + +// @public (undocumented) +export type UnboundedDomainWithInterval = DomainBase; + +// @public (undocumented) +export type UpperBoundedDomain = DomainBase & UpperBound; + +// @public (undocumented) +export type ValueAccessor = (d: Datum) => AdditiveNumber; + +// @public (undocumented) +export type ValueFormatter = (value: number) => string; + +// Warning: (ae-forgotten-export) The symbol "ValueGetterName" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type ValueGetter = ValueGetterFunction | ValueGetterName; + +// @public (undocumented) +export type ValueGetterFunction = (node: ShapeTreeNode) => number; + +// @public (undocumented) +export const VerticalAlignment: Readonly<{ + Middle: "middle"; + Top: "top"; + Bottom: "bottom"; + Near: "near"; + Far: "far"; +}>; + +// @public +export type VerticalAlignment = $Values; + +// @public (undocumented) +export interface Visible { + // (undocumented) + visible: boolean; +} + +// @public (undocumented) +export const WeightFn: Readonly<{ + log: "log"; + linear: "linear"; + exponential: "exponential"; + squareRoot: "squareRoot"; +}>; + +// @public (undocumented) +export type WeightFn = $Values; + +// Warning: (ae-forgotten-export) The symbol "SpecRequiredProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "SpecOptionalProps" needs to be exported by the entry point index.d.ts +// +// @alpha (undocumented) +export const Wordcloud: React_2.FunctionComponent; + +// @public (undocumented) +export interface WordcloudConfigs { + // (undocumented) + count: number; + // (undocumented) + endAngle: number; + // (undocumented) + exponent: number; + // (undocumented) + fontFamily: string; + // (undocumented) + fontStyle: string; + // (undocumented) + fontWeight: number; + // (undocumented) + height: number; + // (undocumented) + maxFontSize: number; + // (undocumented) + minFontSize: number; + // (undocumented) + padding: number; + // (undocumented) + spiral: string; + // (undocumented) + startAngle: number; + // (undocumented) + weightFn: WeightFn; + // (undocumented) + width: number; +} + +// @public (undocumented) +export type WordCloudElementEvent = [WordModel, SeriesIdentifier]; + +// @alpha (undocumented) +export interface WordcloudSpec extends Spec { + // (undocumented) + angleCount: number; + // (undocumented) + chartType: typeof ChartType.Wordcloud; + // (undocumented) + config: RecursivePartial; + // (undocumented) + data: WordModel[]; + // (undocumented) + endAngle: number; + // (undocumented) + exponent: number; + // (undocumented) + fontFamily: string; + // (undocumented) + fontStyle: string; + // (undocumented) + fontWeight: number; + // (undocumented) + maxFontSize: number; + // (undocumented) + minFontSize: number; + // (undocumented) + outOfRoomCallback: OutOfRoomCallback; + // (undocumented) + padding: number; + // (undocumented) + specType: typeof SpecType.Series; + // (undocumented) + spiral: string; + // (undocumented) + startAngle: number; + // (undocumented) + weightFn: WeightFn; +} + +// @public (undocumented) +export interface WordModel { + // (undocumented) + color: Color; + // (undocumented) + text: string; + // (undocumented) + weight: number; +} + +// @public (undocumented) +export type XScaleType = typeof ScaleType.Ordinal | ScaleContinuousType; + +// @public (undocumented) +export interface XYBrushArea { + // (undocumented) + x?: [number, number]; + // (undocumented) + y?: Array; +} + +// @public (undocumented) +export type XYChartElementEvent = [GeometryValue, XYChartSeriesIdentifier]; + +// @public (undocumented) +export interface XYChartSeriesIdentifier extends SeriesIdentifier { + // (undocumented) + seriesKeys: (string | number)[]; + // (undocumented) + smHorizontalAccessorValue?: string | number; + // (undocumented) + smVerticalAccessorValue?: string | number; + // (undocumented) + splitAccessors: Map; + // (undocumented) + yAccessor: Accessor; +} + +// @public +export interface YDomainBase { + constrainPadding?: boolean; + fit?: boolean; + padding?: number; + paddingUnit?: DomainPaddingUnit; +} + +// @public (undocumented) +export type YDomainRange = YDomainBase & DomainRange & LogScaleOptions; + + +// Warnings were encountered during analysis: +// +// src/chart_types/heatmap/layout/types/config_types.ts:31:13 - (ae-forgotten-export) The symbol "SizeRatio" needs to be exported by the entry point index.d.ts +// src/chart_types/heatmap/layout/types/config_types.ts:63:5 - (ae-forgotten-export) The symbol "TextAlign" needs to be exported by the entry point index.d.ts +// src/chart_types/heatmap/layout/types/config_types.ts:64:5 - (ae-forgotten-export) The symbol "TextBaseline" needs to be exported by the entry point index.d.ts +// src/chart_types/partition_chart/layout/types/config_types.ts:149:5 - (ae-forgotten-export) The symbol "TimeMs" needs to be exported by the entry point index.d.ts +// src/chart_types/partition_chart/layout/types/config_types.ts:150:5 - (ae-forgotten-export) The symbol "AnimKeyframe" needs to be exported by the entry point index.d.ts + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/osd-charts/package.json b/packages/osd-charts/package.json new file mode 100644 index 000000000000..a984e98fb992 --- /dev/null +++ b/packages/osd-charts/package.json @@ -0,0 +1,67 @@ +{ + "name": "@elastic/charts", + "description": "Elastic-Charts data visualization library", + "version": "30.0.0", + "author": "Elastic DataVis", + "license": "Apache-2.0", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "repository": "git@github.com:elastic/elastic-charts.git", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist/**/*" + ], + "scripts": { + "autoprefix:css": "echo 'Autoprefixing...' && yarn postcss dist/*.css --no-map --use autoprefixer -d dist", + "api:check": "yarn build:ts && yarn api:extract", + "api:check:local": "yarn api:check --local", + "api:extract": "yarn api-extractor run -c ./api-extractor.jsonc --verbose", + "build": "yarn build:ts && yarn build:css && yarn build:txt", + "build:ts": "yarn build:clean && yarn build:compile && yarn build:check", + "build:css": "yarn build:sass && yarn autoprefix:css && yarn concat:sass", + "build:clean": "echo 'Cleaning dist...' && rm -rf ./dist", + "build:compile": "echo 'Compiling...' && tsc -p ./tsconfig.json && tsc -p ./tsconfig.nocomments.json", + "build:sass": "echo 'Building sass...' && node-sass src/theme_light.scss dist/theme_light.css --output-style compressed && node-sass src/theme_dark.scss dist/theme_dark.css --output-style compressed && node-sass src/theme_only_light.scss dist/theme_only_light.css --output-style compressed && node-sass src/theme_only_dark.scss dist/theme_only_dark.css --output-style compressed", + "build:check": "echo 'Type checking dist...' && tsc -p ./tsconfig.check.json", + "build:watch": "echo 'Watching build...' && yarn build:clean && yarn build:css && tsc -p ./tsconfig.json -w", + "concat:sass": "echo 'Concat SASS...' && node scripts/concat_sass.js", + "build:txt": "node scripts/move_txt_files.js", + "semantic-release": "semantic-release", + "typecheck": "tsc -p ./tsconfig.json --noEmit && tsc -p ./tsconfig.nocomments.json --noEmit" + }, + "dependencies": { + "@popperjs/core": "^2.4.0", + "chroma-js": "^2.1.0", + "classnames": "^2.2.6", + "d3-array": "^1.2.4", + "d3-cloud": "^1.2.5", + "d3-collection": "^1.0.7", + "d3-color": "^1.4.0", + "d3-interpolate": "^1.4.0", + "d3-scale": "^1.0.7", + "d3-shape": "^1.3.4", + "newtype-ts": "^0.2.4", + "prop-types": "^15.7.2", + "re-reselect": "^3.4.0", + "react-redux": "^7.1.0", + "redux": "^4.0.4", + "reselect": "^4.0.0", + "resize-observer-polyfill": "^1.5.1", + "ts-debounce": "^1.0.0", + "utility-types": "^3.10.0", + "uuid": "^3.3.2" + }, + "peerDependencies": { + "moment": "^2.29.1", + "moment-timezone": "^0.5.32", + "react": "^16.12.0", + "react-dom": "^16.12.0" + }, + "browserslist": [ + "last 2 versions", + "ie 11" + ] +} diff --git a/packages/osd-charts/scripts/concat_sass.js b/packages/osd-charts/scripts/concat_sass.js new file mode 100644 index 000000000000..4541064b462e --- /dev/null +++ b/packages/osd-charts/scripts/concat_sass.js @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const fs = require('fs'); + +const sassGraph = require('sass-graph'); + +const graph = sassGraph.parseFile('./src/components/_index.scss'); + +const root = Object.keys(graph.index)[0]; + +const content = recursiveReadSCSS(root, graph.index[root]); + +fs.writeFileSync('./dist/theme.scss', content); + +function recursiveReadSCSS(branchId, branch) { + if (branch.imports.length === 0) { + return fs.readFileSync(branchId, 'utf8'); + } + const file = fs.readFileSync(branchId, 'utf8'); + const sassFileContent = []; + branch.imports.forEach((branchImport) => { + sassFileContent.push(recursiveReadSCSS(branchImport, graph.index[branchImport])); + }); + // remove imports + const contentWithoutImports = removeImportsFromFile(file); + sassFileContent.push(contentWithoutImports); + return sassFileContent.join('\n'); +} + +function removeImportsFromFile(fileContent) { + const lines = fileContent.split(/\r\n|\r|\n/g); + + return lines.filter((line) => !line.match(/@import\s/i)).join('\n'); +} diff --git a/packages/osd-charts/scripts/move_txt_files.js b/packages/osd-charts/scripts/move_txt_files.js new file mode 100644 index 000000000000..c80826b03a18 --- /dev/null +++ b/packages/osd-charts/scripts/move_txt_files.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const fs = require('fs'); + +fs.copyFileSync('../../README.md', './README.md'); +fs.copyFileSync('../../LICENSE.txt', './LICENSE.txt'); +fs.copyFileSync('../../NOTICE.txt', './NOTICE.txt'); +fs.copyFileSync('../../CHANGELOG.md', './CHANGELOG.md'); diff --git a/packages/osd-charts/src/_eui_imports.scss b/packages/osd-charts/src/_eui_imports.scss new file mode 100644 index 000000000000..8f77ad4cd21c --- /dev/null +++ b/packages/osd-charts/src/_eui_imports.scss @@ -0,0 +1,3 @@ +@import '../../../node_modules/@elastic/eui/src/global_styling/functions/index'; +@import '../../../node_modules/@elastic/eui/src/global_styling/variables/index'; +@import '../../../node_modules/@elastic/eui/src/global_styling/mixins/index'; diff --git a/packages/osd-charts/src/chart_types/goal_chart/layout/config/config.ts b/packages/osd-charts/src/chart_types/goal_chart/layout/config/config.ts new file mode 100644 index 000000000000..5bd2e3d55248 --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/layout/config/config.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ConfigItem, configMap } from '../../../../common/config_objects'; +import { TAU } from '../../../../common/constants'; +import { Config } from '../types/config_types'; + +/** @internal */ +export const configMetadata: Record = { + angleStart: { dflt: Math.PI + Math.PI / 4, min: -TAU, max: TAU, type: 'number' }, + angleEnd: { dflt: -Math.PI / 4, min: -TAU, max: TAU, type: 'number' }, + + // shape geometry + width: { dflt: 300, min: 0, max: 1024, type: 'number', reconfigurable: false }, + height: { dflt: 150, min: 0, max: 1024, type: 'number', reconfigurable: false }, + margin: { + type: 'group', + values: { + left: { dflt: 0, min: -0.25, max: 0.25, type: 'number' }, + right: { dflt: 0, min: -0.25, max: 0.25, type: 'number' }, + top: { dflt: 0, min: -0.25, max: 0.25, type: 'number' }, + bottom: { dflt: 0, min: -0.25, max: 0.25, type: 'number' }, + }, + }, + + // general text config + fontFamily: { + dflt: 'Sans-Serif', + type: 'string', + }, + + // fill text config + minFontSize: { dflt: 8, min: 0.1, max: 8, type: 'number', reconfigurable: true }, + maxFontSize: { dflt: 64, min: 0.1, max: 64, type: 'number' }, + + backgroundColor: { dflt: '#ffffff', type: 'color' }, + sectorLineWidth: { dflt: 1, min: 0, max: 4, type: 'number' }, +}; + +/** @internal */ +export const config: Config = configMap((item: ConfigItem) => item.dflt, configMetadata); diff --git a/packages/osd-charts/src/chart_types/goal_chart/layout/types/config_types.ts b/packages/osd-charts/src/chart_types/goal_chart/layout/types/config_types.ts new file mode 100644 index 000000000000..c68b6475ac46 --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/layout/types/config_types.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Pixels, SizeRatio } from '../../../../common/geometry'; +import { FontFamily } from '../../../../common/text_utils'; +import { Color } from '../../../../utils/common'; + +// todo switch to `io-ts` style, generic way of combining static and runtime type info +/** @public */ +export interface Config { + angleStart: number; + angleEnd: number; + + // shape geometry + width: number; + height: number; + margin: { left: SizeRatio; right: SizeRatio; top: SizeRatio; bottom: SizeRatio }; + + // general text config + fontFamily: FontFamily; + + // fill text config + minFontSize: Pixels; + maxFontSize: Pixels; + + // other + backgroundColor: Color; + sectorLineWidth: Pixels; +} diff --git a/packages/osd-charts/src/chart_types/goal_chart/layout/types/viewmodel_types.ts b/packages/osd-charts/src/chart_types/goal_chart/layout/types/viewmodel_types.ts new file mode 100644 index 000000000000..d9882a6674b3 --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/layout/types/viewmodel_types.ts @@ -0,0 +1,116 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Pixels, PointObject } from '../../../../common/geometry'; +import { SpecType } from '../../../../specs/constants'; +import { BandFillColorAccessorInput } from '../../specs'; +import { GoalSubtype } from '../../specs/constants'; +import { config } from '../config/config'; +import { Config } from './config_types'; + +interface BandViewModel { + value: number; + fillColor: string; +} + +interface TickViewModel { + value: number; + text: string; +} + +/** @internal */ +export interface BulletViewModel { + subtype: string; + base: number; + target: number; + actual: number; + bands: Array; + ticks: Array; + labelMajor: string; + labelMinor: string; + centralMajor: string; + centralMinor: string; + highestValue: number; + lowestValue: number; + aboveBaseCount: number; + belowBaseCount: number; +} + +/** @internal */ +export type PickFunction = (x: Pixels, y: Pixels) => Array; + +/** @internal */ +export type ShapeViewModel = { + config: Config; + bulletViewModel: BulletViewModel; + chartCenter: PointObject; + pickQuads: PickFunction; +}; + +const commonDefaults = { + specType: SpecType.Series, + subtype: GoalSubtype.Goal, + base: 0, + target: 100, + actual: 50, + ticks: [0, 25, 50, 75, 100], +}; + +/** @internal */ +export const defaultGoalSpec = { + ...commonDefaults, + bands: [50, 75, 100], + bandFillColor: ({ value, base, highestValue, lowestValue }: BandFillColorAccessorInput) => { + const aboveBase = value > base; + const ratio = aboveBase + ? (value - base) / (Math.max(base, highestValue) - base) + : (value - base) / (Math.min(base, lowestValue) - base); + const level = Math.round(255 * ratio); + return aboveBase ? `rgb(0, ${level}, 0)` : `rgb( ${level}, 0, 0)`; + }, + tickValueFormatter: ({ value }: BandFillColorAccessorInput) => String(value), + labelMajor: ({ base }: BandFillColorAccessorInput) => String(base), + // eslint-disable-next-line no-empty-pattern + labelMinor: ({}: BandFillColorAccessorInput) => 'unit', + centralMajor: ({ base }: BandFillColorAccessorInput) => String(base), + centralMinor: ({ target }: BandFillColorAccessorInput) => String(target), +}; + +/** @internal */ +export const nullGoalViewModel = { + ...commonDefaults, + bands: [], + ticks: [], + labelMajor: '', + labelMinor: '', + centralMajor: '', + centralMinor: '', + highestValue: 100, + lowestValue: 0, + aboveBaseCount: 0, + belowBaseCount: 0, +}; + +/** @internal */ +export const nullShapeViewModel = (specifiedConfig?: Config, chartCenter?: PointObject): ShapeViewModel => ({ + config: specifiedConfig || config, + bulletViewModel: nullGoalViewModel, + chartCenter: chartCenter || { x: 0, y: 0 }, + pickQuads: () => [], +}); diff --git a/packages/osd-charts/src/chart_types/goal_chart/layout/viewmodel/viewmodel.ts b/packages/osd-charts/src/chart_types/goal_chart/layout/viewmodel/viewmodel.ts new file mode 100644 index 000000000000..06cd9e727a8a --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/layout/viewmodel/viewmodel.ts @@ -0,0 +1,107 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TextMeasure } from '../../../../common/text_utils'; +import { GoalSpec } from '../../specs'; +import { Config } from '../types/config_types'; +import { BulletViewModel, PickFunction, ShapeViewModel } from '../types/viewmodel_types'; + +/** @internal */ +export function shapeViewModel(textMeasure: TextMeasure, spec: GoalSpec, config: Config): ShapeViewModel { + const { width, height, margin } = config; + + const innerWidth = width * (1 - Math.min(1, margin.left + margin.right)); + const innerHeight = height * (1 - Math.min(1, margin.top + margin.bottom)); + + const chartCenter = { + x: width * margin.left + innerWidth / 2, + y: height * margin.top + innerHeight / 2, + }; + + const pickQuads: PickFunction = (x, y) => + -innerWidth / 2 <= x && x <= innerWidth / 2 && -innerHeight / 2 <= y && y <= innerHeight / 2 + ? [bulletViewModel] + : []; + + const { + subtype, + base, + target, + actual, + bands, + ticks, + bandFillColor, + tickValueFormatter, + labelMajor, + labelMinor, + centralMajor, + centralMinor, + } = spec; + + const [lowestValue, highestValue] = [base, target, actual, ...bands, ...ticks].reduce( + ([min, max], value) => [Math.min(min, value), Math.max(max, value)], + [Infinity, -Infinity], + ); + + const aboveBaseCount = bands.filter((b: number) => b > base).length; + const belowBaseCount = bands.filter((b: number) => b <= base).length; + + const callbackArgs = { + base, + target, + actual, + highestValue, + lowestValue, + aboveBaseCount, + belowBaseCount, + }; + + const bulletViewModel: BulletViewModel = { + subtype, + base, + target, + actual, + bands: bands.map((value: number, index: number) => ({ + value, + fillColor: bandFillColor({ value, index, ...callbackArgs }), + })), + ticks: ticks.map((value: number, index: number) => ({ + value, + text: tickValueFormatter({ value, index, ...callbackArgs }), + })), + labelMajor: typeof labelMajor === 'string' ? labelMajor : labelMajor({ value: NaN, index: 0, ...callbackArgs }), + labelMinor: typeof labelMinor === 'string' ? labelMinor : labelMinor({ value: NaN, index: 0, ...callbackArgs }), + centralMajor: + typeof centralMajor === 'string' ? centralMajor : centralMajor({ value: NaN, index: 0, ...callbackArgs }), + centralMinor: + typeof centralMinor === 'string' ? centralMinor : centralMinor({ value: NaN, index: 0, ...callbackArgs }), + highestValue, + lowestValue, + aboveBaseCount, + belowBaseCount, + }; + + // combined viewModel + return { + config, + chartCenter, + bulletViewModel, + pickQuads, + }; +} diff --git a/packages/osd-charts/src/chart_types/goal_chart/renderer/canvas/canvas_renderers.ts b/packages/osd-charts/src/chart_types/goal_chart/renderer/canvas/canvas_renderers.ts new file mode 100644 index 000000000000..a6c1622199c6 --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/renderer/canvas/canvas_renderers.ts @@ -0,0 +1,345 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { GOLDEN_RATIO } from '../../../../common/constants'; +import { cssFontShorthand } from '../../../../common/text_utils'; +import { clearCanvas, renderLayers, withContext } from '../../../../renderers/canvas'; +import { ShapeViewModel } from '../../layout/types/viewmodel_types'; +import { GoalSubtype } from '../../specs/constants'; + +// fixme turn these into config, or capitalize as constants +const referenceCircularSizeCap = 360; // goal/gauge won't be bigger even if there's ample room: it'd be a waste of space +const referenceBulletSizeCap = 500; // goal/gauge won't be bigger even if there's ample room: it'd be a waste of space +const barThicknessMinSizeRatio = 1 / 10; // bar thickness is a maximum of this fraction of the smaller graph area size +const baselineArcThickness = 32; // bar is this thick if there's ample room; no need for greater thickness even if there's a large area +const baselineBarThickness = 32; // bar is this thick if there's ample room; no need for greater thickness even if there's a large area +const marginRatio = 0.05; // same ratio on each side +const maxTickFontSize = 24; +const maxLabelFontSize = 32; +const maxCentralFontSize = 38; + +function get(o: { [k: string]: any }, name: string, dflt: T) { + return name in o ? o[name] || dflt : dflt; +} + +/** @internal */ +export function renderCanvas2d( + ctx: CanvasRenderingContext2D, + dpr: number, + { config, bulletViewModel, chartCenter }: ShapeViewModel, +) { + // eslint-disable-next-line no-empty-pattern + const {} = config; + + withContext(ctx, (ctx) => { + // set some defaults for the overall rendering + + // let's set the devicePixelRatio once and for all; then we'll never worry about it again + ctx.scale(dpr, dpr); + + // all texts are currently center-aligned because + // - the calculations manually compute and lay out text (word) boxes, so we can choose whatever + // - but center/middle has mathematical simplicity and the most unassuming thing + // - due to using the math x/y convention (+y is up) while Canvas uses screen convention (+y is down) + // text rendering must be y-flipped, which is a bit easier this way + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.translate(chartCenter.x, chartCenter.y); + // this applies the mathematical x/y conversion (+y is North) which is easier when developing geometry + // functions - also, all renderers have flexibility (eg. SVG scale) and WebGL NDC is also +y up + // - in any case, it's possible to refactor for a -y = North convention if that's deemed preferable + ctx.scale(1, -1); + + const { + subtype, + lowestValue, + highestValue, + base, + target, + actual, + bands, + ticks, + labelMajor, + labelMinor, + centralMajor, + centralMinor, + } = bulletViewModel; + + const circular = subtype === GoalSubtype.Goal; + const vertical = subtype === GoalSubtype.VerticalBullet; + + const domain = [lowestValue, highestValue]; + + const data = { + base: { value: base }, + ...Object.fromEntries(bands.map(({ value }, index) => [`qualitative_${index}`, { value }])), + target: { value: target }, + actual: { value: actual }, + labelMajor: { value: domain[circular || !vertical ? 0 : 1], text: labelMajor }, + labelMinor: { value: domain[circular || !vertical ? 0 : 1], text: labelMinor }, + ...Object.assign({}, ...ticks.map(({ value, text }, i) => ({ [`tick_${i}`]: { value, text } }))), + ...(circular + ? { + centralMajor: { value: 0, text: centralMajor }, + centralMinor: { value: 0, text: centralMinor }, + } + : {}), + }; + + const minSize = Math.min(config.width, config.height); + + const referenceSize = + Math.min( + circular ? referenceCircularSizeCap : referenceBulletSizeCap, + circular ? minSize : vertical ? config.height : config.width, + ) * + (1 - 2 * marginRatio); + + const barThickness = Math.min( + circular ? baselineArcThickness : baselineBarThickness, + referenceSize * barThicknessMinSizeRatio, + ); + + const tickLength = barThickness * Math.pow(1 / GOLDEN_RATIO, 3); + const tickOffset = -tickLength / 2 - barThickness / 2; + const tickFontSize = Math.min(maxTickFontSize, referenceSize / 25); + const labelFontSize = Math.min(maxLabelFontSize, referenceSize / 18); + const centralFontSize = Math.min(maxCentralFontSize, referenceSize / 14); + + const geoms = [ + ...bulletViewModel.bands.map((b, i) => ({ + order: 0, + landmarks: { + from: i ? `qualitative_${i - 1}` : 'base', + to: `qualitative_${i}`, + }, + aes: { + shape: 'line', + fillColor: b.fillColor, + lineWidth: barThickness, + }, + })), + { + order: 1, + landmarks: { from: 'base', to: 'actual' }, + aes: { shape: 'line', fillColor: 'black', lineWidth: tickLength }, + }, + { + order: 2, + landmarks: { at: 'target' }, + aes: { shape: 'line', fillColor: 'black', lineWidth: barThickness / GOLDEN_RATIO }, + }, + ...bulletViewModel.ticks.map((b, i) => ({ + order: 3, + landmarks: { at: `tick_${i}` }, + aes: { + shape: 'line', + fillColor: 'darkgrey', + lineWidth: tickLength, + axisNormalOffset: tickOffset, + }, + })), + ...bulletViewModel.ticks.map((b, i) => ({ + order: 4, + landmarks: { at: `tick_${i}` }, + aes: { + shape: 'text', + textAlign: vertical ? 'right' : 'center', + textBaseline: vertical ? 'middle' : 'top', + fillColor: 'black', + fontShape: { fontStyle: 'normal', fontVariant: 'normal', fontWeight: '500', fontFamily: 'sans-serif' }, + axisNormalOffset: -barThickness, + }, + })), + { + order: 5, + landmarks: { at: 'labelMajor' }, + aes: { + shape: 'text', + axisNormalOffset: 0, + axisTangentOffset: circular || !vertical ? 0 : 2 * labelFontSize, + textAlign: vertical ? 'center' : 'right', + textBaseline: 'bottom', + fillColor: 'black', + fontShape: { fontStyle: 'normal', fontVariant: 'normal', fontWeight: '900', fontFamily: 'sans-serif' }, + }, + }, + { + order: 5, + landmarks: { at: 'labelMinor' }, + aes: { + shape: 'text', + axisNormalOffset: 0, + axisTangentOffset: circular || !vertical ? 0 : 2 * labelFontSize, + textAlign: vertical ? 'center' : 'right', + textBaseline: 'top', + fillColor: 'black', + fontShape: { fontStyle: 'normal', fontVariant: 'normal', fontWeight: '300', fontFamily: 'sans-serif' }, + }, + }, + ...(circular + ? [ + { + order: 6, + landmarks: { at: 'centralMajor' }, + aes: { + shape: 'text', + textAlign: 'center', + textBaseline: 'bottom', + fillColor: 'black', + fontShape: { fontStyle: 'normal', fontVariant: 'normal', fontWeight: '900', fontFamily: 'sans-serif' }, + }, + }, + { + order: 6, + landmarks: { at: 'centralMinor' }, + aes: { + shape: 'text', + textAlign: 'center', + textBaseline: 'top', + fillColor: 'black', + fontShape: { fontStyle: 'normal', fontVariant: 'normal', fontWeight: '300', fontFamily: 'sans-serif' }, + }, + }, + ] + : []), + ]; + + const maxWidth = geoms.reduce((p, g) => Math.max(p, get(g.aes, 'lineWidth', 0)), 0); + const r = 0.5 * referenceSize - maxWidth / 2; + + renderLayers(ctx, [ + // clear the canvas + (context: CanvasRenderingContext2D) => clearCanvas(context, 200000, 200000), + + (context: CanvasRenderingContext2D) => + withContext(context, (ctx) => { + const fullSize = referenceSize; + const labelSize = fullSize / 2; + const pxRangeFrom = -fullSize / 2 + (circular || vertical ? 0 : labelSize); + const pxRangeTo = fullSize / 2 + (!circular && vertical ? -2 * labelFontSize : 0); + const pxRangeMid = (pxRangeFrom + pxRangeTo) / 2; + const pxRange = pxRangeTo - pxRangeFrom; + + const domainExtent = domain[1] - domain[0]; + + const linearScale = (x: number) => pxRangeFrom + (pxRange * (x - domain[0])) / domainExtent; + + const { angleStart, angleEnd } = config; + const angleRange = angleEnd - angleStart; + const angleScale = (x: number) => angleStart + (angleRange * (x - domain[0])) / domainExtent; + const clockwise = angleStart > angleEnd; // todo refine this crude approach + + geoms + .slice() + .sort((a, b) => a.order - b.order) + .forEach(({ landmarks, aes }) => { + const at = get(landmarks, 'at', ''); + const from = get(landmarks, 'from', ''); + const to = get(landmarks, 'to', ''); + const textAlign = get(aes, 'textAlign', ''); + const textBaseline = get(aes, 'textBaseline', ''); + const fontShape = get(aes, 'fontShape', ''); + const axisNormalOffset = get(aes, 'axisNormalOffset', 0); + const axisTangentOffset = get(aes, 'axisTangentOffset', 0); + const lineWidth = get(aes, 'lineWidth', 0); + const strokeStyle = get(aes, 'fillColor', ''); + withContext(ctx, (ctx) => { + ctx.beginPath(); + if (circular) { + if (aes.shape === 'line') { + ctx.lineWidth = lineWidth; + ctx.strokeStyle = strokeStyle; + if (at) { + ctx.arc( + pxRangeMid, + 0, + r + axisNormalOffset, + angleScale(data[at].value) + Math.PI / 360, + angleScale(data[at].value) - Math.PI / 360, + true, + ); + } else { + const dataClockwise = data[from].value < data[to].value; + ctx.arc( + pxRangeMid, + 0, + r, + angleScale(data[from].value), + angleScale(data[to].value), + clockwise === dataClockwise, + ); + } + } else if (aes.shape === 'text') { + const label = at.slice(0, 5) === 'label'; + const central = at.slice(0, 7) === 'central'; + ctx.textAlign = 'center'; + ctx.textBaseline = label || central ? textBaseline : 'middle'; + ctx.font = cssFontShorthand( + fontShape, + label ? labelFontSize : central ? centralFontSize : tickFontSize, + ); + ctx.scale(1, -1); + const angle = angleScale(data[at].value); + if (label) { + ctx.translate(0, r); + } else if (!central) { + ctx.translate( + (r - GOLDEN_RATIO * barThickness) * Math.cos(angle), + -(r - GOLDEN_RATIO * barThickness) * Math.sin(angle), + ); + } + ctx.fillText(data[at].text, 0, 0); + } + } else { + ctx.translate( + vertical ? axisNormalOffset : axisTangentOffset, + vertical ? axisTangentOffset : axisNormalOffset, + ); + const atPx = data[at] && linearScale(data[at].value); + if (aes.shape === 'line') { + ctx.lineWidth = lineWidth; + ctx.strokeStyle = aes.fillColor; + if (at) { + const atFromPx = atPx - 1; + const atToPx = atPx + 1; + ctx.moveTo(vertical ? 0 : atFromPx, vertical ? atFromPx : 0); + ctx.lineTo(vertical ? 0 : atToPx, vertical ? atToPx : 0); + } else { + const fromPx = linearScale(data[from].value); + const toPx = linearScale(data[to].value); + ctx.moveTo(vertical ? 0 : fromPx, vertical ? fromPx : 0); + ctx.lineTo(vertical ? 0 : toPx, vertical ? toPx : 0); + } + } else if (aes.shape === 'text') { + ctx.textAlign = textAlign; + ctx.textBaseline = textBaseline; + ctx.font = cssFontShorthand(fontShape, tickFontSize); + ctx.scale(1, -1); + ctx.translate(vertical ? 0 : atPx, vertical ? -atPx : 0); + ctx.fillText(data[at].text, 0, 0); + } + } + ctx.stroke(); + }); + }); + }), + ]); + }); +} diff --git a/packages/osd-charts/src/chart_types/goal_chart/renderer/canvas/connected_component.tsx b/packages/osd-charts/src/chart_types/goal_chart/renderer/canvas/connected_component.tsx new file mode 100644 index 000000000000..eb3fe6c1f320 --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/renderer/canvas/connected_component.tsx @@ -0,0 +1,190 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { MouseEvent, RefObject } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; + +import { ScreenReaderSummary } from '../../../../components/accessibility'; +import { onChartRendered } from '../../../../state/actions/chart'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { + A11ySettings, + DEFAULT_A11Y_SETTINGS, + getA11ySettingsSelector, +} from '../../../../state/selectors/get_accessibility_config'; +import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; +import { Dimensions } from '../../../../utils/dimensions'; +import { nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; +import { geometries } from '../../state/selectors/geometries'; +import { renderCanvas2d } from './canvas_renderers'; + +interface ReactiveChartStateProps { + initialized: boolean; + geometries: ShapeViewModel; + chartContainerDimensions: Dimensions; + a11ySettings: A11ySettings; +} + +interface ReactiveChartDispatchProps { + onChartRendered: typeof onChartRendered; +} + +interface ReactiveChartOwnProps { + forwardStageRef: RefObject; +} + +type Props = ReactiveChartStateProps & ReactiveChartDispatchProps & ReactiveChartOwnProps; +class Component extends React.Component { + static displayName = 'Goal'; + + // firstRender = true; // this'll be useful for stable resizing of treemaps + private ctx: CanvasRenderingContext2D | null; + + // see example https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#Example + private readonly devicePixelRatio: number; // fixme this be no constant: multi-monitor window drag may necessitate modifying the `` dimensions + + constructor(props: Readonly) { + super(props); + this.ctx = null; + this.devicePixelRatio = window.devicePixelRatio; + } + + componentDidMount() { + /* + * the DOM element has just been appended, and getContext('2d') is always non-null, + * so we could use a couple of ! non-null assertions but no big plus + */ + this.tryCanvasContext(); + if (this.props.initialized) { + this.drawCanvas(); + this.props.onChartRendered(); + } + } + + componentDidUpdate() { + if (!this.ctx) { + this.tryCanvasContext(); + } + if (this.props.initialized) { + this.drawCanvas(); + this.props.onChartRendered(); + } + } + + handleMouseMove(e: MouseEvent) { + const { + initialized, + chartContainerDimensions: { width, height }, + forwardStageRef, + geometries, + } = this.props; + if (!forwardStageRef.current || !this.ctx || !initialized || width === 0 || height === 0) { + return; + } + const picker = geometries.pickQuads; + const box = forwardStageRef.current.getBoundingClientRect(); + const { chartCenter } = geometries; + const x = e.clientX - box.left - chartCenter.x; + const y = e.clientY - box.top - chartCenter.y; + return picker(x, y); + } + + render() { + const { + initialized, + chartContainerDimensions: { width, height }, + forwardStageRef, + a11ySettings, + } = this.props; + if (!initialized || width === 0 || height === 0) { + return null; + } + + return ( +
+ + + +
+ ); + } + + private tryCanvasContext() { + const canvas = this.props.forwardStageRef.current; + this.ctx = canvas && canvas.getContext('2d'); + } + + private drawCanvas() { + if (this.ctx) { + const { width, height }: Dimensions = this.props.chartContainerDimensions; + renderCanvas2d(this.ctx, this.devicePixelRatio, { + ...this.props.geometries, + config: { ...this.props.geometries.config, width, height }, + }); + } + } +} + +const mapDispatchToProps = (dispatch: Dispatch): ReactiveChartDispatchProps => + bindActionCreators( + { + onChartRendered, + }, + dispatch, + ); + +const DEFAULT_PROPS: ReactiveChartStateProps = { + initialized: false, + geometries: nullShapeViewModel(), + chartContainerDimensions: { + width: 0, + height: 0, + left: 0, + top: 0, + }, + a11ySettings: DEFAULT_A11Y_SETTINGS, +}; + +const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return DEFAULT_PROPS; + } + return { + initialized: true, + geometries: geometries(state), + chartContainerDimensions: state.parentDimensions, + a11ySettings: getA11ySettingsSelector(state), + }; +}; + +/** @internal */ +export const Goal = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/packages/osd-charts/src/chart_types/goal_chart/specs/constants.ts b/packages/osd-charts/src/chart_types/goal_chart/specs/constants.ts new file mode 100644 index 000000000000..9a143e069f35 --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/specs/constants.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $Values } from 'utility-types'; + +/** @public */ +export const GoalSubtype = Object.freeze({ + Goal: 'goal' as const, + HorizontalBullet: 'horizontalBullet' as const, + VerticalBullet: 'verticalBullet' as const, +}); +/** @public */ +export type GoalSubtype = $Values; diff --git a/packages/osd-charts/src/chart_types/goal_chart/specs/index.ts b/packages/osd-charts/src/chart_types/goal_chart/specs/index.ts new file mode 100644 index 000000000000..245142908787 --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/specs/index.ts @@ -0,0 +1,94 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { ChartType } from '../..'; +import { Spec } from '../../../specs'; +import { SpecType } from '../../../specs/constants'; +import { getConnect, specComponentFactory } from '../../../state/spec_factory'; +import { Color, RecursivePartial } from '../../../utils/common'; +import { config } from '../layout/config/config'; +import { Config } from '../layout/types/config_types'; +import { defaultGoalSpec } from '../layout/types/viewmodel_types'; +import { GoalSubtype } from './constants'; + +/** @alpha */ +export interface BandFillColorAccessorInput { + value: number; + index: number; + base: number; + target: number; + highestValue: number; + lowestValue: number; + aboveBaseCount: number; + belowBaseCount: number; +} + +/** @alpha */ +export type BandFillColorAccessor = (input: BandFillColorAccessorInput) => Color; + +const defaultProps = { + chartType: ChartType.Goal, + ...defaultGoalSpec, + config, +}; + +/** @alpha */ +export interface GoalSpec extends Spec { + specType: typeof SpecType.Series; + chartType: typeof ChartType.Goal; + subtype: GoalSubtype; + base: number; + target: number; + actual: number; + bands: number[]; + ticks: number[]; + bandFillColor: BandFillColorAccessor; + tickValueFormatter: BandFillColorAccessor; + labelMajor: string | BandFillColorAccessor; + labelMinor: string | BandFillColorAccessor; + centralMajor: string | BandFillColorAccessor; + centralMinor: string | BandFillColorAccessor; + config: RecursivePartial; +} + +type SpecRequiredProps = Pick; +type SpecOptionalProps = Partial>; + +/** @alpha */ +export const Goal: React.FunctionComponent = getConnect()( + specComponentFactory< + GoalSpec, + | 'config' + | 'chartType' + | 'subtype' + | 'base' + | 'target' + | 'actual' + | 'bands' + | 'ticks' + | 'bandFillColor' + | 'tickValueFormatter' + | 'labelMajor' + | 'labelMinor' + | 'centralMajor' + | 'centralMinor' + >(defaultProps), +); diff --git a/packages/osd-charts/src/chart_types/goal_chart/state/chart_state.tsx b/packages/osd-charts/src/chart_types/goal_chart/state/chart_state.tsx new file mode 100644 index 000000000000..f4e12e68c696 --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/state/chart_state.tsx @@ -0,0 +1,149 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { RefObject } from 'react'; + +import { ChartType } from '../..'; +import { DEFAULT_CSS_CURSOR } from '../../../common/constants'; +import { LegendItem } from '../../../common/legend'; +import { Tooltip } from '../../../components/tooltip'; +import { InternalChartState, GlobalChartState, BackwardRef } from '../../../state/chart_state'; +import { InitStatus } from '../../../state/selectors/get_internal_is_intialized'; +import { LegendItemLabel } from '../../../state/selectors/get_legend_items_labels'; +import { DebugState } from '../../../state/types'; +import { Dimensions } from '../../../utils/dimensions'; +import { Goal } from '../renderer/canvas/connected_component'; +import { getChartTypeDescriptionSelector } from './selectors/get_chart_type_description'; +import { getSpecOrNull } from './selectors/goal_spec'; +import { isTooltipVisibleSelector } from './selectors/is_tooltip_visible'; +import { createOnElementClickCaller } from './selectors/on_element_click_caller'; +import { createOnElementOutCaller } from './selectors/on_element_out_caller'; +import { createOnElementOverCaller } from './selectors/on_element_over_caller'; +import { getTooltipInfoSelector } from './selectors/tooltip'; + +const EMPTY_MAP = new Map(); +const EMPTY_LEGEND_LIST: LegendItem[] = []; +const EMPTY_LEGEND_ITEM_LIST: LegendItemLabel[] = []; + +/** @internal */ +export class GoalState implements InternalChartState { + chartType = ChartType.Goal; + + onElementClickCaller: (state: GlobalChartState) => void; + + onElementOverCaller: (state: GlobalChartState) => void; + + onElementOutCaller: (state: GlobalChartState) => void; + + constructor() { + this.onElementClickCaller = createOnElementClickCaller(); + this.onElementOverCaller = createOnElementOverCaller(); + this.onElementOutCaller = createOnElementOutCaller(); + } + + isInitialized(globalState: GlobalChartState) { + return getSpecOrNull(globalState) !== null ? InitStatus.Initialized : InitStatus.ChartNotInitialized; + } + + isBrushAvailable() { + return false; + } + + isBrushing() { + return false; + } + + isChartEmpty() { + return false; + } + + getLegendItems() { + return EMPTY_LEGEND_LIST; + } + + getLegendItemsLabels() { + return EMPTY_LEGEND_ITEM_LIST; + } + + getLegendExtraValues() { + return EMPTY_MAP; + } + + chartRenderer(containerRef: BackwardRef, forwardStageRef: RefObject) { + return ( + <> + + + + ); + } + + getPointerCursor() { + return DEFAULT_CSS_CURSOR; + } + + isTooltipVisible(globalState: GlobalChartState) { + return { visible: isTooltipVisibleSelector(globalState), isExternal: false }; + } + + getTooltipInfo(globalState: GlobalChartState) { + return getTooltipInfoSelector(globalState); + } + + getTooltipAnchor(state: GlobalChartState) { + const { position } = state.interactions.pointer.current; + return { + isRotated: false, + x: position.x, + width: 0, + y: position.y, + height: 0, + }; + } + + eventCallbacks(globalState: GlobalChartState) { + this.onElementOverCaller(globalState); + this.onElementOutCaller(globalState); + this.onElementClickCaller(globalState); + } + + getChartTypeDescription(globalState: GlobalChartState) { + return getChartTypeDescriptionSelector(globalState); + } + + // TODO + getProjectionContainerArea(): Dimensions { + return { width: 0, height: 0, top: 0, left: 0 }; + } + + // TODO + getMainProjectionArea(): Dimensions { + return { width: 0, height: 0, top: 0, left: 0 }; + } + + // TODO + getBrushArea(): Dimensions | null { + return null; + } + + // TODO + getDebugState(): DebugState { + return {}; + } +} diff --git a/packages/osd-charts/src/chart_types/goal_chart/state/selectors/geometries.ts b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/geometries.ts new file mode 100644 index 000000000000..bb616e870648 --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/geometries.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { ChartType } from '../../..'; +import { SpecType } from '../../../../specs/constants'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSpecs } from '../../../../state/selectors/get_settings_specs'; +import { getSpecsFromStore } from '../../../../state/utils'; +import { nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; +import { GoalSpec } from '../../specs'; +import { render } from './scenegraph'; + +const getParentDimensions = (state: GlobalChartState) => state.parentDimensions; + +/** @internal */ +export const geometries = createCachedSelector( + [getSpecs, getParentDimensions], + (specs, parentDimensions): ShapeViewModel => { + const goalSpecs = getSpecsFromStore(specs, ChartType.Goal, SpecType.Series); + return goalSpecs.length === 1 ? render(goalSpecs[0], parentDimensions) : nullShapeViewModel(); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/goal_chart/state/selectors/get_chart_type_description.ts b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/get_chart_type_description.ts new file mode 100644 index 000000000000..d8416442e2bf --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/get_chart_type_description.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSpecOrNull } from './goal_spec'; + +/** @internal */ +export const getChartTypeDescriptionSelector = createCachedSelector([getSpecOrNull], (spec) => { + return `${spec?.subtype ?? 'goal'} chart`; +})(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/goal_chart/state/selectors/goal_spec.ts b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/goal_spec.ts new file mode 100644 index 000000000000..a4adda3de506 --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/goal_spec.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartType } from '../../..'; +import { SpecType } from '../../../../specs/constants'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getSpecsFromStore } from '../../../../state/utils'; +import { GoalSpec } from '../../specs'; + +/** @internal */ +export function getSpecOrNull(state: GlobalChartState): GoalSpec | null { + const specs = getSpecsFromStore(state.specs, ChartType.Goal, SpecType.Series); + return specs.length > 0 ? specs[0] : null; +} diff --git a/packages/osd-charts/src/chart_types/goal_chart/state/selectors/is_tooltip_visible.ts b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/is_tooltip_visible.ts new file mode 100644 index 000000000000..1c3c0d0fb3b5 --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/is_tooltip_visible.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getTooltipType } from '../../../../specs'; +import { TooltipType } from '../../../../specs/constants'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getTooltipInfoSelector } from './tooltip'; + +/** @internal */ +export const isTooltipVisibleSelector = createCachedSelector( + [getSettingsSpecSelector, getTooltipInfoSelector], + (settingsSpec, tooltipInfo): boolean => { + if (getTooltipType(settingsSpec) === TooltipType.None) { + return false; + } + return tooltipInfo.values.length > 0; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_click_caller.ts b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_click_caller.ts new file mode 100644 index 000000000000..0056d38df28b --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_click_caller.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; +import { Selector } from 'reselect'; + +import { ChartType } from '../../..'; +import { getOnElementClickSelector } from '../../../../common/event_handler_selectors'; +import { GlobalChartState, PointerStates } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getLastClickSelector } from '../../../../state/selectors/get_last_click'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getSpecOrNull } from './goal_spec'; +import { getPickedShapesLayerValues } from './picked_shapes'; + +/** + * Will call the onElementClick listener every time the following preconditions are met: + * - the onElementClick listener is available + * - we have at least one highlighted geometry + * - the pointer state goes from down state to up state + * @internal + */ +export function createOnElementClickCaller(): (state: GlobalChartState) => void { + const prev: { click: PointerStates['lastClick'] } = { click: null }; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartType.Goal) { + selector = createCachedSelector( + [getSpecOrNull, getLastClickSelector, getSettingsSpecSelector, getPickedShapesLayerValues], + getOnElementClickSelector(prev), + )(getChartIdSelector); + } + if (selector) { + selector(state); + } + }; +} diff --git a/packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_out_caller.ts b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_out_caller.ts new file mode 100644 index 000000000000..66e3e6c37073 --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_out_caller.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; +import { Selector } from 'react-redux'; + +import { ChartType } from '../../..'; +import { getOnElementOutSelector } from '../../../../common/event_handler_selectors'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getSpecOrNull } from './goal_spec'; +import { getPickedShapesLayerValues } from './picked_shapes'; + +/** + * Will call the onElementOut listener every time the following preconditions are met: + * - the onElementOut listener is available + * - the highlighted geometries list goes from a list of at least one object to an empty one + * @internal + */ +export function createOnElementOutCaller(): (state: GlobalChartState) => void { + const prev: { pickedShapes: number | null } = { pickedShapes: null }; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartType.Goal) { + selector = createCachedSelector( + [getSpecOrNull, getPickedShapesLayerValues, getSettingsSpecSelector], + getOnElementOutSelector(prev), + )(getChartIdSelector); + } + if (selector) { + selector(state); + } + }; +} diff --git a/packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_over_caller.ts b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_over_caller.ts new file mode 100644 index 000000000000..007a268afab9 --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_over_caller.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; +import { Selector } from 'react-redux'; + +import { ChartType } from '../../..'; +import { getOnElementOverSelector } from '../../../../common/event_handler_selectors'; +import { LayerValue } from '../../../../specs'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getSpecOrNull } from './goal_spec'; +import { getPickedShapesLayerValues } from './picked_shapes'; + +/** + * Will call the onElementOver listener every time the following preconditions are met: + * - the onElementOver listener is available + * - we have a new set of highlighted geometries on our state + * @internal + */ +export function createOnElementOverCaller(): (state: GlobalChartState) => void { + const prev: { pickedShapes: LayerValue[][] } = { pickedShapes: [] }; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartType.Goal) { + selector = createCachedSelector( + [getSpecOrNull, getPickedShapesLayerValues, getSettingsSpecSelector], + getOnElementOverSelector(prev), + )({ + keySelector: getChartIdSelector, + }); + } + if (selector) { + selector(state); + } + }; +} diff --git a/packages/osd-charts/src/chart_types/goal_chart/state/selectors/picked_shapes.ts b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/picked_shapes.ts new file mode 100644 index 000000000000..1ea06d9dc919 --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/picked_shapes.ts @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { LayerValue } from '../../../../specs'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { BulletViewModel } from '../../layout/types/viewmodel_types'; +import { geometries } from './geometries'; + +function getCurrentPointerPosition(state: GlobalChartState) { + return state.interactions.pointer.current.position; +} + +/** @internal */ +export const getPickedShapes = createCachedSelector( + [geometries, getCurrentPointerPosition], + (geoms, pointerPosition): BulletViewModel[] => { + const picker = geoms.pickQuads; + const { chartCenter } = geoms; + const x = pointerPosition.x - chartCenter.x; + const y = pointerPosition.y - chartCenter.y; + return picker(x, y); + }, +)(getChartIdSelector); + +/** @internal */ +export const getPickedShapesLayerValues = createCachedSelector( + [getPickedShapes], + (pickedShapes): Array> => { + const elements = pickedShapes.map>((model) => { + const values: Array = []; + values.push({ + smAccessorValue: '', + groupByRollup: 'Actual', + value: model.actual, + sortIndex: 0, + path: [], + depth: 0, + }); + return values.reverse(); + }); + return elements; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/goal_chart/state/selectors/scenegraph.ts b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/scenegraph.ts new file mode 100644 index 000000000000..79b12269e387 --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/scenegraph.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { measureText } from '../../../../common/text_utils'; +import { mergePartial, RecursivePartial } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { config as defaultConfig } from '../../layout/config/config'; +import { Config } from '../../layout/types/config_types'; +import { ShapeViewModel, nullShapeViewModel } from '../../layout/types/viewmodel_types'; +import { shapeViewModel } from '../../layout/viewmodel/viewmodel'; +import { GoalSpec } from '../../specs'; + +/** @internal */ +export function render(spec: GoalSpec, parentDimensions: Dimensions): ShapeViewModel { + const { width, height } = parentDimensions; + const { config: specConfig } = spec; + const textMeasurer = document.createElement('canvas'); + const textMeasurerCtx = textMeasurer.getContext('2d'); + const partialConfig: RecursivePartial = { ...specConfig, width, height }; + const config: Config = mergePartial(defaultConfig, partialConfig); + if (!textMeasurerCtx) { + return nullShapeViewModel(config, { x: width / 2, y: height / 2 }); + } + return shapeViewModel(measureText(textMeasurerCtx), spec, config); +} diff --git a/packages/osd-charts/src/chart_types/goal_chart/state/selectors/tooltip.ts b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/tooltip.ts new file mode 100644 index 000000000000..6eb4c32c3b06 --- /dev/null +++ b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/tooltip.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { TooltipInfo } from '../../../../components/tooltip/types'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSpecOrNull } from './goal_spec'; +import { getPickedShapes } from './picked_shapes'; + +const EMPTY_TOOLTIP = Object.freeze({ + header: null, + values: [], +}); + +/** @internal */ +export const getTooltipInfoSelector = createCachedSelector( + [getSpecOrNull, getPickedShapes], + (spec, pickedShapes): TooltipInfo => { + if (!spec) { + return EMPTY_TOOLTIP; + } + + const tooltipInfo: TooltipInfo = { + header: null, + values: [], + }; + + pickedShapes.forEach((shape) => { + tooltipInfo.values.push({ + label: 'Actual', + color: 'white', + isHighlighted: false, + isVisible: true, + seriesIdentifier: { + specId: spec.id, + key: spec.id, + }, + value: shape.actual, + formattedValue: `${shape.actual}`, + datum: shape.actual, + }); + }); + + return tooltipInfo; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/layout/config/config.ts b/packages/osd-charts/src/chart_types/heatmap/layout/config/config.ts new file mode 100644 index 000000000000..991fd63db47f --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/layout/config/config.ts @@ -0,0 +1,121 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Config } from '../types/config_types'; + +/** @internal */ +export const config: Config = { + width: 500, + height: 500, + margin: { left: 0.01, right: 0.01, top: 0.01, bottom: 0.01 }, + maxRowHeight: 30, + maxColumnWidth: 30, + + fontFamily: 'Sans-Serif', + + onBrushEnd: undefined, + + brushArea: { + visible: true, + fill: 'black', // black === transparent + stroke: '#69707D', // euiColorDarkShade, + strokeWidth: 2, + }, + brushMask: { + visible: true, + fill: 'rgb(115 115 115 / 50%)', + }, + brushTool: { + visible: false, + fill: 'gray', + }, + + timeZone: 'UTC', + + xAxisLabel: { + name: 'X Value', + visible: true, + width: 'auto', + fill: 'black', + fontSize: 12, + fontFamily: 'Sans-Serif', + fontStyle: 'normal', + textColor: 'black', + fontVariant: 'normal', + fontWeight: 'normal', + textOpacity: 1, + align: 'center' as CanvasTextAlign, + baseline: 'verticalAlign' as CanvasTextBaseline, + padding: 6, + formatter: String, + }, + yAxisLabel: { + name: 'Y Value', + visible: true, + width: 'auto', + fill: 'black', + fontSize: 12, + fontFamily: 'Sans-Serif', + fontStyle: 'normal', + textColor: 'black', + fontVariant: 'normal', + fontWeight: 'normal', + textOpacity: 1, + baseline: 'verticalAlign' as CanvasTextBaseline, + padding: 5, + formatter: String, + }, + grid: { + cellWidth: { + min: 0, + max: 30, + }, + cellHeight: { + min: 12, + max: 30, + }, + stroke: { + width: 1, + color: 'gray', + }, + }, + cell: { + maxWidth: 'fill', + maxHeight: 'fill', + align: 'center', + label: { + visible: true, + maxWidth: 'fill', + fill: 'black', + fontSize: 10, + fontFamily: 'Sans-Serif', + fontStyle: 'normal', + textColor: 'black', + fontVariant: 'normal', + fontWeight: 'normal', + textOpacity: 1, + align: 'center' as CanvasTextAlign, + baseline: 'verticalAlign' as CanvasTextBaseline, + }, + border: { + strokeWidth: 1, + stroke: 'gray', + }, + }, +}; diff --git a/packages/osd-charts/src/chart_types/heatmap/layout/types/config_types.ts b/packages/osd-charts/src/chart_types/heatmap/layout/types/config_types.ts new file mode 100644 index 000000000000..45762b0cd4af --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/layout/types/config_types.ts @@ -0,0 +1,118 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Pixels, SizeRatio } from '../../../../common/geometry'; +import { Font, FontFamily, TextAlign, TextBaseline } from '../../../../common/text_utils'; +import { Color } from '../../../../utils/common'; +import { Cell } from './viewmodel_types'; + +/** + * @public + */ +export interface Config { + width: Pixels; + height: Pixels; + margin: { left: SizeRatio; right: SizeRatio; top: SizeRatio; bottom: SizeRatio }; + maxRowHeight: Pixels; + maxColumnWidth: Pixels; + // general text config + fontFamily: FontFamily; + + timeZone: string; + + onBrushEnd?: (brushArea: HeatmapBrushEvent) => void; + + /** + * Config of the mask over the area outside of the selected cells + */ + brushMask: { visible: boolean; fill: Color }; + /** + * Config of the mask over the selected cells + */ + brushArea: { visible: boolean; fill: Color; stroke: Color; strokeWidth: number }; + /** + * Config of the brushing tool + */ + brushTool: { + visible: boolean; + // TODO add support for changing the brush tool color + fill: Color; + }; + + xAxisLabel: Font & { + name: string; + fontSize: Pixels; + width: Pixels | 'auto'; + fill: string; + align: TextAlign; + baseline: TextBaseline; + visible: boolean; + padding: number; + formatter: (value: string | number) => string; + }; + yAxisLabel: Font & { + name: string; + fontSize: Pixels; + width: Pixels | 'auto' | { max: Pixels }; + fill: string; + baseline: TextBaseline; + visible: boolean; + padding: number | { left?: number; right?: number; top?: number; bottom?: number }; + formatter: (value: string | number) => string; + }; + grid: { + cellWidth: { + min: Pixels; + max: Pixels | 'fill'; + }; + cellHeight: { + min: Pixels; + max: Pixels | 'fill'; + }; + stroke: { + color: string; + width: number; + }; + }; + cell: { + maxWidth: Pixels | 'fill'; + maxHeight: Pixels | 'fill'; + align: 'center'; + label: Font & { + fontSize: Pixels; + maxWidth: Pixels | 'fill'; + fill: string; + align: TextAlign; + baseline: TextBaseline; + visible: boolean; + }; + border: { + strokeWidth: Pixels; + stroke: Color; + }; + }; + maxLegendHeight?: number; +} + +/** @public */ +export type HeatmapBrushEvent = { + cells: Cell[]; + x: (string | number)[]; + y: (string | number)[]; +}; diff --git a/packages/osd-charts/src/chart_types/heatmap/layout/types/viewmodel_types.ts b/packages/osd-charts/src/chart_types/heatmap/layout/types/viewmodel_types.ts new file mode 100644 index 000000000000..15ac98e8f842 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/layout/types/viewmodel_types.ts @@ -0,0 +1,133 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartType } from '../../..'; +import { Pixels } from '../../../../common/geometry'; +import { Fill, Line, Stroke } from '../../../../geoms/types'; +import { Point } from '../../../../utils/point'; +import { config } from '../config/config'; +import { HeatmapCellDatum, TextBox } from '../viewmodel/viewmodel'; +import { Config, HeatmapBrushEvent } from './config_types'; + +/** @internal */ +export interface Value { + order: number; + value: string | number; + formatted: string; +} + +/** @public */ +export interface Cell { + x: number; + y: number; + yIndex: number; + width: number; + height: number; + fill: Fill; + stroke: Stroke; + value: number; + formatted: string; + visible: boolean; + datum: HeatmapCellDatum; +} + +/** @internal */ +export interface HeatmapViewModel { + gridOrigin: { + x: number; + y: number; + }; + gridLines: { + x: Line[]; + y: Line[]; + stroke: Stroke; + }; + cells: Cell[]; + xValues: Array; + yValues: Array; + pageSize: number; +} + +/** @internal */ +export function isPickedCells(v: any): v is Cell[] { + return Array.isArray(v); +} + +/** @internal */ +export type PickFunction = (x: Pixels, y: Pixels) => Cell[] | TextBox; + +/** @internal */ +export type PickDragFunction = (points: [Point, Point]) => HeatmapBrushEvent; + +/** @internal */ +export type PickDragShapeFunction = ( + points: [Point, Point], +) => { x: number; y: number; width: number; height: number } | null; + +/** + * From x and y coordinates in the data domain space to a canvas projected rectangle + * that cover entirely the data domain coordinates provided. + * If the data domain coordinates leaves within a bucket, then the full bucket is taken in consideration. + * Used mainly for the Highlighter that shows the rounded selected area. + * @internal + */ +export type PickHighlightedArea = ( + x: any[], + y: any[], +) => { x: number; y: number; width: number; height: number } | null; + +/** @internal */ +export type DragShape = ReturnType; + +/** @internal */ +export type ShapeViewModel = { + config: Config; + heatmapViewModel: HeatmapViewModel; + pickQuads: PickFunction; + pickDragArea: PickDragFunction; + pickDragShape: PickDragShapeFunction; + pickHighlightedArea: PickHighlightedArea; +}; + +/** @internal */ +export const nullHeatmapViewModel: HeatmapViewModel = { + gridOrigin: { + x: 0, + y: 0, + }, + gridLines: { + x: [], + y: [], + stroke: { width: 0, color: { r: 0, g: 0, b: 0, opacity: 0 } }, + }, + cells: [], + xValues: [], + yValues: [], + pageSize: 0, +}; + +/** @internal */ +export const nullShapeViewModel = (specifiedConfig?: Config): ShapeViewModel => ({ + config: specifiedConfig || config, + heatmapViewModel: nullHeatmapViewModel, + pickQuads: () => [], + pickDragArea: () => ({ cells: [], x: [], y: [], chartType: ChartType.Heatmap }), + pickDragShape: () => ({ x: 0, y: 0, width: 0, height: 0 }), + pickHighlightedArea: () => ({ x: 0, y: 0, width: 0, height: 0 }), +}); diff --git a/packages/osd-charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts b/packages/osd-charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts new file mode 100644 index 000000000000..8fbb8d6ef8d4 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts @@ -0,0 +1,431 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { bisectLeft } from 'd3-array'; +import { scaleBand, scaleQuantize } from 'd3-scale'; + +import { stringToRGB } from '../../../../common/color_library_wrappers'; +import { Pixels } from '../../../../common/geometry'; +import { Box, TextMeasure } from '../../../../common/text_utils'; +import { ScaleContinuous } from '../../../../scales'; +import { ScaleType } from '../../../../scales/constants'; +import { SettingsSpec } from '../../../../specs'; +import { CanvasTextBBoxCalculator } from '../../../../utils/bbox/canvas_text_bbox_calculator'; +import { Dimensions } from '../../../../utils/dimensions'; +import { HeatmapSpec } from '../../specs'; +import { HeatmapTable } from '../../state/selectors/compute_chart_dimensions'; +import { ColorScaleType } from '../../state/selectors/get_color_scale'; +import { GridHeightParams } from '../../state/selectors/get_grid_full_height'; +import { Config } from '../types/config_types'; +import { + Cell, + PickDragFunction, + PickDragShapeFunction, + PickHighlightedArea, + ShapeViewModel, +} from '../types/viewmodel_types'; + +/** @public */ +export interface HeatmapCellDatum { + x: string | number; + y: string | number; + value: number; + originalIndex: number; +} + +/** @internal */ +export interface TextBox extends Box { + value: string | number; + x: number; + y: number; +} + +/** + * Resolves the maximum number of ticks based on the chart width and sample label based on formatter config. + */ +function getTicks(chartWidth: number, xAxisLabelConfig: Config['xAxisLabel']): number { + const bboxCompute = new CanvasTextBBoxCalculator(); + const labelSample = xAxisLabelConfig.formatter(Date.now()); + const { width } = bboxCompute.compute( + labelSample, + xAxisLabelConfig.padding, + xAxisLabelConfig.fontSize, + xAxisLabelConfig.fontFamily, + ); + bboxCompute.destroy(); + const maxTicks = Math.floor(chartWidth / width); + // Dividing by 2 is a temp fix to make sure {@link ScaleContinuous} won't produce + // to many ticks creating nice rounded tick values + // TODO add support for limiting the number of tick in {@link ScaleContinuous} + return maxTicks / 2; +} + +/** @internal */ +export function shapeViewModel( + textMeasure: TextMeasure, + spec: HeatmapSpec, + config: Config, + settingsSpec: SettingsSpec, + chartDimensions: Dimensions, + heatmapTable: HeatmapTable, + colorScale: ColorScaleType, + filterRanges: Array<[number, number | null]>, + { height, pageSize }: GridHeightParams, +): ShapeViewModel { + const gridStrokeWidth = config.grid.stroke.width ?? 1; + + const { table, yValues, xDomain } = heatmapTable; + + // measure the text width of all rows values to get the grid area width + const boxedYValues = yValues.map((value) => { + return { + text: config.yAxisLabel.formatter(value), + value, + ...config.yAxisLabel, + }; + }); + + // compute the scale for the rows positions + const yScale = scaleBand().domain(yValues).range([0, height]); + + const yInvertedScale = scaleQuantize().domain([0, height]).range(yValues); + + // TODO: Fix domain type to be `Array` + let xValues = xDomain.domain as any[]; + + const timeScale = + xDomain.type === ScaleType.Time + ? new ScaleContinuous( + { + type: ScaleType.Time, + domain: xDomain.domain, + range: [0, chartDimensions.width], + nice: false, + }, + { + desiredTickCount: getTicks(chartDimensions.width, config.xAxisLabel), + timeZone: config.timeZone, + }, + ) + : null; + + if (timeScale) { + const result = []; + let [timePoint] = xValues; + while (timePoint < xValues[1]) { + result.push(timePoint); + timePoint += xDomain.minInterval; + } + + xValues = result; + } + + // compute the scale for the columns positions + const xScale = scaleBand().domain(xValues).range([0, chartDimensions.width]); + + const xInvertedScale = scaleQuantize().domain([0, chartDimensions.width]).range(xValues); + + // compute the cell width (can be smaller then the available size depending on config + const cellWidth = + config.cell.maxWidth !== 'fill' && xScale.bandwidth() > config.cell.maxWidth + ? config.cell.maxWidth + : xScale.bandwidth(); + + // compute the cell height (we already computed the max size for that) + const cellHeight = yScale.bandwidth(); + + const getTextValue = ( + formatter: (v: any, options: any) => string, + scaleCallback: (x: any) => number | undefined | null = xScale, + ) => (value: any): TextBox => { + return { + text: formatter(value, { timeZone: config.timeZone }), + value, + ...config.xAxisLabel, + x: chartDimensions.left + (scaleCallback(value) || 0), + y: cellHeight * pageSize + config.xAxisLabel.fontSize / 2 + config.xAxisLabel.padding, + }; + }; + + // compute the position of each column label + const textXValues: Array = timeScale + ? timeScale.ticks().map(getTextValue(config.xAxisLabel.formatter, (x: any) => timeScale.scale(x))) + : xValues.map((textBox: any) => { + return { + ...getTextValue(config.xAxisLabel.formatter)(textBox), + x: chartDimensions.left + (xScale(textBox) || 0) + xScale.bandwidth() / 2, + }; + }); + + const { padding } = config.yAxisLabel; + const rightPadding = typeof padding === 'number' ? padding : padding.right ?? 0; + + // compute the position of each row label + const textYValues = boxedYValues.map((d) => { + return { + ...d, + // position of the Y labels + x: chartDimensions.left - rightPadding, + y: cellHeight / 2 + (yScale(d.value) || 0), + }; + }); + + // compute each available cell position, color and value + const cellMap = table.reduce>((acc, d) => { + const x = xScale(String(d.x)); + const y = yScale(String(d.y))! + gridStrokeWidth; + const yIndex = yValues.indexOf(d.y); + const color = colorScale.config(d.value); + if (x === undefined || y === undefined || yIndex === -1) { + return acc; + } + const cellKey = getCellKey(d.x, d.y); + acc[cellKey] = { + x: + (config.cell.maxWidth !== 'fill' ? x + xScale.bandwidth() / 2 - config.cell.maxWidth / 2 : x) + gridStrokeWidth, + y, + yIndex, + width: cellWidth - gridStrokeWidth * 2, + height: cellHeight - gridStrokeWidth * 2, + datum: d, + fill: { + color: stringToRGB(color), + }, + stroke: { + color: stringToRGB(config.cell.border.stroke), + width: config.cell.border.strokeWidth, + }, + value: d.value, + visible: !isFilteredValue(filterRanges, d.value), + formatted: spec.valueFormatter(d.value), + }; + return acc; + }, {}); + + /** + * Returns selected elements based on coordinates. + * @param x + * @param y + */ + const pickQuads = (x: Pixels, y: Pixels): Array | TextBox => { + if (x > 0 && x < chartDimensions.left && y > chartDimensions.top && y < chartDimensions.height) { + // look up for a Y axis elements + const yLabelKey = yInvertedScale(y); + const yLabelValue = textYValues.find((v) => v.value === yLabelKey); + if (yLabelValue) { + return yLabelValue; + } + } + + if (x < chartDimensions.left || y < chartDimensions.top) { + return []; + } + if (x > chartDimensions.width + chartDimensions.left || y > chartDimensions.height) { + return []; + } + const xValue = xInvertedScale(x - chartDimensions.left); + const yValue = yInvertedScale(y); + if (xValue === undefined || yValue === undefined) { + return []; + } + const cellKey = getCellKey(xValue, yValue); + const cell = cellMap[cellKey]; + if (cell) { + return [cell]; + } + return []; + }; + + /** + * Return selected cells and X,Y ranges based on the drag selection. + */ + const pickDragArea: PickDragFunction = (bound) => { + const [start, end] = bound; + + const { left, top } = chartDimensions; + const invertedBounds = { + startX: xInvertedScale(Math.min(start.x, end.x) - left), + startY: yInvertedScale(Math.min(start.y, end.y) - top), + endX: xInvertedScale(Math.max(start.x, end.x) - left), + endY: yInvertedScale(Math.max(start.y, end.y) - top), + }; + + let allXValuesInRange = []; + const invertedXValues: Array = []; + const { startX, endX, startY, endY } = invertedBounds; + invertedXValues.push(startX); + if (typeof endX === 'number') { + invertedXValues.push(endX + xDomain.minInterval); + let [startXValue] = invertedXValues; + if (typeof startXValue === 'number') { + while (startXValue < invertedXValues[1]) { + allXValuesInRange.push(startXValue); + startXValue += xDomain.minInterval; + } + } + } else { + invertedXValues.push(endX); + const startXIndex = xValues.indexOf(startX); + const endXIndex = Math.min(xValues.indexOf(endX) + 1, xValues.length); + allXValuesInRange = xValues.slice(startXIndex, endXIndex); + invertedXValues.push(...allXValuesInRange); + } + + const invertedYValues: Array = []; + + const startYIndex = yValues.indexOf(startY); + const endYIndex = Math.min(yValues.indexOf(endY) + 1, yValues.length); + const allYValuesInRange = yValues.slice(startYIndex, endYIndex); + invertedYValues.push(...allYValuesInRange); + + const cells: Cell[] = []; + + allXValuesInRange.forEach((x) => { + allYValuesInRange.forEach((y) => { + const cellKey = getCellKey(x, y); + cells.push(cellMap[cellKey]); + }); + }); + + return { + cells: cells.filter(Boolean), + x: invertedXValues, + y: invertedYValues, + }; + }; + + /** + * Resolves rect area based on provided X and Y ranges + * @param x + * @param y + */ + const pickHighlightedArea: PickHighlightedArea = (x: Array, y: Array) => { + if (xDomain.type !== ScaleType.Time) { + return null; + } + const [startValue, endValue] = x; + + if (typeof startValue !== 'number' || typeof endValue !== 'number') { + return null; + } + const start = Math.min(startValue, endValue); + const end = Math.max(startValue, endValue); + + // find X coordinated based on the time range + const leftIndex = bisectLeft(xValues, start); + const rightIndex = bisectLeft(xValues, end); + + const isOutOfRange = rightIndex > xValues.length - 1; + + const startFromScale = xScale(xValues[leftIndex]); + const endFromScale = xScale(isOutOfRange ? xValues[xValues.length - 1] : xValues[rightIndex]); + + if (startFromScale === undefined || endFromScale === undefined) { + return null; + } + + const xStart = chartDimensions.left + startFromScale; + + // extend the range in case the right boundary has been selected + const width = endFromScale - startFromScale + (isOutOfRange ? cellWidth : 0); + + // resolve Y coordinated making sure the order is correct + const { y: yStart, totalHeight } = y + .filter((v) => yValues.includes(v)) + .reduce( + (acc, current, i) => { + if (i === 0) { + acc.y = yScale(current) || 0; + } + acc.totalHeight += cellHeight; + return acc; + }, + { y: 0, totalHeight: 0 }, + ); + + return { + x: xStart, + y: yStart, + width, + height: totalHeight, + }; + }; + + /** + * Resolves coordinates and metrics of the selected rect area. + */ + const pickDragShape: PickDragShapeFunction = (bound) => { + const area = pickDragArea(bound); + return pickHighlightedArea(area.x, area.y); + }; + + // vertical lines + const xLines = []; + for (let i = 0; i < xValues.length + 1; i++) { + const x = chartDimensions.left + i * cellWidth; + const y1 = chartDimensions.top; + const y2 = cellHeight * pageSize; + xLines.push({ x1: x, y1, x2: x, y2 }); + } + // horizontal lines + const yLines = []; + for (let i = 0; i < pageSize + 1; i++) { + const y = i * cellHeight; + yLines.push({ x1: chartDimensions.left, y1: y, x2: chartDimensions.width + chartDimensions.left, y2: y }); + } + + return { + config, + heatmapViewModel: { + gridOrigin: { + x: chartDimensions.left, + y: chartDimensions.top, + }, + gridLines: { + x: xLines, + y: yLines, + stroke: { + color: stringToRGB(config.grid.stroke.color), + width: gridStrokeWidth, + }, + }, + pageSize, + cells: Object.values(cellMap), + xValues: textXValues, + yValues: textYValues, + }, + pickQuads, + pickDragArea, + pickDragShape, + pickHighlightedArea, + }; +} + +function getCellKey(x: string | number, y: string | number) { + return [String(x), String(y)].join('&_&'); +} + +function isFilteredValue(filterRanges: Array<[number, number | null]>, value: number) { + return filterRanges.some(([min, max]) => { + if (max !== null && value > min && value < max) { + return true; + } + return max === null && value > min; + }); +} diff --git a/packages/osd-charts/src/chart_types/heatmap/renderer/canvas/canvas_renderers.ts b/packages/osd-charts/src/chart_types/heatmap/renderer/canvas/canvas_renderers.ts new file mode 100644 index 000000000000..e98b8730a1cc --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/renderer/canvas/canvas_renderers.ts @@ -0,0 +1,170 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Font } from '../../../../common/text_utils'; +import { clearCanvas, renderLayers, withContext } from '../../../../renderers/canvas'; +import { renderMultiLine } from '../../../xy_chart/renderer/canvas/primitives/line'; +import { renderRect } from '../../../xy_chart/renderer/canvas/primitives/rect'; +import { renderText, wrapLines } from '../../../xy_chart/renderer/canvas/primitives/text'; +import { ShapeViewModel } from '../../layout/types/viewmodel_types'; + +/** @internal */ +export function renderCanvas2d( + globalCtx: CanvasRenderingContext2D, + dpr: number, + { config, heatmapViewModel }: ShapeViewModel, +) { + // eslint-disable-next-line no-empty-pattern + const {} = config; + withContext(globalCtx, (context) => { + // set some defaults for the overall rendering + + // let's set the devicePixelRatio once and for all; then we'll never worry about it again + context.scale(dpr, dpr); + + // all texts are currently center-aligned because + // - the calculations manually compute and lay out text (word) boxes, so we can choose whatever + // - but center/middle has mathematical simplicity and the most unassuming thing + // - due to using the math x/y convention (+y is up) while Canvas uses screen convention (+y is down) + // text rendering must be y-flipped, which is a bit easier this way + context.textAlign = 'center'; + context.textBaseline = 'middle'; + // ctx.translate(chartCenter.x, chartCenter.y); + // this applies the mathematical x/y conversion (+y is North) which is easier when developing geometry + // functions - also, all renderers have flexibility (eg. SVG scale) and WebGL NDC is also +y up + // - in any case, it's possible to refactor for a -y = North convention if that's deemed preferable + // ctx.scale(1, -1); + + // TODO this should be filtered by the pageSize AND the pageNumber + const filteredCells = heatmapViewModel.cells.filter((cell) => { + return cell.yIndex < heatmapViewModel.pageSize; + }); + const filteredYValues = heatmapViewModel.yValues.filter((value, yIndex) => { + return yIndex < heatmapViewModel.pageSize; + }); + + renderLayers(context, [ + // clear the canvas + (ctx: CanvasRenderingContext2D) => clearCanvas(ctx, config.width, config.height), + + (ctx: CanvasRenderingContext2D) => { + withContext(ctx, (ctx) => { + // render grid + renderMultiLine(ctx, heatmapViewModel.gridLines.x, heatmapViewModel.gridLines.stroke); + renderMultiLine(ctx, heatmapViewModel.gridLines.y, heatmapViewModel.gridLines.stroke); + }); + }, + (ctx: CanvasRenderingContext2D) => + withContext(ctx, (ctx) => { + // render cells + const { x, y } = heatmapViewModel.gridOrigin; + ctx.translate(x, y); + filteredCells.forEach((cell) => { + if (!cell.visible) { + return; + } + renderRect(ctx, cell, cell.fill, cell.stroke); + }); + }), + (ctx: CanvasRenderingContext2D) => + withContext(ctx, (ctx) => { + // render text on cells + const { x, y } = heatmapViewModel.gridOrigin; + ctx.translate(x, y); + if (!config.cell.label.visible) { + return; + } + filteredCells.forEach((cell) => { + if (!cell.visible) { + return; + } + renderText( + ctx, + { + x: cell.x + cell.width / 2, + y: cell.y + cell.height / 2, + }, + cell.formatted, + config.cell.label, + ); + }); + }), + (ctx: CanvasRenderingContext2D) => + withContext(ctx, (ctx) => { + // render text on Y axis + if (!config.yAxisLabel.visible) { + return; + } + filteredYValues.forEach((yValue) => { + const font: Font = { + fontFamily: config.yAxisLabel.fontFamily, + fontStyle: config.yAxisLabel.fontStyle ? config.yAxisLabel.fontStyle : 'normal', + fontVariant: 'normal', + fontWeight: 'normal', + textColor: 'black', + textOpacity: 1, + }; + const { padding } = config.yAxisLabel; + const horizontalPadding = + typeof padding === 'number' ? padding * 2 : (padding.left ?? 0) + (padding.right ?? 0); + const [resultText] = wrapLines( + ctx, + yValue.text, + font, + config.yAxisLabel.fontSize, + heatmapViewModel.gridOrigin.x - horizontalPadding, + 16, + { + shouldAddEllipsis: true, + wrapAtWord: false, + }, + ).lines; + renderText( + ctx, + { + x: yValue.x, + y: yValue.y, + }, + resultText, + // the alignment for y axis labels is fixed to the right + { ...config.yAxisLabel, align: 'right' }, + ); + }); + }), + (ctx: CanvasRenderingContext2D) => + withContext(ctx, (ctx) => { + // render text on X axis + if (!config.xAxisLabel.visible) { + return; + } + heatmapViewModel.xValues.forEach((xValue) => { + renderText( + ctx, + { + x: xValue.x, + y: xValue.y, + }, + xValue.text, + config.xAxisLabel, + ); + }); + }), + ]); + }); +} diff --git a/packages/osd-charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx b/packages/osd-charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx new file mode 100644 index 000000000000..a1a573bc767b --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx @@ -0,0 +1,172 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { RefObject } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; + +import { ScreenReaderSummary } from '../../../../components/accessibility'; +import { onChartRendered } from '../../../../state/actions/chart'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { + A11ySettings, + DEFAULT_A11Y_SETTINGS, + getA11ySettingsSelector, +} from '../../../../state/selectors/get_accessibility_config'; +import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; +import { Dimensions } from '../../../../utils/dimensions'; +import { nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; +import { geometries } from '../../state/selectors/geometries'; +import { getHeatmapContainerSizeSelector } from '../../state/selectors/get_heatmap_container_size'; +import { renderCanvas2d } from './canvas_renderers'; + +interface ReactiveChartStateProps { + initialized: boolean; + geometries: ShapeViewModel; + chartContainerDimensions: Dimensions; + a11ySettings: A11ySettings; +} + +interface ReactiveChartDispatchProps { + onChartRendered: typeof onChartRendered; +} + +interface ReactiveChartOwnProps { + forwardStageRef: RefObject; +} + +type Props = ReactiveChartStateProps & ReactiveChartDispatchProps & ReactiveChartOwnProps; +class Component extends React.Component { + static displayName = 'Heatmap'; + + // firstRender = true; // this will be useful for stable resizing of treemaps + private ctx: CanvasRenderingContext2D | null; + + // see example https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#Example + private readonly devicePixelRatio: number; // fixme this be no constant: multi-monitor window drag may necessitate modifying the `` dimensions + + constructor(props: Readonly) { + super(props); + this.ctx = null; + this.devicePixelRatio = window.devicePixelRatio; + } + + componentDidMount() { + /* + * the DOM element has just been appended, and getContext('2d') is always non-null, + * so we could use a couple of ! non-null assertions but no big plus + */ + this.tryCanvasContext(); + if (this.props.initialized) { + this.drawCanvas(); + this.props.onChartRendered(); + } + } + + componentDidUpdate() { + if (!this.ctx) { + this.tryCanvasContext(); + } + if (this.props.initialized) { + this.drawCanvas(); + this.props.onChartRendered(); + } + } + + private tryCanvasContext() { + const canvas = this.props.forwardStageRef.current; + this.ctx = canvas && canvas.getContext('2d'); + } + + private drawCanvas() { + if (this.ctx) { + const { width, height }: Dimensions = this.props.chartContainerDimensions; + renderCanvas2d(this.ctx, this.devicePixelRatio, { + ...this.props.geometries, + config: { ...this.props.geometries.config, width, height }, + }); + } + } + + // eslint-disable-next-line @typescript-eslint/member-ordering + render() { + const { + initialized, + chartContainerDimensions: { width, height }, + forwardStageRef, + a11ySettings, + } = this.props; + if (!initialized || width === 0 || height === 0) { + return null; + } + return ( +
+ + + +
+ ); + } +} + +const mapDispatchToProps = (dispatch: Dispatch): ReactiveChartDispatchProps => + bindActionCreators( + { + onChartRendered, + }, + dispatch, + ); + +const DEFAULT_PROPS: ReactiveChartStateProps = { + initialized: false, + geometries: nullShapeViewModel(), + chartContainerDimensions: { + width: 0, + height: 0, + left: 0, + top: 0, + }, + a11ySettings: DEFAULT_A11Y_SETTINGS, +}; + +const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return DEFAULT_PROPS; + } + return { + initialized: true, + geometries: geometries(state), + chartContainerDimensions: getHeatmapContainerSizeSelector(state), + a11ySettings: getA11ySettingsSelector(state), + }; +}; + +/** @internal */ +export const Heatmap = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/packages/osd-charts/src/chart_types/heatmap/renderer/dom/highlighter.tsx b/packages/osd-charts/src/chart_types/heatmap/renderer/dom/highlighter.tsx new file mode 100644 index 000000000000..98e922ef8a86 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/renderer/dom/highlighter.tsx @@ -0,0 +1,154 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC } from 'react'; + +import { Dimensions } from '../../../../utils/dimensions'; +import { config } from '../../layout/config/config'; +import { Config } from '../../layout/types/config_types'; +import { DragShape, nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; + +/** @internal */ +export interface HighlighterCellsProps { + chartId: string; + initialized: boolean; + canvasDimension: Dimensions; + geometries: ShapeViewModel; + dragShape: DragShape | null; + brushMask: Config['brushMask']; + brushArea: Config['brushArea']; +} + +/** + * @internal + * + * Component for highlighting selected cells + */ +export const HighlighterCellsComponent: FC = ({ + initialized, + dragShape, + chartId, + canvasDimension, + brushArea, + brushMask, +}) => { + if (!initialized || dragShape === null) return null; + + const maskId = `echHighlighterMask__${chartId}`; + + return ( + + + + {brushMask.visible && ( + + )} + {brushArea.visible && ( + <> + + + + )} + + + + {brushMask.visible && ( + + )} + {brushArea.visible && ( + <> + + + + + + )} + + + ); +}; + +/** @internal */ +export const DEFAULT_PROPS: HighlighterCellsProps = { + chartId: 'empty', + initialized: false, + canvasDimension: { + width: 0, + height: 0, + left: 0, + top: 0, + }, + geometries: nullShapeViewModel(), + dragShape: { x: 0, y: 0, height: 0, width: 0 }, + brushArea: config.brushArea, + brushMask: config.brushMask, +}; diff --git a/packages/osd-charts/src/chart_types/heatmap/renderer/dom/highlighter_brush.tsx b/packages/osd-charts/src/chart_types/heatmap/renderer/dom/highlighter_brush.tsx new file mode 100644 index 000000000000..63b8a3850e93 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/renderer/dom/highlighter_brush.tsx @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { connect } from 'react-redux'; + +import { GlobalChartState } from '../../../../state/chart_state'; +import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; +import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; +import { geometries } from '../../state/selectors/geometries'; +import { getBrushedHighlightedShapesSelector } from '../../state/selectors/get_brushed_highlighted_shapes'; +import { getHeatmapConfigSelector } from '../../state/selectors/get_heatmap_config'; +import { getHighlightedAreaSelector } from '../../state/selectors/get_highlighted_area'; +import { DEFAULT_PROPS, HighlighterCellsComponent, HighlighterCellsProps } from './highlighter'; + +const brushMapStateToProps = (state: GlobalChartState): HighlighterCellsProps => { + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return DEFAULT_PROPS; + } + + const { chartId } = state; + + const geoms = geometries(state); + const canvasDimension = computeChartDimensionsSelector(state); + + let dragShape = getBrushedHighlightedShapesSelector(state); + const highlightedArea = getHighlightedAreaSelector(state); + if (highlightedArea) { + dragShape = highlightedArea; + } + const { brushMask, brushArea } = getHeatmapConfigSelector(state); + + return { + chartId, + initialized: true, + canvasDimension, + geometries: geoms, + dragShape, + brushMask, + brushArea, + }; +}; + +/** + * @internal + */ +export const HighlighterFromBrush = connect(brushMapStateToProps)(HighlighterCellsComponent); diff --git a/packages/osd-charts/src/chart_types/heatmap/specs/heatmap.ts b/packages/osd-charts/src/chart_types/heatmap/specs/heatmap.ts new file mode 100644 index 000000000000..d399c46c46eb --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/specs/heatmap.ts @@ -0,0 +1,94 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { ChartType } from '../..'; +import { Predicate } from '../../../common/predicate'; +import { ScaleType } from '../../../scales/constants'; +import { SeriesScales, Spec } from '../../../specs'; +import { SpecType } from '../../../specs/constants'; +import { getConnect, specComponentFactory } from '../../../state/spec_factory'; +import { Accessor, AccessorFn } from '../../../utils/accessor'; +import { Color, Datum, RecursivePartial } from '../../../utils/common'; +import { config } from '../layout/config/config'; +import { Config } from '../layout/types/config_types'; +import { X_SCALE_DEFAULT } from './scale_defaults'; + +const defaultProps = { + chartType: ChartType.Heatmap, + specType: SpecType.Series, + data: [], + colors: ['red', 'yellow', 'green'], + colorScale: ScaleType.Linear, + xAccessor: ({ x }: { x: string | number }) => x, + yAccessor: ({ y }: { y: string | number }) => y, + xScaleType: X_SCALE_DEFAULT.type, + valueAccessor: ({ value }: { value: string | number }) => value, + valueFormatter: (value: number) => `${value}`, + xSortPredicate: Predicate.AlphaAsc, + ySortPredicate: Predicate.AlphaAsc, + config, +}; + +/** @public */ +export type HeatmapScaleType = + | typeof ScaleType.Linear + | typeof ScaleType.Quantile + | typeof ScaleType.Quantize + | typeof ScaleType.Threshold; + +/** @alpha */ +export interface HeatmapSpec extends Spec { + specType: typeof SpecType.Series; + chartType: typeof ChartType.Heatmap; + data: Datum[]; + colorScale?: HeatmapScaleType; + ranges?: number[] | [number, number]; + colors: Color[]; + xAccessor: Accessor | AccessorFn; + yAccessor: Accessor | AccessorFn; + valueAccessor: Accessor | AccessorFn; + valueFormatter: (value: number) => string; + xSortPredicate: Predicate; + ySortPredicate: Predicate; + xScaleType: SeriesScales['xScaleType']; + config: RecursivePartial; + highlightedData?: { x: Array; y: Array }; + name?: string; +} + +/** @alpha */ +export const Heatmap: React.FunctionComponent< + Pick & Partial> +> = getConnect()( + specComponentFactory< + HeatmapSpec, + | 'xAccessor' + | 'yAccessor' + | 'valueAccessor' + | 'colors' + | 'data' + | 'ySortPredicate' + | 'xSortPredicate' + | 'valueFormatter' + | 'config' + | 'xScaleType' + >(defaultProps), +); diff --git a/packages/osd-charts/src/chart_types/heatmap/specs/index.ts b/packages/osd-charts/src/chart_types/heatmap/specs/index.ts new file mode 100644 index 000000000000..4dd09d70d01e --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/specs/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './heatmap'; diff --git a/packages/osd-charts/src/chart_types/heatmap/specs/scale_defaults.ts b/packages/osd-charts/src/chart_types/heatmap/specs/scale_defaults.ts new file mode 100644 index 000000000000..429953259911 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/specs/scale_defaults.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ScaleType } from '../../../scales/constants'; + +/** @internal */ +export const X_SCALE_DEFAULT = { + type: ScaleType.Ordinal, + nice: false, + desiredTickCount: 10, +}; diff --git a/packages/osd-charts/src/chart_types/heatmap/state/chart_state.tsx b/packages/osd-charts/src/chart_types/heatmap/state/chart_state.tsx new file mode 100644 index 000000000000..63a82a58f151 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/chart_state.tsx @@ -0,0 +1,143 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { RefObject } from 'react'; + +import { ChartType } from '../..'; +import { BrushTool } from '../../../components/brush/brush'; +import { Tooltip } from '../../../components/tooltip'; +import { InternalChartState, GlobalChartState, BackwardRef } from '../../../state/chart_state'; +import { getChartContainerDimensionsSelector } from '../../../state/selectors/get_chart_container_dimensions'; +import { InitStatus } from '../../../state/selectors/get_internal_is_intialized'; +import { Dimensions } from '../../../utils/dimensions'; +import { Heatmap } from '../renderer/canvas/connected_component'; +import { HighlighterFromBrush } from '../renderer/dom/highlighter_brush'; +import { computeChartDimensionsSelector } from './selectors/compute_chart_dimensions'; +import { computeLegendSelector } from './selectors/compute_legend'; +import { getBrushAreaSelector } from './selectors/get_brush_area'; +import { getPointerCursorSelector } from './selectors/get_cursor_pointer'; +import { getDebugStateSelector } from './selectors/get_debug_state'; +import { getLegendItemsLabelsSelector } from './selectors/get_legend_items_labels'; +import { getTooltipAnchorSelector } from './selectors/get_tooltip_anchor'; +import { getSpecOrNull } from './selectors/heatmap_spec'; +import { isBrushAvailableSelector } from './selectors/is_brush_available'; +import { isBrushingSelector } from './selectors/is_brushing'; +import { isTooltipVisibleSelector } from './selectors/is_tooltip_visible'; +import { createOnBrushEndCaller } from './selectors/on_brush_end_caller'; +import { createOnElementClickCaller } from './selectors/on_element_click_caller'; +import { createOnElementOutCaller } from './selectors/on_element_out_caller'; +import { createOnElementOverCaller } from './selectors/on_element_over_caller'; +import { getTooltipInfoSelector } from './selectors/tooltip'; + +const EMPTY_MAP = new Map(); + +/** @internal */ +export class HeatmapState implements InternalChartState { + chartType = ChartType.Heatmap; + + onElementClickCaller: (state: GlobalChartState) => void = createOnElementClickCaller(); + + onElementOverCaller: (state: GlobalChartState) => void = createOnElementOverCaller(); + + onElementOutCaller: (state: GlobalChartState) => void = createOnElementOutCaller(); + + onBrushEndCaller: (state: GlobalChartState) => void = createOnBrushEndCaller(); + + isInitialized(globalState: GlobalChartState) { + return getSpecOrNull(globalState) !== null ? InitStatus.Initialized : InitStatus.ChartNotInitialized; + } + + isBrushAvailable(globalState: GlobalChartState) { + return isBrushAvailableSelector(globalState); + } + + isBrushing(globalState: GlobalChartState) { + return isBrushingSelector(globalState); + } + + isChartEmpty() { + return false; + } + + getLegendItems(globalState: GlobalChartState) { + return computeLegendSelector(globalState); + } + + getLegendItemsLabels(globalState: GlobalChartState) { + return getLegendItemsLabelsSelector(globalState); + } + + getLegendExtraValues() { + return EMPTY_MAP; + } + + chartRenderer(containerRef: BackwardRef, forwardStageRef: RefObject) { + return ( + <> + + + + + + ); + } + + getPointerCursor(globalState: GlobalChartState) { + return getPointerCursorSelector(globalState); + } + + isTooltipVisible(globalState: GlobalChartState) { + return { visible: isTooltipVisibleSelector(globalState), isExternal: false }; + } + + getTooltipInfo(globalState: GlobalChartState) { + return getTooltipInfoSelector(globalState); + } + + getTooltipAnchor(globalState: GlobalChartState) { + return getTooltipAnchorSelector(globalState); + } + + getProjectionContainerArea(globalState: GlobalChartState): Dimensions { + return getChartContainerDimensionsSelector(globalState); + } + + getMainProjectionArea(globalState: GlobalChartState): Dimensions { + return computeChartDimensionsSelector(globalState); + } + + getBrushArea(globalState: GlobalChartState): Dimensions | null { + return getBrushAreaSelector(globalState); + } + + getDebugState(globalState: GlobalChartState) { + return getDebugStateSelector(globalState); + } + + getChartTypeDescription() { + return 'Heatmap chart'; + } + + eventCallbacks(globalState: GlobalChartState) { + this.onElementOverCaller(globalState); + this.onElementOutCaller(globalState); + this.onElementClickCaller(globalState); + this.onBrushEndCaller(globalState); + } +} diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/compute_chart_dimensions.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/compute_chart_dimensions.ts new file mode 100644 index 000000000000..29f63fc25113 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/compute_chart_dimensions.ts @@ -0,0 +1,117 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { max as d3Max } from 'd3-array'; +import createCachedSelector from 're-reselect'; + +import { Box, measureText } from '../../../../common/text_utils'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getLegendSizeSelector } from '../../../../state/selectors/get_legend_size'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { Position } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { XDomain } from '../../../xy_chart/domains/types'; +import { HeatmapCellDatum } from '../../layout/viewmodel/viewmodel'; +import { getGridHeightParamsSelector } from './get_grid_full_height'; +import { getHeatmapConfigSelector } from './get_heatmap_config'; +import { getHeatmapTableSelector } from './get_heatmap_table'; +import { getXAxisRightOverflow } from './get_x_axis_right_overflow'; + +/** @internal */ +export interface HeatmapTable { + table: Array; + // unique set of column values + xDomain: XDomain; + // unique set of row values + yValues: Array; + extent: [number, number]; +} + +const getParentDimension = (state: GlobalChartState) => state.parentDimensions; + +/** + * Gets charts grid area excluding legend and X,Y axis labels and paddings. + * @internal + */ +export const computeChartDimensionsSelector = createCachedSelector( + [ + getParentDimension, + getLegendSizeSelector, + getHeatmapTableSelector, + getHeatmapConfigSelector, + getXAxisRightOverflow, + getGridHeightParamsSelector, + getSettingsSpecSelector, + ], + ( + chartContainerDimensions, + legendSize, + heatmapTable, + config, + rightOverflow, + { height }, + { showLegend, legendPosition }, + ): Dimensions => { + let { width, left } = chartContainerDimensions; + const { top } = chartContainerDimensions; + const { padding } = config.yAxisLabel; + + const textMeasurer = document.createElement('canvas'); + const textMeasurerCtx = textMeasurer.getContext('2d'); + const textMeasure = measureText(textMeasurerCtx!); + + const totalHorizontalPadding = + typeof padding === 'number' ? padding * 2 : (padding.left ?? 0) + (padding.right ?? 0); + + if (config.yAxisLabel.visible) { + // measure the text width of all rows values to get the grid area width + const boxedYValues = heatmapTable.yValues.map((value) => { + return { + text: String(value), + value, + ...config.yAxisLabel, + }; + }); + const measuredYValues = textMeasure(config.yAxisLabel.fontSize, boxedYValues); + + let yColumnWidth: number = d3Max(measuredYValues, ({ width }) => width) ?? 0; + if (typeof config.yAxisLabel.width === 'number') { + yColumnWidth = config.yAxisLabel.width; + } else if (typeof config.yAxisLabel.width === 'object' && yColumnWidth > config.yAxisLabel.width.max) { + yColumnWidth = config.yAxisLabel.width.max; + } + + width -= yColumnWidth + rightOverflow + totalHorizontalPadding; + left += yColumnWidth + totalHorizontalPadding; + } + let legendWidth = 0; + if (showLegend && (legendPosition === Position.Right || legendPosition === Position.Left)) { + legendWidth = legendSize.width - legendSize.margin * 2; + } + width -= legendWidth; + + return { + height, + width, + top, + left, + }; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/compute_legend.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/compute_legend.ts new file mode 100644 index 000000000000..2c6ac6340f81 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/compute_legend.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { LegendItem } from '../../../../common/legend'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getDeselectedSeriesSelector } from '../../../../state/selectors/get_deselected_data_series'; +import { getColorScale } from './get_color_scale'; +import { getSpecOrNull } from './heatmap_spec'; + +/** @internal */ +export const computeLegendSelector = createCachedSelector( + [getSpecOrNull, getColorScale, getDeselectedSeriesSelector], + (spec, colorScale, deselectedDataSeries): LegendItem[] => { + const legendItems: LegendItem[] = []; + + if (colorScale === null || spec === null) { + return legendItems; + } + + return colorScale.ticks.map((tick) => { + const color = colorScale.config(tick); + const seriesIdentifier = { + key: String(tick), + specId: String(tick), + }; + + return { + color, + label: `> ${spec.valueFormatter ? spec.valueFormatter(tick) : tick}`, + seriesIdentifiers: [seriesIdentifier], + isSeriesHidden: deselectedDataSeries.some((dataSeries) => dataSeries.key === seriesIdentifier.key), + isToggleable: true, + path: [{ index: 0, value: seriesIdentifier.key }], + keys: [], + }; + }); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/geometries.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/geometries.ts new file mode 100644 index 000000000000..193b207bf58f --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/geometries.ts @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { getColorScale } from './get_color_scale'; +import { getGridHeightParamsSelector } from './get_grid_full_height'; +import { getHeatmapSpecSelector } from './get_heatmap_spec'; +import { getHeatmapTableSelector } from './get_heatmap_table'; +import { getLegendItemsLabelsSelector } from './get_legend_items_labels'; +import { render } from './scenegraph'; + +const getDeselectedSeriesSelector = (state: GlobalChartState) => state.interactions.deselectedDataSeries; + +/** @internal */ +export const geometries = createCachedSelector( + [ + getHeatmapSpecSelector, + computeChartDimensionsSelector, + getSettingsSpecSelector, + getHeatmapTableSelector, + getColorScale, + getLegendItemsLabelsSelector, + getDeselectedSeriesSelector, + getGridHeightParamsSelector, + ], + ( + heatmapSpec, + chartDimensions, + settingSpec, + heatmapTable, + colorScale, + legendItems, + deselectedSeries, + gridHeightParams, + ): ShapeViewModel => { + const deselectedTicks = new Set( + deselectedSeries.map(({ specId }) => { + return Number(specId); + }), + ); + const { ticks } = colorScale; + const ranges = ticks.reduce>((acc, d, i) => { + if (deselectedTicks.has(d)) { + const rangeEnd = i + 1 === ticks.length ? null : ticks[i + 1]; + acc.push([d, rangeEnd]); + } + return acc; + }, []); + + return heatmapSpec + ? render(heatmapSpec, settingSpec, chartDimensions, heatmapTable, colorScale, ranges, gridHeightParams) + : nullShapeViewModel(); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_brush_area.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_brush_area.ts new file mode 100644 index 000000000000..0445c3ef17d6 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_brush_area.ts @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { BrushAxis } from '../../../../specs'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { Dimensions } from '../../../../utils/dimensions'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; + +const getMouseDownPosition = (state: GlobalChartState) => state.interactions.pointer.down; +const getIsDragging = (state: GlobalChartState) => state.interactions.pointer.dragging; +const getCurrentPointerPosition = (state: GlobalChartState) => state.interactions.pointer.current.position; + +/** @internal */ +export const getBrushAreaSelector = createCachedSelector( + [ + getIsDragging, + getMouseDownPosition, + getCurrentPointerPosition, + getChartRotationSelector, + getSettingsSpecSelector, + computeChartDimensionsSelector, + ], + (isDragging, mouseDownPosition, end, chartRotation, { brushAxis }, chartDimensions): Dimensions | null => { + if (!isDragging || !mouseDownPosition) { + return null; + } + const start = { + x: mouseDownPosition.position.x - chartDimensions.left, + y: mouseDownPosition.position.y, + }; + switch (brushAxis) { + case BrushAxis.Both: + default: + return { top: start.y, left: start.x, width: end.x - start.x - chartDimensions.left, height: end.y - start.y }; + } + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.ts new file mode 100644 index 000000000000..184dc0e55cad --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { DragShape } from '../../layout/types/viewmodel_types'; +import { geometries } from './geometries'; + +function getCurrentPointerStates(state: GlobalChartState) { + return state.interactions.pointer; +} + +/** @internal */ +export const getBrushedHighlightedShapesSelector = createCachedSelector( + [geometries, getCurrentPointerStates], + (geoms, pointerStates): DragShape | null => { + if (!pointerStates.dragging || !pointerStates.down) { + return null; + } + + const { + down: { + position: { x: startX, y: startY }, + }, + current: { + position: { x: endX, y: endY }, + }, + } = pointerStates; + + const shape = geoms.pickDragShape([ + { x: startX, y: startY }, + { x: endX, y: endY }, + ]); + return shape; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_color_scale.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_color_scale.ts new file mode 100644 index 000000000000..0ee0f3363fb6 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_color_scale.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { extent as d3Extent } from 'd3-array'; +import { interpolateHcl } from 'd3-interpolate'; +import { + ScaleLinear, + scaleLinear, + ScaleQuantile, + scaleQuantile, + ScaleQuantize, + scaleQuantize, + ScaleThreshold, + scaleThreshold, +} from 'd3-scale'; +import createCachedSelector from 're-reselect'; + +import { ScaleType } from '../../../../scales/constants'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getHeatmapSpecSelector } from './get_heatmap_spec'; +import { getHeatmapTableSelector } from './get_heatmap_table'; + +type ScaleModelType = { + type: Type; + config: Config; + ticks: number[]; +}; +type ScaleLinearType = ScaleModelType>; +type ScaleQuantizeType = ScaleModelType>; +type ScaleQuantileType = ScaleModelType>; +type ScaleThresholdType = ScaleModelType>; +/** @internal */ +export type ColorScaleType = ScaleLinearType | ScaleQuantizeType | ScaleQuantileType | ScaleThresholdType; + +/** + * @internal + * Gets color scale based on specification and values range. + */ +export const getColorScale = createCachedSelector( + [getHeatmapSpecSelector, getHeatmapTableSelector], + (spec, heatmapTable) => { + const { colors, colorScale: colorScaleSpec } = spec; + + // compute the color scale based domain and colors + const { ranges = heatmapTable.extent } = spec; + const colorRange = colors ?? ['green', 'red']; + + const colorScale = { + type: colorScaleSpec, + } as ColorScaleType; + if (colorScale.type === ScaleType.Quantize) { + colorScale.config = scaleQuantize() + .domain(d3Extent(ranges) as [number, number]) + .range(colorRange); + colorScale.ticks = colorScale.config.ticks(spec.colors.length); + } else if (colorScale.type === ScaleType.Quantile) { + colorScale.config = scaleQuantile().domain(ranges).range(colorRange); + colorScale.ticks = colorScale.config.quantiles(); + } else if (colorScale.type === ScaleType.Threshold) { + colorScale.config = scaleThreshold().domain(ranges).range(colorRange); + colorScale.ticks = colorScale.config.domain(); + } else { + colorScale.config = scaleLinear().domain(ranges).interpolate(interpolateHcl).range(colorRange); + colorScale.ticks = addBaselineOnLinearScale(ranges[0], ranges[1], colorScale.config.ticks(6)); + } + return colorScale; + }, +)(getChartIdSelector); + +function addBaselineOnLinearScale(min: number, max: number, ticks: Array): Array { + if (min < 0 && max < 0) { + return [...ticks, 0]; + } + if (min >= 0 && max >= 0) { + return [0, ...ticks]; + } + + return ticks; +} diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_cursor_pointer.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_cursor_pointer.ts new file mode 100644 index 000000000000..7a67dfd27c59 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_cursor_pointer.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { DEFAULT_CSS_CURSOR } from '../../../../common/constants'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { isBrushingSelector } from './is_brushing'; +import { getPickedShapes } from './picked_shapes'; + +/** @internal */ +export const getPointerCursorSelector = createCachedSelector( + [getPickedShapes, isBrushingSelector], + (pickedShapes, isBrushing) => { + return isBrushing || (Array.isArray(pickedShapes) && pickedShapes.length > 0) ? 'pointer' : DEFAULT_CSS_CURSOR; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_debug_state.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_debug_state.ts new file mode 100644 index 000000000000..48c334080f35 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_debug_state.ts @@ -0,0 +1,92 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { RGBtoString } from '../../../../common/color_library_wrappers'; +import { LegendItem } from '../../../../common/legend'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { DebugState, DebugStateLegend } from '../../../../state/types'; +import { Position } from '../../../../utils/common'; +import { computeLegendSelector } from './compute_legend'; +import { geometries } from './geometries'; +import { getHighlightedAreaSelector, getHighlightedDataSelector } from './get_highlighted_area'; +import { getPickedCells } from './get_picked_cells'; + +/** + * Returns a stringified version of the `debugState` + * @internal + */ +export const getDebugStateSelector = createCachedSelector( + [geometries, computeLegendSelector, getHighlightedAreaSelector, getPickedCells, getHighlightedDataSelector], + (geoms, legend, pickedArea, pickedCells, highlightedData): DebugState => { + return { + // Common debug state + legend: getLegendState(legend), + axes: { + x: [ + { + id: 'x', + position: Position.Left, + labels: geoms.heatmapViewModel.xValues.map(({ text }) => text), + values: geoms.heatmapViewModel.xValues.map(({ value }) => value), + // vertical lines + gridlines: geoms.heatmapViewModel.gridLines.x.map((line) => ({ x: line.x1, y: line.y2 })), + }, + ], + y: [ + { + id: 'y', + position: Position.Bottom, + labels: geoms.heatmapViewModel.yValues.map(({ text }) => text), + values: geoms.heatmapViewModel.yValues.map(({ value }) => value), + // horizontal lines + gridlines: geoms.heatmapViewModel.gridLines.y.map((line) => ({ x: line.x2, y: line.y1 })), + }, + ], + }, + // Heatmap debug state + heatmap: { + cells: geoms.heatmapViewModel.cells.map(({ x, y, fill, formatted, value }) => ({ + x, + y, + fill: RGBtoString(fill.color), + formatted, + value, + })), + selection: { + area: pickedArea, + data: highlightedData, + }, + }, + }; + }, +)(getChartIdSelector); + +function getLegendState(legendItems: LegendItem[]): DebugStateLegend { + const items = legendItems + .filter(({ isSeriesHidden }) => !isSeriesHidden) + .map(({ label: name, color, seriesIdentifiers: [{ key }] }) => ({ + key, + name, + color, + })); + + return { items }; +} diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_grid_full_height.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_grid_full_height.ts new file mode 100644 index 000000000000..4a062df58e12 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_grid_full_height.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getLegendSizeSelector } from '../../../../state/selectors/get_legend_size'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { isHorizontalLegend } from '../../../../utils/legend'; +import { Config } from '../../layout/types/config_types'; +import { getHeatmapConfigSelector } from './get_heatmap_config'; +import { getHeatmapTableSelector } from './get_heatmap_table'; + +/** @internal */ +export interface GridHeightParams { + height: number; + gridCellHeight: number; + pageSize: number; +} +const getParentDimension = (state: GlobalChartState) => state.parentDimensions; + +/** @internal */ +export const getGridHeightParamsSelector = createCachedSelector( + [ + getLegendSizeSelector, + getSettingsSpecSelector, + getParentDimension, + getHeatmapConfigSelector, + getHeatmapTableSelector, + ], + ( + legendSize, + { showLegend }, + { height: containerHeight }, + { xAxisLabel: { padding, visible, fontSize }, grid, maxLegendHeight }, + { yValues }, + ): GridHeightParams => { + const xAxisHeight = visible ? fontSize : 0; + const totalVerticalPadding = padding * 2; + let legendHeight = 0; + if (showLegend && isHorizontalLegend(legendSize.position)) { + legendHeight = maxLegendHeight ?? legendSize.height; + } + const verticalRemainingSpace = containerHeight - xAxisHeight - totalVerticalPadding - legendHeight; + + // compute the grid cell height + const gridCellHeight = getGridCellHeight(yValues, grid, verticalRemainingSpace); + const height = gridCellHeight * yValues.length; + + const pageSize = + gridCellHeight > 0 && height > containerHeight + ? Math.floor(verticalRemainingSpace / gridCellHeight) + : yValues.length; + return { + height, + gridCellHeight, + pageSize, + }; + }, +)(getChartIdSelector); + +function getGridCellHeight(yValues: Array, grid: Config['grid'], height: number): number { + if (yValues.length === 0) { + return height; + } + const stretchedHeight = height / yValues.length; + + if (stretchedHeight < grid.cellHeight.min) { + return grid.cellHeight.min; + } + if (grid.cellHeight.max !== 'fill' && stretchedHeight > grid.cellHeight.max) { + return grid.cellHeight.max; + } + + return stretchedHeight; +} diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_heatmap_config.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_heatmap_config.ts new file mode 100644 index 000000000000..857e31368b1c --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_heatmap_config.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { mergePartial } from '../../../../utils/common'; +import { config as defaultConfig } from '../../layout/config/config'; +import { Config } from '../../layout/types/config_types'; +import { getHeatmapSpecSelector } from './get_heatmap_spec'; + +/** @internal */ +export const getHeatmapConfigSelector = createCachedSelector( + [getHeatmapSpecSelector], + (spec): Config => { + return mergePartial(defaultConfig, spec.config, { mergeOptionalPartialValues: true }); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_heatmap_container_size.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_heatmap_container_size.ts new file mode 100644 index 000000000000..d5df6246c8eb --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_heatmap_container_size.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getLegendConfigSelector } from '../../../../state/selectors/get_legend_config_selector'; +import { getLegendSizeSelector } from '../../../../state/selectors/get_legend_size'; +import { LayoutDirection } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { getHeatmapConfigSelector } from './get_heatmap_config'; + +const getParentDimension = (state: GlobalChartState) => state.parentDimensions; + +/** + * Gets charts grid area excluding legend and X,Y axis labels and paddings. + * @internal + */ +export const getHeatmapContainerSizeSelector = createCachedSelector( + [getParentDimension, getLegendSizeSelector, getHeatmapConfigSelector, getLegendConfigSelector], + (parentDimensions, legendSize, { maxLegendHeight }, { showLegend, legendPosition }): Dimensions => { + if (!showLegend || legendPosition.floating) { + return parentDimensions; + } + if (legendPosition.direction === LayoutDirection.Vertical) { + return { + left: 0, + top: 0, + width: parentDimensions.width - legendSize.width - legendSize.margin * 2, + height: parentDimensions.height, + }; + } + + const legendHeight = maxLegendHeight ?? legendSize.height + legendSize.margin * 2; + + return { + left: 0, + top: 0, + width: parentDimensions.width, + height: parentDimensions.height - legendHeight, + }; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_heatmap_spec.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_heatmap_spec.ts new file mode 100644 index 000000000000..f6c98bec6789 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_heatmap_spec.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { ChartType } from '../../..'; +import { SpecType } from '../../../../specs'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSpecs } from '../../../../state/selectors/get_settings_specs'; +import { getSpecsFromStore } from '../../../../state/utils'; +import { HeatmapSpec } from '../../specs'; + +/** @internal */ +export const getHeatmapSpecSelector = createCachedSelector([getSpecs], (specs) => { + const spec = getSpecsFromStore(specs, ChartType.Heatmap, SpecType.Series); + return spec[0]; +})(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_heatmap_table.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_heatmap_table.ts new file mode 100644 index 000000000000..63e3c2c8298c --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_heatmap_table.ts @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getPredicateFn } from '../../../../common/predicate'; +import { ScaleType } from '../../../../scales/constants'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getAccessorValue } from '../../../../utils/accessor'; +import { mergeXDomain } from '../../../xy_chart/domains/x_domain'; +import { getXNiceFromSpec, getXScaleTypeFromSpec } from '../../../xy_chart/scales/get_api_scales'; +import { X_SCALE_DEFAULT } from '../../specs/scale_defaults'; +import { HeatmapTable } from './compute_chart_dimensions'; +import { getHeatmapSpecSelector } from './get_heatmap_spec'; + +/** + * Extracts axis and cell values from the input data. + * @internal + */ +export const getHeatmapTableSelector = createCachedSelector( + [getHeatmapSpecSelector, getSettingsSpecSelector], + (spec, settingsSpec): HeatmapTable => { + const { data, valueAccessor, xAccessor, yAccessor, xSortPredicate, ySortPredicate } = spec; + const { xDomain } = settingsSpec; + + const resultData = data.reduce( + (acc, curr, index) => { + const x = getAccessorValue(curr, xAccessor); + + const y = getAccessorValue(curr, yAccessor); + const value = getAccessorValue(curr, valueAccessor); + + // compute the data domain extent + const [min, max] = acc.extent; + acc.extent = [Math.min(min, value), Math.max(max, value)]; + + acc.table.push({ + x, + y, + value, + originalIndex: index, + }); + + if (!acc.xValues.includes(x)) { + acc.xValues.push(x); + } + if (!acc.yValues.includes(y)) { + acc.yValues.push(y); + } + + return acc; + }, + { + table: [], + xValues: [], + yValues: [], + extent: [+Infinity, -Infinity], + }, + ); + + resultData.xDomain = mergeXDomain( + { + type: getXScaleTypeFromSpec(spec.xScaleType), + nice: getXNiceFromSpec(), + isBandScale: false, + desiredTickCount: X_SCALE_DEFAULT.desiredTickCount, + customDomain: xDomain, + }, + resultData.xValues, + ); + + // sort values by their predicates + if (spec.xScaleType === ScaleType.Ordinal) { + resultData.xDomain.domain.sort(getPredicateFn(xSortPredicate)); + } + resultData.yValues.sort(getPredicateFn(ySortPredicate)); + + return resultData; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_highlighted_area.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_highlighted_area.ts new file mode 100644 index 000000000000..4a4a2fe2d38d --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_highlighted_area.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { geometries } from './geometries'; +import { getHeatmapSpecSelector } from './get_heatmap_spec'; +import { isBrushingSelector } from './is_brushing'; + +/** + * @internal + */ +export const getHighlightedDataSelector = createCachedSelector( + [getHeatmapSpecSelector, isBrushingSelector], + (spec, isBrushing) => { + if (!spec.highlightedData || isBrushing) { + return null; + } + return spec.highlightedData; + }, +)(getChartIdSelector); + +/** + * Returns rect position of the highlighted selection. + * @internal + */ +export const getHighlightedAreaSelector = createCachedSelector( + [geometries, getHeatmapSpecSelector, isBrushingSelector], + (geoms, spec, isBrushing) => { + if (!spec.highlightedData || isBrushing) { + return null; + } + return geoms.pickHighlightedArea(spec.highlightedData.x, spec.highlightedData.y); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_legend_items_labels.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_legend_items_labels.ts new file mode 100644 index 000000000000..68c1dbea2965 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_legend_items_labels.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { LegendItemLabel } from '../../../../state/selectors/get_legend_items_labels'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { computeLegendSelector } from './compute_legend'; + +/** @internal */ +export const getLegendItemsLabelsSelector = createCachedSelector( + [computeLegendSelector, getSettingsSpecSelector], + (legendItems, { showLegendExtra }): LegendItemLabel[] => + legendItems.map(({ label, defaultExtra }) => { + if (defaultExtra?.formatted != null) { + return { label: `${label}${showLegendExtra ? defaultExtra.formatted : ''}`, depth: 0 }; + } + return { label, depth: 0 }; + }), +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_picked_cells.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_picked_cells.ts new file mode 100644 index 000000000000..b948afabee1d --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_picked_cells.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getLastDragSelector } from '../../../../state/selectors/get_last_drag'; +import { PickDragFunction } from '../../layout/types/viewmodel_types'; +import { geometries } from './geometries'; + +/** @internal */ +export const getPickedCells = createCachedSelector( + [geometries, getLastDragSelector], + (geoms, dragState): ReturnType | null => { + if (!dragState) { + return null; + } + + const { + start: { + position: { x: startX, y: startY }, + }, + end: { + position: { x: endX, y: endY }, + }, + } = dragState; + + return geoms.pickDragArea([ + { x: startX, y: startY }, + { x: endX, y: endY }, + ]); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_tooltip_anchor.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_tooltip_anchor.ts new file mode 100644 index 000000000000..07e1f320739f --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_tooltip_anchor.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { AnchorPosition } from '../../../../components/portal/types'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { getPickedShapes } from './picked_shapes'; + +function getCurrentPointerPosition(state: GlobalChartState) { + return state.interactions.pointer.current.position; +} + +/** @internal */ +export const getTooltipAnchorSelector = createCachedSelector( + [getPickedShapes, computeChartDimensionsSelector, getCurrentPointerPosition], + (shapes, chartDimensions, position): AnchorPosition => { + if (Array.isArray(shapes) && shapes.length > 0) { + const firstShape = shapes[0]; + return { + x: firstShape.x + chartDimensions.left, + width: firstShape.width, + y: firstShape.y - chartDimensions.top, + height: firstShape.height, + }; + } + return { + x: position.x, + width: 0, + y: position.y, + height: 0, + }; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_x_axis_right_overflow.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_x_axis_right_overflow.ts new file mode 100644 index 000000000000..7a2bccc20cbc --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/get_x_axis_right_overflow.ts @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { ScaleContinuous } from '../../../../scales'; +import { ScaleType } from '../../../../scales/constants'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { CanvasTextBBoxCalculator } from '../../../../utils/bbox/canvas_text_bbox_calculator'; +import { getHeatmapConfigSelector } from './get_heatmap_config'; +import { getHeatmapTableSelector } from './get_heatmap_table'; + +/** + * @internal + * Gets color scale based on specification and values range. + */ +export const getXAxisRightOverflow = createCachedSelector( + [getHeatmapConfigSelector, getHeatmapTableSelector], + ({ xAxisLabel: { fontSize, fontFamily, padding, formatter, width }, timeZone }, { xDomain }): number => { + if (xDomain.type !== ScaleType.Time) { + return 0; + } + if (typeof width === 'number') { + return width / 2; + } + + const timeScale = new ScaleContinuous( + { + type: ScaleType.Time, + domain: xDomain.domain, + range: [0, 1], + }, + { + timeZone, + }, + ); + const bboxCompute = new CanvasTextBBoxCalculator(); + const maxTextWidth = timeScale.ticks().reduce((acc, d) => { + const text = formatter(d); + const textSize = bboxCompute.compute(text, padding, fontSize, fontFamily, 1); + return Math.max(acc, textSize.width + padding); + }, 0); + bboxCompute.destroy(); + return maxTextWidth / 2; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/heatmap_spec.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/heatmap_spec.ts new file mode 100644 index 000000000000..4762252fa7f6 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/heatmap_spec.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartType } from '../../..'; +import { SpecType } from '../../../../specs/constants'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getSpecsFromStore } from '../../../../state/utils'; +import { HeatmapSpec } from '../../specs/heatmap'; + +/** @internal */ +export function getSpecOrNull(state: GlobalChartState): HeatmapSpec | null { + const specs = getSpecsFromStore(state.specs, ChartType.Heatmap, SpecType.Series); + return specs.length > 0 ? specs[0] : null; +} diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/is_brush_available.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/is_brush_available.ts new file mode 100644 index 000000000000..c29600a6a8f1 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/is_brush_available.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getHeatmapConfigSelector } from './get_heatmap_config'; + +/** + * The brush is available only if a onBrushEnd listener is configured + * @internal + */ +export const isBrushAvailableSelector = createCachedSelector([getHeatmapConfigSelector], (config): boolean => { + return Boolean(config.onBrushEnd) && config.brushTool.visible; +})(getChartIdSelector); + +/** @internal */ +export const isBrushEndProvided = createCachedSelector([getHeatmapConfigSelector], (config): boolean => { + return Boolean(config.onBrushEnd); +})(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/is_brushing.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/is_brushing.ts new file mode 100644 index 000000000000..9aacadc31d00 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/is_brushing.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; + +const getPointerSelector = (state: GlobalChartState) => state.interactions.pointer; + +/** @internal */ +export const isBrushingSelector = createCachedSelector([getPointerSelector], (pointer): boolean => { + return pointer.dragging; +})(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/is_tooltip_visible.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/is_tooltip_visible.ts new file mode 100644 index 000000000000..1c3c0d0fb3b5 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/is_tooltip_visible.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getTooltipType } from '../../../../specs'; +import { TooltipType } from '../../../../specs/constants'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getTooltipInfoSelector } from './tooltip'; + +/** @internal */ +export const isTooltipVisibleSelector = createCachedSelector( + [getSettingsSpecSelector, getTooltipInfoSelector], + (settingsSpec, tooltipInfo): boolean => { + if (getTooltipType(settingsSpec) === TooltipType.None) { + return false; + } + return tooltipInfo.values.length > 0; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/on_brush_end_caller.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/on_brush_end_caller.ts new file mode 100644 index 000000000000..f800cac84c52 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/on_brush_end_caller.ts @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; +import { Selector } from 'reselect'; + +import { ChartType } from '../../..'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getLastDragSelector } from '../../../../state/selectors/get_last_drag'; +import { DragCheckProps, hasDragged } from '../../../../utils/events'; +import { getHeatmapConfigSelector } from './get_heatmap_config'; +import { getPickedCells } from './get_picked_cells'; +import { getSpecOrNull } from './heatmap_spec'; +import { isBrushEndProvided } from './is_brush_available'; + +/** + * Will call the onBrushEnd listener every time the following preconditions are met: + * - the onBrushEnd listener is available + * - we dragged the mouse pointer + * @internal + */ +export function createOnBrushEndCaller(): (state: GlobalChartState) => void { + let prevProps: DragCheckProps | null = null; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartType.Heatmap) { + if (!isBrushEndProvided(state)) { + selector = null; + prevProps = null; + return; + } + selector = createCachedSelector( + [getLastDragSelector, getSpecOrNull, getHeatmapConfigSelector, getPickedCells], + (lastDrag, spec, { onBrushEnd }, pickedCells): void => { + const nextProps: DragCheckProps = { + lastDrag, + onBrushEnd, + }; + if (!spec || !onBrushEnd || pickedCells === null) { + return; + } + if (lastDrag !== null && hasDragged(prevProps, nextProps)) { + onBrushEnd(pickedCells); + } + prevProps = nextProps; + }, + )({ + keySelector: getChartIdSelector, + }); + } + if (selector) { + selector(state); + } + }; +} diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/on_element_click_caller.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/on_element_click_caller.ts new file mode 100644 index 000000000000..06807dba7f90 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/on_element_click_caller.ts @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; +import { Selector } from 'reselect'; + +import { ChartType } from '../../..'; +import { SeriesIdentifier } from '../../../../common/series_id'; +import { SettingsSpec } from '../../../../specs'; +import { GlobalChartState, PointerState } from '../../../../state/chart_state'; +import { getLastClickSelector } from '../../../../state/selectors/get_last_click'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { isClicking } from '../../../../state/utils'; +import { Cell, isPickedCells } from '../../layout/types/viewmodel_types'; +import { getSpecOrNull } from './heatmap_spec'; +import { getPickedShapes } from './picked_shapes'; + +/** + * Will call the onElementClick listener every time the following preconditions are met: + * - the onElementClick listener is available + * - we have at least one highlighted geometry + * - the pointer state goes from down state to up state + * @internal + */ +export function createOnElementClickCaller(): (state: GlobalChartState) => void { + let prevClick: PointerState | null = null; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartType.Heatmap) { + selector = createCachedSelector( + [getSpecOrNull, getLastClickSelector, getSettingsSpecSelector, getPickedShapes], + (spec, lastClick: PointerState | null, settings: SettingsSpec, pickedShapes): void => { + if (!spec) { + return; + } + if (!settings.onElementClick) { + return; + } + if (!isPickedCells(pickedShapes)) { + return; + } + const nextPickedShapesLength = pickedShapes.length; + if (nextPickedShapesLength > 0 && isClicking(prevClick, lastClick) && settings && settings.onElementClick) { + const elements = pickedShapes.map<[Cell, SeriesIdentifier]>((value) => [ + value, + { + specId: spec.id, + key: `spec{${spec.id}}`, + }, + ]); + settings.onElementClick(elements); + } + prevClick = lastClick; + }, + )({ + keySelector: (state: GlobalChartState) => state.chartId, + }); + } + if (selector) { + selector(state); + } + }; +} diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/on_element_out_caller.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/on_element_out_caller.ts new file mode 100644 index 000000000000..cab0e9e75aad --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/on_element_out_caller.ts @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; +import { Selector } from 'react-redux'; + +import { ChartType } from '../../..'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { isPickedCells } from '../../layout/types/viewmodel_types'; +import { getSpecOrNull } from './heatmap_spec'; +import { getPickedShapes } from './picked_shapes'; + +/** + * Will call the onElementOut listener every time the following preconditions are met: + * - the onElementOut listener is available + * - the highlighted geometries list goes from a list of at least one object to an empty one + * @internal + */ +export function createOnElementOutCaller(): (state: GlobalChartState) => void { + let prevPickedShapes: number | null = null; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartType.Heatmap) { + selector = createCachedSelector( + [getSpecOrNull, getPickedShapes, getSettingsSpecSelector], + (spec, pickedShapes, settings): void => { + if (!spec) { + return; + } + if (!settings.onElementOut) { + return; + } + const nextPickedShapes = isPickedCells(pickedShapes) ? pickedShapes.length : 0; + + if (prevPickedShapes !== null && prevPickedShapes > 0 && nextPickedShapes === 0) { + settings.onElementOut(); + } + prevPickedShapes = nextPickedShapes; + }, + )({ + keySelector: getChartIdSelector, + }); + } + if (selector) { + selector(state); + } + }; +} diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/on_element_over_caller.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/on_element_over_caller.ts new file mode 100644 index 000000000000..906b4c525298 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/on_element_over_caller.ts @@ -0,0 +1,92 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; +import { Selector } from 'react-redux'; + +import { ChartType } from '../../..'; +import { SeriesIdentifier } from '../../../../common/series_id'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { Cell, isPickedCells } from '../../layout/types/viewmodel_types'; +import { getSpecOrNull } from './heatmap_spec'; +import { getPickedShapes } from './picked_shapes'; + +function isOverElement(prev: Cell[] = [], next: Cell[]) { + if (next.length === 0) { + return; + } + if (next.length !== prev.length) { + return true; + } + return !next.every((nextCell, index) => { + const prevCell = prev[index]; + if (prevCell === null) { + return false; + } + return nextCell.value === prevCell.value && nextCell.x === prevCell.x && nextCell.y === prevCell.y; + }); +} + +/** + * Will call the onElementOver listener every time the following preconditions are met: + * - the onElementOver listener is available + * - we have a new set of highlighted geometries on our state + * @internal + */ +export function createOnElementOverCaller(): (state: GlobalChartState) => void { + let prevPickedShapes: Cell[] = []; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartType.Heatmap) { + selector = createCachedSelector( + [getSpecOrNull, getPickedShapes, getSettingsSpecSelector], + (spec, nextPickedShapes, settings): void => { + if (!spec) { + return; + } + if (!settings.onElementOver) { + return; + } + if (!isPickedCells(nextPickedShapes)) { + return; + } + + if (isOverElement(prevPickedShapes, nextPickedShapes)) { + const elements = nextPickedShapes.map<[Cell, SeriesIdentifier]>((value) => [ + value, + { + specId: spec.id, + key: `spec{${spec.id}}`, + }, + ]); + settings.onElementOver(elements); + } + prevPickedShapes = nextPickedShapes; + }, + )({ + keySelector: getChartIdSelector, + }); + } + if (selector) { + selector(state); + } + }; +} diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/picked_shapes.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/picked_shapes.ts new file mode 100644 index 000000000000..1e4f9155dd90 --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/picked_shapes.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { Cell } from '../../layout/types/viewmodel_types'; +import { TextBox } from '../../layout/viewmodel/viewmodel'; +import { geometries } from './geometries'; + +function getCurrentPointerPosition(state: GlobalChartState) { + return state.interactions.pointer.current.position; +} + +/** @internal */ +export const getPickedShapes = createCachedSelector([geometries, getCurrentPointerPosition], (geoms, pointerPosition): + | Cell[] + | TextBox => { + const picker = geoms.pickQuads; + const { x, y } = pointerPosition; + return picker(x, y); +})(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/scenegraph.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/scenegraph.ts new file mode 100644 index 000000000000..4c29478edf3a --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/scenegraph.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { measureText } from '../../../../common/text_utils'; +import { SettingsSpec } from '../../../../specs'; +import { RecursivePartial, mergePartial } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { config as defaultConfig } from '../../layout/config/config'; +import { Config } from '../../layout/types/config_types'; +import { ShapeViewModel, nullShapeViewModel } from '../../layout/types/viewmodel_types'; +import { shapeViewModel } from '../../layout/viewmodel/viewmodel'; +import { HeatmapSpec } from '../../specs'; +import { HeatmapTable } from './compute_chart_dimensions'; +import { ColorScaleType } from './get_color_scale'; +import { GridHeightParams } from './get_grid_full_height'; + +/** @internal */ +export function render( + spec: HeatmapSpec, + settingsSpec: SettingsSpec, + chartDimensions: Dimensions, + heatmapTable: HeatmapTable, + colorScale: ColorScaleType, + filterRanges: Array<[number, number | null]>, + gridHeightParams: GridHeightParams, +): ShapeViewModel { + const textMeasurer = document.createElement('canvas'); + const textMeasurerCtx = textMeasurer.getContext('2d'); + if (!textMeasurerCtx) { + return nullShapeViewModel(); + } + const { width, height } = chartDimensions; + const { config: specConfig } = spec; + const partialConfig: RecursivePartial = { ...specConfig, width, height }; + const config = mergePartial(defaultConfig, partialConfig); + return shapeViewModel( + measureText(textMeasurerCtx), + spec, + config, + settingsSpec, + chartDimensions, + heatmapTable, + colorScale, + filterRanges, + gridHeightParams, + ); +} diff --git a/packages/osd-charts/src/chart_types/heatmap/state/selectors/tooltip.ts b/packages/osd-charts/src/chart_types/heatmap/state/selectors/tooltip.ts new file mode 100644 index 000000000000..e5851b4f0d5a --- /dev/null +++ b/packages/osd-charts/src/chart_types/heatmap/state/selectors/tooltip.ts @@ -0,0 +1,114 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { RGBtoString } from '../../../../common/color_library_wrappers'; +import { TooltipInfo } from '../../../../components/tooltip/types'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getHeatmapConfigSelector } from './get_heatmap_config'; +import { getSpecOrNull } from './heatmap_spec'; +import { getPickedShapes } from './picked_shapes'; + +const EMPTY_TOOLTIP = Object.freeze({ + header: null, + values: [], +}); + +/** @internal */ +export const getTooltipInfoSelector = createCachedSelector( + [getSpecOrNull, getHeatmapConfigSelector, getPickedShapes], + (spec, config, pickedShapes): TooltipInfo => { + if (!spec) { + return EMPTY_TOOLTIP; + } + + const tooltipInfo: TooltipInfo = { + header: null, + values: [], + }; + + if (Array.isArray(pickedShapes)) { + pickedShapes + .filter(({ visible }) => visible) + .forEach((shape) => { + // X-axis value + tooltipInfo.values.push({ + label: config.xAxisLabel.name, + color: 'transparent', + isHighlighted: false, + isVisible: true, + seriesIdentifier: { + specId: spec.id, + key: spec.id, + }, + value: `${shape.datum.x}`, + formattedValue: config.xAxisLabel.formatter(shape.datum.x), + datum: shape.datum, + }); + + // Y-axis value + tooltipInfo.values.push({ + label: config.yAxisLabel.name, + color: 'transparent', + isHighlighted: false, + isVisible: true, + seriesIdentifier: { + specId: spec.id, + key: spec.id, + }, + value: `${shape.datum.y}`, + formattedValue: config.yAxisLabel.formatter(shape.datum.y), + datum: shape.datum, + }); + + // Cell value + tooltipInfo.values.push({ + label: spec.name ?? spec.id, + color: RGBtoString(shape.fill.color), + isHighlighted: false, + isVisible: true, + seriesIdentifier: { + specId: spec.id, + key: spec.id, + }, + value: `${shape.value}`, + formattedValue: `${shape.formatted}`, + datum: shape.datum, + }); + }); + } else { + tooltipInfo.values.push({ + label: ``, + color: 'transparent', + isHighlighted: false, + isVisible: true, + seriesIdentifier: { + specId: spec.id, + key: spec.id, + }, + value: `${pickedShapes.value}`, + formattedValue: `${pickedShapes.value}`, + datum: pickedShapes.value, + }); + } + + return tooltipInfo; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/index.ts b/packages/osd-charts/src/chart_types/index.ts new file mode 100644 index 000000000000..9a162e01aadf --- /dev/null +++ b/packages/osd-charts/src/chart_types/index.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $Values } from 'utility-types'; + +/** + * Available chart types + * @public + */ +export const ChartType = Object.freeze({ + Global: 'global' as const, + Goal: 'goal' as const, + Partition: 'partition' as const, + XYAxis: 'xy_axis' as const, + Heatmap: 'heatmap' as const, + Wordcloud: 'wordcloud' as const, +}); +/** @public */ +export type ChartType = $Values; diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/config.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/config.ts new file mode 100644 index 000000000000..04e1ce8b580a --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/config.ts @@ -0,0 +1,341 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ConfigItem, configMap, Numeric } from '../../../common/config_objects'; +import { GOLDEN_RATIO, TAU } from '../../../common/constants'; +import { FONT_STYLES, FONT_VARIANTS } from '../../../common/text_utils'; +import { Config, PartitionLayout } from './types/config_types'; +import { ShapeTreeNode } from './types/viewmodel_types'; +import { AGGREGATE_KEY, STATISTICS_KEY } from './utils/group_by_rollup'; + +const LOG_10 = Math.log(10); + +function significantDigitCount(d: number): number { + let n = Math.abs(parseFloat(String(d).replace('.', ''))); + if (n === 0) { + return 0; + } + while (n !== 0 && n % 10 === 0) { + n /= 10; + } + return Math.floor(Math.log(n) / LOG_10) + 1; +} + +/** @internal */ +export function sumValueGetter(node: ShapeTreeNode): number { + return node[AGGREGATE_KEY]; +} + +/* + * It's an unfortunate accident that 'parent' is used both + * - for linking an ArrayNode to a QuadViewModel, and + * - for recursively linking the parent ArrayNode to an ArrayNode (child) in the tree + * + * By extracting out the 'MODEL_KEY', we make the distinction clear, while the API, which depends on this, doesn't + * change. This makes an eventual API change a single-line change, assuming `[MODEL_KEY]` is used where needed, and just there + * + * Todo: + * - replace users' use of `s.parent` with `s[MODEL_KEY]` for the ShapeTreeNode -> ArrayNode access + * - change MODEL_KEY to something other than 'parent' when it's done (might still be breaking change) + */ +/** @public */ +export const MODEL_KEY = 'parent'; + +/** @public */ +export function percentValueGetter(node: ShapeTreeNode): number { + return (100 * node[AGGREGATE_KEY]) / node[MODEL_KEY][STATISTICS_KEY].globalAggregate; +} + +/** @public */ +export function ratioValueGetter(node: ShapeTreeNode): number { + return node[AGGREGATE_KEY] / node[MODEL_KEY][STATISTICS_KEY].globalAggregate; +} + +/** @public */ +export const VALUE_GETTERS = Object.freeze({ percent: percentValueGetter, ratio: ratioValueGetter } as const); +/** @public */ +export type ValueGetterName = keyof typeof VALUE_GETTERS; + +function defaultFormatter(d: number): string { + return Math.abs(d) >= 10000000 || Math.abs(d) < 0.001 + ? d.toExponential(Math.min(2, Math.max(0, significantDigitCount(d) - 1))) + : d.toLocaleString(void 0, { + maximumSignificantDigits: 4, + maximumFractionDigits: 3, + useGrouping: true, + }); +} + +/** @internal */ +export function percentFormatter(d: number): string { + return `${Math.round(d)}%`; +} + +const fontSettings = { + fontFamily: { + dflt: 'Sans-Serif', + type: 'string', + }, + fontSize: { dflt: 12, min: 4, max: 32, type: 'number' }, + fontStyle: { + dflt: 'normal', + type: 'string', + values: FONT_STYLES, + }, + fontVariant: { + dflt: 'normal', + type: 'string', + values: FONT_VARIANTS, + }, + fontWeight: { dflt: 400, min: 100, max: 900, type: 'number' }, +}; + +const valueFont = { + type: 'group', + values: { + /* + * Object.assign interprets the extant `undefined` as legit, so commenting it out till moving away from Object.assign in `const valueFont = ...` + * fontFamily: { + * dflt: undefined, + * type: 'string', + * }, + */ + fontWeight: fontSettings.fontWeight, + fontStyle: fontSettings.fontStyle, + fontVariant: fontSettings.fontVariant, + }, +}; + +/** @internal */ +export const configMetadata: Record = { + // shape geometry + width: { dflt: 300, min: 0, max: 1024, type: 'number', reconfigurable: false }, + height: { dflt: 150, min: 0, max: 1024, type: 'number', reconfigurable: false }, + margin: { + type: 'group', + values: { + left: { dflt: 0, min: -0.25, max: 0.25, type: 'number' }, + right: { dflt: 0, min: -0.25, max: 0.25, type: 'number' }, + top: { dflt: 0, min: -0.25, max: 0.25, type: 'number' }, + bottom: { dflt: 0, min: -0.25, max: 0.25, type: 'number' }, + }, + }, + outerSizeRatio: new Numeric({ + dflt: 1 / GOLDEN_RATIO, + min: 0.25, + max: 1, + reconfigurable: true, + documentation: + 'The diameter of the entire circle, relative to the smaller of the usable rectangular size (smaller of width/height minus the margins)', + }), // todo switch to `io-ts` style, generic way of combining static and runtime type info + emptySizeRatio: new Numeric({ + dflt: 0, + min: 0, + max: 0.8, + reconfigurable: true, + documentation: 'The diameter of the inner circle, relative to `outerSizeRatio`', + }), // todo switch to `io-ts` style, generic way of combining static and runtime type info + clockwiseSectors: { + dflt: true, + type: 'boolean', + documentation: 'Largest to smallest sectors are positioned in a clockwise order', + }, + specialFirstInnermostSector: { + dflt: true, + type: 'boolean', + documentation: 'Starts placement with the second largest slice, for the innermost pie/ring', + }, + + // general text config + fontFamily: { + dflt: 'Sans-Serif', + type: 'string', + }, + + // fill text config + minFontSize: { dflt: 8, min: 0.1, max: 8, type: 'number', reconfigurable: true }, + maxFontSize: { dflt: 64, min: 0.1, max: 64, type: 'number' }, + idealFontSizeJump: { + dflt: 1.05, // Math.pow(goldenRatio, 1 / 3), + min: 1.05, + max: GOLDEN_RATIO, + type: 'number', + reconfigurable: false, // there's no real reason to reconfigure it; finding the largest possible font is good for readability + }, + maximizeFontSize: { + dflt: false, + type: 'boolean', + }, + partitionLayout: { + dflt: PartitionLayout.sunburst, + type: 'string', + values: Object.keys(PartitionLayout), + }, + drilldown: { + dflt: false, + type: 'boolean', + }, + + // fill text layout config + circlePadding: { dflt: 2, min: 0.0, max: 8, type: 'number' }, + radialPadding: { dflt: TAU / 360, min: 0, max: 0.035, type: 'number' }, + horizontalTextAngleThreshold: { dflt: TAU / 12, min: 0, max: TAU, type: 'number' }, + horizontalTextEnforcer: { dflt: 1, min: 0, max: 1, type: 'number' }, + maxRowCount: { dflt: 12, min: 1, max: 16, type: 'number' }, + fillOutside: { dflt: false, type: 'boolean' }, + radiusOutside: { dflt: 128, min: 0, max: 1024, type: 'number' }, + fillRectangleWidth: { dflt: Infinity, reconfigurable: false, type: 'number' }, + fillRectangleHeight: { dflt: Infinity, reconfigurable: false, type: 'number' }, + fillLabel: { + type: 'group', + values: { + textColor: { type: 'color', dflt: '#000000' }, + textInvertible: { dflt: false, type: 'boolean' }, + textContrast: { dflt: false, type: 'boolean' || 'number' }, + ...fontSettings, + valueGetter: { + dflt: sumValueGetter, + type: 'function', + }, + valueFormatter: { + dflt: defaultFormatter, + type: 'function', + }, + valueFont, + padding: { + type: 'group', + values: { + top: { + dflt: 2, + min: 0, + max: 20, + type: 'number', + reconfigurable: true, + documentation: 'Top padding for fill text', + }, + bottom: { + dflt: 2, + min: 0, + max: 20, + type: 'number', + reconfigurable: true, + documentation: 'Bottom padding for fill text', + }, + left: { + dflt: 2, + min: 0, + max: 20, + type: 'number', + reconfigurable: true, + documentation: 'Left padding for fill text', + }, + right: { + dflt: 2, + min: 0, + max: 20, + type: 'number', + reconfigurable: true, + documentation: 'Right padding for fill text', + }, + }, + }, + clipText: { + type: 'boolean', + dflt: false, + documentation: "Renders, but clips, text that's longer than what would fit in a box entirely", + }, + }, + }, + + // linked labels (primarily: single-line) + linkLabel: { + type: 'group', + values: { + maximumSection: { + dflt: 10, + min: 0, + max: 10000, + type: 'number', + reconfigurable: true, + documentation: 'Uses linked labels below this limit of the outer sector arc length (in pixels)', + }, + ...fontSettings, + gap: { dflt: 10, min: 6, max: 16, type: 'number' }, + spacing: { dflt: 2, min: 0, max: 16, type: 'number' }, + horizontalStemLength: { dflt: 10, min: 6, max: 16, type: 'number' }, + radiusPadding: { dflt: 10, min: 6, max: 16, type: 'number' }, + lineWidth: { dflt: 1, min: 0.1, max: 2, type: 'number' }, + maxCount: { + dflt: 36, + min: 2, + max: 64, + type: 'number', + documentation: 'Limits the total count of linked labels. The first N largest slices are kept.', + }, + maxTextLength: { + dflt: 100, + min: 2, + max: 200, + documentation: 'Limits the total number of characters in linked labels.', + }, + textColor: { dflt: '#000000', type: 'color' }, + textInvertible: { dflt: false, type: 'boolean' }, + textContrast: { dflt: false, type: 'boolean' || 'number' }, + textOpacity: { dflt: 1, min: 0, max: 1, type: 'number' }, + minimumStemLength: { + dflt: 0, + min: 0, + max: 16, + type: 'number', + reconfigurable: false, // currently only 0 is reliable + }, + stemAngle: { + dflt: TAU / 8, + min: 0, + max: TAU, + type: 'number', + reconfigurable: false, // currently only tau / 8 is reliable + }, + valueFont, + }, + }, + + // other + backgroundColor: { dflt: '#ffffff', type: 'color' }, + sectorLineWidth: { dflt: 1, min: 0, max: 4, type: 'number' }, + sectorLineStroke: { dflt: 'white', type: 'string' }, + animation: { type: 'group', values: { duration: { dflt: 0, min: 0, max: 3000, type: 'number' } } }, +}; + +/** @internal */ +export const config: Config = configMap((item: ConfigItem) => item.dflt, configMetadata); + +/** + * Part-to-whole visualizations such as treemap, sunburst, pie hinge on an aggregation + * function such that the value is independent of the order of how the constituents are aggregated + * https://en.wikipedia.org/wiki/Associative_property + * Hierarchical, space-filling part-to-whole visualizations also need that the + * the value of a node is equal to the sum of the values of its children + * https://mboehm7.github.io/teaching/ss19_dbs/04_RelationalAlgebra.pdf p21 + * It's now `count` and `sum` but subject to change + * + * potential internal, or removable + * @internal + */ +export type AdditiveAggregation = 'count' | 'sum'; diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/types/config_types.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/types/config_types.ts new file mode 100644 index 000000000000..25ce2804f29c --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/types/config_types.ts @@ -0,0 +1,152 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $Values as Values } from 'utility-types'; + +import { Distance, Pixels, Radian, Radius, Ratio, SizeRatio, TimeMs } from '../../../../common/geometry'; +import { Font, FontFamily, PartialFont, TextContrast } from '../../../../common/text_utils'; +import { Color, StrokeStyle, ValueFormatter } from '../../../../utils/common'; +import { PerSideDistance } from '../../../../utils/dimensions'; + +/** @public */ +export const PartitionLayout = Object.freeze({ + sunburst: 'sunburst' as const, + treemap: 'treemap' as const, + icicle: 'icicle' as const, + flame: 'flame' as const, + mosaic: 'mosaic' as const, +}); + +/** @public */ +export type PartitionLayout = Values; // could use ValuesType + +/** @public */ +export type PerSidePadding = PerSideDistance; + +/** @public */ +export type Padding = Pixels | Partial; + +interface LabelConfig extends Font { + textColor: Color; + textInvertible: boolean; + textContrast: TextContrast; + textOpacity: Ratio; + valueFormatter: ValueFormatter; + valueFont: PartialFont; + padding: Padding; +} + +/** @public */ +export interface FillLabelConfig extends LabelConfig { + clipText: boolean; +} + +/** @public */ +export interface LinkLabelConfig extends LabelConfig { + fontSize: Pixels; // todo consider putting it in Font + maximumSection: Distance; // use linked labels below this limit + gap: Pixels; + spacing: Pixels; + minimumStemLength: Distance; + stemAngle: Radian; + horizontalStemLength: Distance; + radiusPadding: Distance; + lineWidth: Pixels; + maxCount: number; + maxTextLength: number; +} + +/** @public */ +export interface FillFontSizeRange { + minFontSize: Pixels; + maxFontSize: Pixels; + idealFontSizeJump: Ratio; + /** + * When `maximizeFontSize` is false (the default), text font will not be larger than font sizes in larger sectors/rectangles in the same pie chart, + * sunburst ring or treemap layer. When it is set to true, the largest font, not exceeding `maxFontSize`, that fits in the slice/sector/rectangle + * will be chosen for easier text readability, irrespective of the value. + */ + maximizeFontSize: boolean; +} + +/** @public */ +export interface RelativeMargins { + left: SizeRatio; + right: SizeRatio; + top: SizeRatio; + bottom: SizeRatio; +} + +// todo switch to `io-ts` style, generic way of combining static and runtime type info +/** @public */ +export interface StaticConfig extends FillFontSizeRange { + // shape geometry + width: number; + height: number; + margin: RelativeMargins; + emptySizeRatio: SizeRatio; + outerSizeRatio: SizeRatio; + clockwiseSectors: boolean; + specialFirstInnermostSector: boolean; + partitionLayout: PartitionLayout; + /** @alpha */ + drilldown: boolean; + + // general text config + fontFamily: FontFamily; + + // fill text layout config + circlePadding: Distance; + radialPadding: Distance; + horizontalTextAngleThreshold: Radian; + horizontalTextEnforcer: Ratio; + maxRowCount: number; + fillOutside: boolean; + radiusOutside: Radius; + fillRectangleWidth: Distance; + fillRectangleHeight: Distance; + fillLabel: FillLabelConfig; + + // linked labels (primarily: single-line) + linkLabel: LinkLabelConfig; + + // global + backgroundColor: Color; + sectorLineWidth: Pixels; + sectorLineStroke: StrokeStyle; +} + +/** @alpha */ +export type EasingFunction = (x: Ratio) => Ratio; + +/** @alpha */ +export interface AnimKeyframe { + time: number; + easingFunction: EasingFunction; + keyframeConfig: Partial; +} + +/** @public */ +export interface Config extends StaticConfig { + /** @alpha */ + animation: { + duration: TimeMs; + keyframes: Array; + }; +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/types/viewmodel_types.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/types/viewmodel_types.ts new file mode 100644 index 000000000000..b1093afc5f59 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/types/viewmodel_types.ts @@ -0,0 +1,251 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CategoryKey } from '../../../../common/category'; +import { + Coordinate, + Distance, + Pixels, + PointObject, + PointTuple, + PointTuples, + Radian, + SizeRatio, +} from '../../../../common/geometry'; +import { Font, VerticalAlignments } from '../../../../common/text_utils'; +import { GroupByAccessor } from '../../../../specs'; +import { LegendPath } from '../../../../state/actions/legend'; +import { Color } from '../../../../utils/common'; +import { ContinuousDomainFocus } from '../../renderer/canvas/partition'; +import { Layer } from '../../specs'; +import { config, MODEL_KEY, ValueGetterName } from '../config'; +import { ArrayNode, HierarchyOfArrays } from '../utils/group_by_rollup'; +import { LinkLabelsViewModelSpec } from '../viewmodel/link_text_layout'; +import { Config, PartitionLayout } from './config_types'; + +/** @internal */ +export type LinkLabelVM = { + linkLabels: PointTuples; + translate: PointTuple; + textAlign: CanvasTextAlign; + text: string; + valueText: string; + width: Distance; + valueWidth: Distance; + verticalOffset: Distance; +}; + +/** @internal */ +export interface RowBox extends Font { + text: string; + width: Distance; + verticalOffset: Distance; + wordBeginning: Distance; +} + +interface RowAnchor { + rowAnchorX: Coordinate; + rowAnchorY: Coordinate; +} + +/** @internal */ +export interface RowSpace extends RowAnchor { + maximumRowLength: Distance; +} + +/** @internal */ +export interface TextRow extends RowAnchor { + length: number; + maximumLength: number; + rowWords: Array; +} + +/** @internal */ +export interface RowSet { + id: string; + rows: Array; + fillTextColor?: string; + fontSize: number; + rotation: Radian; + verticalAlignment: VerticalAlignments; + leftAlign: boolean; // might be generalized into horizontalAlign - if needed + container?: any; + clipText?: boolean; +} + +/** @internal */ +export interface SmallMultiplesDescriptors { + smAccessorValue: ReturnType; + index: number; + innerIndex: number; +} + +/** @internal */ +export interface QuadViewModel extends ShapeTreeNode, SmallMultiplesDescriptors { + strokeWidth: number; + strokeStyle: string; + fillColor: string; + textColor: string; +} + +/** @internal */ +export interface OutsideLinksViewModel { + points: Array; +} + +/** @internal */ +export type PickFunction = (x: Pixels, y: Pixels, focus: ContinuousDomainFocus) => Array; + +/** @internal */ +export interface PartitionSmallMultiplesModel extends SmallMultiplesDescriptors { + panelTitle: string; + smAccessorValue: number | string; + partitionLayout: PartitionLayout; + top: SizeRatio; + left: SizeRatio; + width: SizeRatio; + height: SizeRatio; + innerRowCount: number; + innerColumnCount: number; + innerRowIndex: number; + innerColumnIndex: number; + marginLeftPx: Pixels; + marginTopPx: Pixels; + panelInnerWidth: Pixels; + panelInnerHeight: Pixels; +} + +/** @internal */ +export interface ShapeViewModel extends PartitionSmallMultiplesModel { + config: Config; + layers: Layer[]; + quadViewModel: QuadViewModel[]; + rowSets: RowSet[]; + linkLabelViewModels: LinkLabelsViewModelSpec; + outsideLinksViewModel: OutsideLinksViewModel[]; + diskCenter: PointObject; + pickQuads: PickFunction; + outerRadius: number; +} + +const defaultFont: Font = { + fontStyle: 'normal', + fontVariant: 'normal', + fontFamily: '', + fontWeight: 'normal', + textColor: 'black', + textOpacity: 1, +}; + +/** @internal */ +export const nullPartitionSmallMultiplesModel = (partitionLayout: PartitionLayout): PartitionSmallMultiplesModel => ({ + index: 0, + innerIndex: 0, + smAccessorValue: '', + panelTitle: '', + top: 0, + left: 0, + width: 0, + height: 0, + innerRowCount: 0, + innerColumnCount: 0, + innerRowIndex: 0, + innerColumnIndex: 0, + marginLeftPx: 0, + marginTopPx: 0, + panelInnerWidth: 0, + panelInnerHeight: 0, + partitionLayout, +}); + +/** @internal */ +export const nullShapeViewModel = (specifiedConfig?: Config, diskCenter?: PointObject): ShapeViewModel => ({ + ...nullPartitionSmallMultiplesModel((specifiedConfig || config).partitionLayout), + config: specifiedConfig || config, + layers: [], + quadViewModel: [], + rowSets: [], + linkLabelViewModels: { + linkLabels: [], + labelFontSpec: defaultFont, + valueFontSpec: defaultFont, + strokeColor: '', + }, + outsideLinksViewModel: [], + diskCenter: diskCenter || { x: 0, y: 0 }, + pickQuads: () => [], + outerRadius: 0, +}); + +/** @public */ +export type TreeLevel = number; + +/** @public */ +export interface AngleFromTo { + x0: Radian; + x1: Radian; +} + +/** @internal */ +export interface LayerFromTo { + y0: TreeLevel; + y1: TreeLevel; +} + +/** + * @public + */ +export interface TreeNode extends AngleFromTo { + x0: Radian; + x1: Radian; + y0: TreeLevel; + y1: TreeLevel; + fill?: Color; +} + +/** + * @public + */ +export interface SectorGeomSpecY { + y0px: Distance; + y1px: Distance; +} + +/** @public */ +export type DataName = CategoryKey; // todo consider narrowing it to eg. primitives + +/** @public */ +export interface ShapeTreeNode extends TreeNode, SectorGeomSpecY { + yMidPx: Distance; + depth: number; + sortIndex: number; + path: LegendPath; + dataName: DataName; + value: number; + [MODEL_KEY]: ArrayNode; +} + +/** @public */ +export type RawTextGetter = (node: ShapeTreeNode) => string; +/** @public */ +export type ValueGetterFunction = (node: ShapeTreeNode) => number; +/** @public */ +export type ValueGetter = ValueGetterFunction | ValueGetterName; +/** @public */ +export type NodeColorAccessor = (d: ShapeTreeNode, index: number, array: HierarchyOfArrays) => string; diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/utils/circline_geometry.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/utils/circline_geometry.ts new file mode 100644 index 000000000000..07596143b2c4 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/utils/circline_geometry.ts @@ -0,0 +1,240 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TAU } from '../../../../common/constants'; +import { + Circline, + CirclineArc, + CirclinePredicate, + Coordinate, + Distance, + PointObject, + Radian, + Radius, + RingSectorConstruction, + trueBearingToStandardPositionAngle, +} from '../../../../common/geometry'; +import { Config } from '../types/config_types'; +import { AngleFromTo, LayerFromTo, ShapeTreeNode } from '../types/viewmodel_types'; + +function euclideanDistance({ x: x1, y: y1 }: PointObject, { x: x2, y: y2 }: PointObject): Distance { + return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)); +} + +function fullyContained(c1: Circline, c2: Circline): boolean { + return euclideanDistance(c1, c2) + c2.r <= c1.r; +} + +function noOverlap(c1: Circline, c2: Circline): boolean { + return euclideanDistance(c1, c2) >= c1.r + c2.r; +} + +function circlineIntersect(c1: Circline, c2: Circline): PointObject[] { + const d = Math.sqrt((c1.x - c2.x) * (c1.x - c2.x) + (c1.y - c2.y) * (c1.y - c2.y)); + if (c1.r + c2.r >= d && d >= Math.abs(c1.r - c2.r)) { + const a1 = d + c1.r + c2.r; + const a2 = d + c1.r - c2.r; + const a3 = d - c1.r + c2.r; + const a4 = -d + c1.r + c2.r; + const area = Math.sqrt(a1 * a2 * a3 * a4) / 4; + + const xAux1 = (c1.x + c2.x) / 2 + ((c2.x - c1.x) * (c1.r * c1.r - c2.r * c2.r)) / (2 * d * d); + const xAux2 = (2 * (c1.y - c2.y) * area) / (d * d); + const x1 = xAux1 + xAux2; + const x2 = xAux1 - xAux2; + + const yAux1 = (c1.y + c2.y) / 2 + ((c2.y - c1.y) * (c1.r * c1.r - c2.r * c2.r)) / (2 * d * d); + const yAux2 = (2 * (c1.x - c2.x) * area) / (d * d); + const y1 = yAux1 - yAux2; + const y2 = yAux1 + yAux2; + + return [ + { x: x1, y: y1 }, + { x: x2, y: y2 }, + ]; + } + return []; +} + +function circlineValidSectors(refC: CirclinePredicate, c: CirclineArc): CirclineArc[] { + const { inside } = refC; + const { x, y, r, from, to } = c; + const fullContainment = fullyContained(refC, c); + const fullyOutside = noOverlap(refC, c) || fullyContained(c, refC); + + // handle clear cases + + // nothing kept: + if ((inside && fullContainment) || (!inside && fullyOutside)) { + return []; + } + + // the entire sector is kept + if ((inside && fullyOutside) || (!inside && fullContainment)) { + return [c]; + } + + // now we know there's intersection and we're supposed to get back two distinct points + const circlineIntersections = circlineIntersect(refC, c); + // These conditions don't happen; kept for documentation purposes: + // if (circlineIntersections.length !== 2) throw new Error('Problem in intersection calculation.') + // if (from > to) throw new Error('From/to problem in intersection calculation.') + if (circlineIntersections.length !== 2) return []; + const [p1, p2] = circlineIntersections; + const aPre1 = Math.atan2(p1.y - c.y, p1.x - c.x); + const aPre2 = Math.atan2(p2.y - c.y, p2.x - c.x); + const a1p = Math.max(from, Math.min(to, aPre1 < 0 ? aPre1 + TAU : aPre1)); + const a2p = Math.max(from, Math.min(to, aPre2 < 0 ? aPre2 + TAU : aPre2)); + const a1 = Math.min(a1p, a2p); + const a2 = a1p === a2p ? TAU : Math.max(a1p, a2p); // make a2 drop out in next step + + // imperative, slightly optimized buildup of `breakpoints` as it's in the hot loop: + const breakpoints = [from]; + if (from < a1 && a1 < to) breakpoints.push(a1); + if (from < a2 && a2 < to) breakpoints.push(a2); + breakpoints.push(to); + + const predicate = inside ? noOverlap : fullyContained; + + // imperative, slightly optimized buildup of `result` as it's in the hot loop: + const result = []; + for (let i = 0; i < breakpoints.length - 1; i++) { + // eslint-disable-next-line no-shadow + const from = breakpoints[i]; + // eslint-disable-next-line no-shadow + const to = breakpoints[i + 1]; + const midAngle = (from + to) / 2; // no winding clip ie. `meanAngle()` would be wrong here + const xx = x + r * Math.cos(midAngle); + const yy = y + r * Math.sin(midAngle); + if (predicate(refC, { x: xx, y: yy, r: 0 })) result.push({ x, y, r, from, to }); + } + return result; +} + +/** @internal */ +export function conjunctiveConstraint(constraints: RingSectorConstruction, c: CirclineArc): CirclineArc[] { + // imperative, slightly optimized buildup of `valids` as it's in the hot loop: + let valids = [c]; + for (let i = 0; i < constraints.length; i++) { + const refC = constraints[i]; // reference circle + const nextValids: CirclineArc[] = []; + for (let j = 0; j < valids.length; j++) { + const cc = valids[j]; + const currentValids = circlineValidSectors(refC, cc); + nextValids.push(...currentValids); + } + valids = nextValids; + } + return valids; +} + +/** @internal */ +export const INFINITY_RADIUS = 1e4; // far enough for a sub-2px precision on a 4k screen, good enough for text bounds; 64 bit floats still work well with it + +/** @internal */ +export function angleToCircline( + midRadius: Radius, + alpha: Radian, + direction: 1 | -1 /* 1 for clockwise and -1 for anticlockwise circline */, +) { + const sectorRadiusLineX = Math.cos(alpha) * midRadius; + const sectorRadiusLineY = Math.sin(alpha) * midRadius; + const normalAngle = alpha + (direction * Math.PI) / 2; + const x = sectorRadiusLineX + INFINITY_RADIUS * Math.cos(normalAngle); + const y = sectorRadiusLineY + INFINITY_RADIUS * Math.sin(normalAngle); + return { x, y, r: INFINITY_RADIUS, inside: false, from: 0, to: TAU }; +} + +function ringSectorStartAngle(d: AngleFromTo): Radian { + return trueBearingToStandardPositionAngle(d.x0 + Math.max(0, d.x1 - d.x0 - TAU / 2) / 2); +} + +function ringSectorEndAngle(d: AngleFromTo): Radian { + return trueBearingToStandardPositionAngle(d.x1 - Math.max(0, d.x1 - d.x0 - TAU / 2) / 2); +} + +function ringSectorInnerRadius(innerRadius: Radian, ringThickness: Distance) { + return (d: LayerFromTo): Radius => innerRadius + d.y0 * ringThickness; +} + +function ringSectorOuterRadius(innerRadius: Radian, ringThickness: Distance) { + return (d: LayerFromTo): Radius => innerRadius + (d.y0 + 1) * ringThickness; +} + +/** @internal */ +export function ringSectorConstruction(config: Config, innerRadius: Radius, ringThickness: Distance) { + return (ringSector: ShapeTreeNode): RingSectorConstruction => { + const { + circlePadding, + radialPadding, + fillOutside, + radiusOutside, + fillRectangleWidth, + fillRectangleHeight, + } = config; + const radiusGetter = fillOutside ? ringSectorOuterRadius : ringSectorInnerRadius; + const geometricInnerRadius = radiusGetter(innerRadius, ringThickness)(ringSector); + const innerR = geometricInnerRadius + circlePadding * 2; + const outerR = Math.max( + innerR, + ringSectorOuterRadius(innerRadius, ringThickness)(ringSector) - circlePadding + (fillOutside ? radiusOutside : 0), + ); + const startAngle = ringSectorStartAngle(ringSector); + const endAngle = ringSectorEndAngle(ringSector); + const innerCircline = { x: 0, y: 0, r: innerR, inside: true, from: 0, to: TAU }; + const outerCircline = { x: 0, y: 0, r: outerR, inside: false, from: 0, to: TAU }; + const midRadius = (innerR + outerR) / 2; + const sectorStartCircle = angleToCircline(midRadius, startAngle - radialPadding, -1); + const sectorEndCircle = angleToCircline(midRadius, endAngle + radialPadding, 1); + const outerRadiusFromRectangleWidth = fillRectangleWidth / 2; + const outerRadiusFromRectanglHeight = fillRectangleHeight / 2; + const fullCircle = ringSector.x0 === 0 && ringSector.x1 === TAU && geometricInnerRadius === 0; + const sectorCirclines = [ + ...(fullCircle && innerRadius === 0 ? [] : [innerCircline]), + outerCircline, + ...(fullCircle ? [] : [sectorStartCircle, sectorEndCircle]), + ]; + const rectangleCirclines = + outerRadiusFromRectangleWidth === Infinity && outerRadiusFromRectanglHeight === Infinity + ? [] + : [ + { x: INFINITY_RADIUS - outerRadiusFromRectangleWidth, y: 0, r: INFINITY_RADIUS, inside: true }, + { x: -INFINITY_RADIUS + outerRadiusFromRectangleWidth, y: 0, r: INFINITY_RADIUS, inside: true }, + { x: 0, y: INFINITY_RADIUS - outerRadiusFromRectanglHeight, r: INFINITY_RADIUS, inside: true }, + { x: 0, y: -INFINITY_RADIUS + outerRadiusFromRectanglHeight, r: INFINITY_RADIUS, inside: true }, + ]; + return [...sectorCirclines, ...rectangleCirclines]; + }; +} +/** @internal */ +export function makeRowCircline( + cx: Coordinate, + cy: Coordinate, + radialOffset: Distance, + rotation: Radian, + fontSize: number, + offsetSign: -1 | 0 | 1, +) { + const r = INFINITY_RADIUS; + const offset = (offsetSign * fontSize) / 2; + const topRadius = r - offset; + const x = cx + topRadius * Math.cos(-rotation + TAU / 4); + const y = cy + topRadius * Math.cos(-rotation + TAU / 2); + return { r: r + radialOffset, x, y }; +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/utils/group_by_rollup.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/utils/group_by_rollup.ts new file mode 100644 index 000000000000..e87f758e82cf --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/utils/group_by_rollup.ts @@ -0,0 +1,299 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CategoryKey } from '../../../../common/category'; +import { Relation } from '../../../../common/text_utils'; +import { LegendPath } from '../../../../state/actions/legend'; +import { Datum, ValueAccessor } from '../../../../utils/common'; + +/** @public */ +export const AGGREGATE_KEY = 'value'; +/** @public */ +export const STATISTICS_KEY = 'statistics'; +/** @public */ +export const DEPTH_KEY = 'depth'; +/** @public */ +export const CHILDREN_KEY = 'children'; +/** @public */ +export const INPUT_KEY = 'inputIndex'; +/** @public */ +export const PARENT_KEY = 'parent'; +/** @public */ +export const SORT_INDEX_KEY = 'sortIndex'; +/** @public */ +export const PATH_KEY = 'path'; + +/** @public */ +export interface Statistics { + globalAggregate: number; +} + +/** @public */ +export interface NodeDescriptor { + [AGGREGATE_KEY]: number; + [DEPTH_KEY]: number; + [STATISTICS_KEY]: Statistics; + [INPUT_KEY]?: Array; +} + +/** @public */ +export type ArrayEntry = [Key, ArrayNode]; +/** @public */ +export type HierarchyOfArrays = Array; +/** @public */ +export interface ArrayNode extends NodeDescriptor { + [CHILDREN_KEY]: HierarchyOfArrays; + [PARENT_KEY]: ArrayNode; + [SORT_INDEX_KEY]: number; + [PATH_KEY]: LegendPath; +} + +type HierarchyOfMaps = Map; +interface MapNode extends NodeDescriptor { + [CHILDREN_KEY]?: HierarchyOfMaps; + [PARENT_KEY]?: ArrayNode; +} + +/** @internal */ +export const HIERARCHY_ROOT_KEY: Key = '__root_key__'; + +/** @public */ +export type PrimitiveValue = string | number | null; // there could be more but sufficient for now +/** @public */ +export type Key = CategoryKey; +/** @public */ +export type Sorter = (a: number, b: number) => number; + +/** + * Binary predicate function used for `[].sort`ing partitions represented as ArrayEntries + * @public + */ +export type NodeSorter = (a: ArrayEntry, b: ArrayEntry) => number; + +/** @public */ +export const entryKey = ([key]: ArrayEntry) => key; +/** @public */ +export const entryValue = ([, value]: ArrayEntry) => value; +/** @public */ +export function depthAccessor(n: ArrayEntry) { + return entryValue(n)[DEPTH_KEY]; +} +/** @public */ +export function aggregateAccessor(n: ArrayEntry): number { + return entryValue(n)[AGGREGATE_KEY]; +} +/** @public */ +export function parentAccessor(n: ArrayEntry): ArrayNode { + return entryValue(n)[PARENT_KEY]; +} +/** @public */ +export function childrenAccessor(n: ArrayEntry) { + return entryValue(n)[CHILDREN_KEY]; +} +/** @public */ +export function sortIndexAccessor(n: ArrayEntry) { + return entryValue(n)[SORT_INDEX_KEY]; +} +/** @public */ +export function pathAccessor(n: ArrayEntry) { + return entryValue(n)[PATH_KEY]; +} + +/** @public */ +export function getNodeName(node: ArrayNode) { + const index = node[SORT_INDEX_KEY]; + const arrayEntry: ArrayEntry = node[PARENT_KEY][CHILDREN_KEY][index]; + return entryKey(arrayEntry); +} + +/** @internal */ +export function groupByRollup( + keyAccessors: Array<((a: Datum) => Key) | ((a: Datum, i: number) => Key)>, + valueAccessor: ValueAccessor, + { + reducer, + identity, + }: { + reducer: (prev: number, next: number) => number; + identity: () => any; + }, + factTable: Relation, +): HierarchyOfMaps { + const statistics: Statistics = { + globalAggregate: NaN, + }; + const reductionMap: HierarchyOfMaps = factTable.reduce((p: HierarchyOfMaps, n, index) => { + const keyCount = keyAccessors.length; + let pointer: HierarchyOfMaps = p; + keyAccessors.forEach((keyAccessor, i) => { + const key: Key = keyAccessor(n, index); + const last = i === keyCount - 1; + const node = pointer.get(key); + const inputIndices = node?.[INPUT_KEY] ?? []; + const childrenMap = node?.[CHILDREN_KEY] ?? new Map(); + const aggregate = node?.[AGGREGATE_KEY] ?? identity(); + const reductionValue = reducer(aggregate, valueAccessor(n)); + pointer.set(key, { + [AGGREGATE_KEY]: reductionValue, + [STATISTICS_KEY]: statistics, + [INPUT_KEY]: [...inputIndices, index], + [DEPTH_KEY]: i, + ...(!last && { [CHILDREN_KEY]: childrenMap }), + }); + if (childrenMap) { + // will always be true except when exiting from forEach, ie. upon encountering the leaf node + pointer = childrenMap; + } + }); + return p; + }, new Map()); + if (reductionMap.get(HIERARCHY_ROOT_KEY) !== undefined) { + statistics.globalAggregate = (reductionMap.get(HIERARCHY_ROOT_KEY) as MapNode)[AGGREGATE_KEY]; + } + return reductionMap; +} + +function getRootArrayNode(): ArrayNode { + const children: HierarchyOfArrays = []; + const bootstrap: Omit = { + [AGGREGATE_KEY]: NaN, + [DEPTH_KEY]: NaN, + [CHILDREN_KEY]: children, + [INPUT_KEY]: [] as number[], + [PATH_KEY]: [] as LegendPath, + [SORT_INDEX_KEY]: 0, + [STATISTICS_KEY]: { globalAggregate: 0 }, + }; + (bootstrap as ArrayNode)[PARENT_KEY] = bootstrap as ArrayNode; + return bootstrap as ArrayNode; // TS doesn't yet handle bootstrapping but the `Omit` above retains guarantee for all props except `[PARENT_KEY]` +} + +/** @internal */ +export function mapsToArrays(root: HierarchyOfMaps, sortSpecs: (NodeSorter | null)[]): HierarchyOfArrays { + const groupByMap = (node: HierarchyOfMaps, parent: ArrayNode) => { + const items = Array.from( + node, + ([key, value]: [Key, MapNode]): ArrayEntry => { + const valueElement = value[CHILDREN_KEY]; + const resultNode: ArrayNode = { + [AGGREGATE_KEY]: NaN, + [STATISTICS_KEY]: { globalAggregate: NaN }, + [CHILDREN_KEY]: [], + [DEPTH_KEY]: NaN, + [SORT_INDEX_KEY]: NaN, + [PARENT_KEY]: parent, + [INPUT_KEY]: [], + [PATH_KEY]: [], + }; + const newValue: ArrayNode = Object.assign( + resultNode, + value, + valueElement && { [CHILDREN_KEY]: groupByMap(valueElement, resultNode) }, + ); + return [key, newValue]; + }, + ); + if (sortSpecs.some((s) => s !== null)) { + items.sort((e1: ArrayEntry, e2: ArrayEntry) => { + const node1 = e1[1]; + const node2 = e2[1]; + if (node1[DEPTH_KEY] !== node2[DEPTH_KEY]) return node1[DEPTH_KEY] - node2[DEPTH_KEY]; + const depth = node1[DEPTH_KEY]; + const sorterWithinLayer = sortSpecs[depth]; + return sorterWithinLayer ? sorterWithinLayer(e1, e2) : node2.value - node1.value; + }); + } + return items.map((n: ArrayEntry, i) => { + entryValue(n).sortIndex = i; + return n; + }); + }; // with the current algo, decreasing order is important + const tree = groupByMap(root, getRootArrayNode()); + const buildPaths = ([key, mapNode]: ArrayEntry, currentPath: LegendPath) => { + const newPath = [...currentPath, { index: mapNode[SORT_INDEX_KEY], value: key }]; + mapNode[PATH_KEY] = newPath; // in-place mutation, so disabled `no-param-reassign` + mapNode.children.forEach((entry) => buildPaths(entry, newPath)); + }; + buildPaths(tree[0], []); + return tree; +} + +/** @internal */ +export function mapEntryValue(entry: ArrayEntry) { + return entryValue(entry)[AGGREGATE_KEY]; +} + +// type MeanReduction = { sum: number; count: number }; +// type MedianReduction = Array; + +/** @internal */ +export const aggregators = { + one: { + identity: () => 0, + reducer: () => 1, + }, + count: { + identity: () => 0, + reducer: (r: number) => r + 1, + }, + sum: { + identity: () => 0, + reducer: (r: number, n: number) => r + n, + }, + min: { + identity: () => Infinity, + reducer: (r: number, n: number) => Math.min(r, n), + }, + max: { + identity: () => -Infinity, + reducer: (r: number, n: number) => Math.max(r, n), + }, + min0: { + identity: () => 0, + reducer: (r: number, n: number) => Math.min(r, n), + }, + max0: { + identity: () => 0, + reducer: (r: number, n: number) => Math.max(r, n), + }, + // todo more TS typing is needed to use these + // mean: { + // identity: (): MeanReduction => ({ sum: 0, count: 0 }), + // reducer: (r: MeanReduction, n: number) => { + // r.sum += n; + // r.count++; + // return r; + // }, + // finalizer: (r: MeanReduction): number => r.sum / r.count, + // }, + // median: { + // identity: (): MedianReduction => [], + // reducer: (r: MedianReduction, n: number) => { + // r.push(n); + // return r; + // }, + // finalizer: (r: MedianReduction): number => { + // const sorted = r.sort(ascending); + // const len = r.length; + // const even = len === len % 2; + // const half = len / 2; + // return even ? (sorted[half - 1] + sorted[half]) / 2 : sorted[half - 0.5]; + // }, + // }, +}; diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/utils/highlighted_geoms.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/utils/highlighted_geoms.ts new file mode 100644 index 000000000000..f0a852f80290 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/utils/highlighted_geoms.ts @@ -0,0 +1,98 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $Values as Values } from 'utility-types'; + +import { LegendPath } from '../../../../state/actions/legend'; +import { DataName, QuadViewModel } from '../types/viewmodel_types'; + +type LegendStrategyFn = (legendPath: LegendPath) => (partialShape: { path: LegendPath; dataName: DataName }) => boolean; + +const legendStrategies: Record = { + node: (legendPath) => ({ path }) => + // highlight exact match in the path only + legendPath.length === path.length && + legendPath.every(({ index, value }, i) => index === path[i]?.index && value === path[i]?.value), + + path: (legendPath) => ({ path }) => + // highlight members of the exact path; ie. exact match in the path, plus all its ancestors + path.every(({ index, value }, i) => index === legendPath[i]?.index && value === legendPath[i]?.value), + + keyInLayer: (legendPath) => ({ path, dataName }) => + // highlight all identically named items which are within the same depth (ring) as the hovered legend depth + legendPath.length === path.length && dataName === legendPath[legendPath.length - 1].value, + + key: (legendPath) => ({ dataName }) => + // highlight all identically named items, no matter where they are + dataName === legendPath[legendPath.length - 1].value, + + nodeWithDescendants: (legendPath) => ({ path }) => + // highlight exact match in the path, and everything that is its descendant in that branch + legendPath.every(({ index, value }, i) => index === path[i]?.index && value === path[i]?.value), + + pathWithDescendants: (legendPath) => ({ path }) => + // highlight exact match in the path, and everything that is its ancestor, or its descendant in that branch + legendPath + .slice(0, path.length) + .every(({ index, value }, i) => index === path[i]?.index && value === path[i]?.value), +}; + +/** @public */ +export const LegendStrategy = Object.freeze({ + /** + * Highlight the specific node(s) that the legend item stands for. + */ + Node: 'node' as const, + /** + * Highlight members of the exact path; ie. like `Node`, plus all its ancestors + */ + Path: 'path' as const, + /** + * Highlight all identically named (labelled) items within the tree layer (depth or ring) of the specific node(s) that the legend item stands for + */ + KeyInLayer: 'keyInLayer' as const, + /** + * Highlight all identically named (labelled) items, no matter where they are + */ + Key: 'key' as const, + /** + * Highlight the specific node(s) that the legend item stands for, plus all descendants + */ + NodeWithDescendants: 'nodeWithDescendants' as const, + /** + * Highlight the specific node(s) that the legend item stands for, plus all ancestors and descendants + */ + PathWithDescendants: 'pathWithDescendants' as const, +}); + +/** @public */ +export type LegendStrategy = Values; + +const defaultStrategy: LegendStrategy = LegendStrategy.Key; + +/** @internal */ +export function highlightedGeoms( + legendStrategy: LegendStrategy | undefined, + flatLegend: boolean | undefined, + quadViewModel: QuadViewModel[], + highlightedLegendItemPath: LegendPath, +) { + const pickedLogic: LegendStrategy = flatLegend ? LegendStrategy.Key : legendStrategy ?? defaultStrategy; + return quadViewModel.filter(legendStrategies[pickedLogic](highlightedLegendItemPath)); +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/utils/legend.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/utils/legend.ts new file mode 100644 index 000000000000..8d51e379c463 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/utils/legend.ts @@ -0,0 +1,99 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CategoryKey } from '../../../../common/category'; +import { map } from '../../../../common/iterables'; +import { LegendItem } from '../../../../common/legend'; +import { LegendPositionConfig } from '../../../../specs/settings'; +import { isHierarchicalLegend } from '../../../../utils/legend'; +import { Layer } from '../../specs'; +import { QuadViewModel } from '../types/viewmodel_types'; + +function makeKey(...keyParts: CategoryKey[]): string { + return keyParts.join('---'); +} + +function compareTreePaths( + { index: oiA, innerIndex: iiA, path: a }: QuadViewModel, + { index: oiB, innerIndex: iiB, path: b }: QuadViewModel, +): number { + if (oiA !== oiB) return oiA - oiB; + if (iiA !== iiB) return iiA - iiB; + for (let i = 0; i < Math.min(a.length, b.length); i++) { + const diff = a[i].index - b[i].index; + if (diff) { + return diff; + } + } + return a.length - b.length; // if one path is fully contained in the other, then parent (shorter) goes first +} + +/** @internal */ +export function getLegendItems( + id: string, + layers: Layer[], + flatLegend: boolean | undefined, + legendMaxDepth: number, + legendPosition: LegendPositionConfig, + quadViewModel: QuadViewModel[], +): LegendItem[] { + const uniqueNames = new Set(map(({ dataName, fillColor }) => makeKey(dataName, fillColor), quadViewModel)); + const useHierarchicalLegend = isHierarchicalLegend(flatLegend, legendPosition); + + const formattedLabel = ({ dataName, depth }: QuadViewModel) => { + const formatter = layers[depth - 1]?.nodeLabel; + return formatter ? formatter(dataName) : dataName; + }; + + function compareNames(aItem: QuadViewModel, bItem: QuadViewModel): number { + const a = formattedLabel(aItem); + const b = formattedLabel(bItem); + return a < b ? -1 : a > b ? 1 : 0; + } + + const excluded: Set = new Set(); + const items = quadViewModel.filter(({ depth, dataName, fillColor }) => { + if (legendMaxDepth !== null && depth > legendMaxDepth) { + return false; + } + if (!useHierarchicalLegend) { + const key = makeKey(dataName, fillColor); + if (uniqueNames.has(key) && excluded.has(key)) { + return false; + } + excluded.add(key); + } + return true; + }); + + items.sort(flatLegend ? compareNames : compareTreePaths); + + return items.map((item) => { + const { dataName, fillColor, depth, path } = item; + return { + color: fillColor, + label: formattedLabel(item), + childId: dataName, + depth: useHierarchicalLegend ? depth - 1 : 0, + path, + seriesIdentifiers: [{ key: dataName, specId: id }], + keys: [], + }; + }); +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/utils/legend_labels.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/utils/legend_labels.ts new file mode 100644 index 000000000000..b7d85ffed92f --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/utils/legend_labels.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegendItemLabel } from '../../../../state/selectors/get_legend_items_labels'; +import { Layer } from '../../specs'; +import { CHILDREN_KEY, HIERARCHY_ROOT_KEY, HierarchyOfArrays } from './group_by_rollup'; + +/** @internal */ +export function getLegendLabels(layers: Layer[], tree: HierarchyOfArrays, legendMaxDepth: number) { + return flatSlicesNames(layers, 0, tree).filter(({ depth }) => depth <= legendMaxDepth); +} + +function flatSlicesNames( + layers: Layer[], + depth: number, + tree: HierarchyOfArrays, + keys: Map = new Map(), +): LegendItemLabel[] { + if (tree.length === 0) { + return []; + } + + for (let i = 0; i < tree.length; i++) { + const branch = tree[i]; + const arrayNode = branch[1]; + const key = branch[0]; + + // format the key with the layer formatter + const layer = layers[depth - 1]; + const formatter = layer?.nodeLabel; + let formattedValue = ''; + if (key != null) { + formattedValue = formatter ? formatter(key) : `${key}`; + } + // preventing errors from external formatters + if (formattedValue != null && formattedValue !== '' && formattedValue !== HIERARCHY_ROOT_KEY) { + // save only the max depth, so we can compute the the max extension of the legend + keys.set(formattedValue, Math.max(depth, keys.get(formattedValue) ?? 0)); + } + + const children = arrayNode[CHILDREN_KEY]; + flatSlicesNames(layers, depth + 1, children, keys); + } + return [...keys.keys()].map((k) => ({ + label: k, + depth: keys.get(k) ?? 0, + })); +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/utils/sunburst.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/utils/sunburst.ts new file mode 100644 index 000000000000..cdff11cec359 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/utils/sunburst.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Origin, Part } from '../../../../common/text_utils'; +import { ArrayEntry, childrenAccessor, HierarchyOfArrays } from './group_by_rollup'; + +/** @internal */ +export function sunburst( + outerNodes: HierarchyOfArrays, + areaAccessor: (e: ArrayEntry) => number, + { x0: outerX0, y0: outerY0 }: Origin, + clockwiseSectors: boolean, + specialFirstInnermostSector: boolean, + heightStep: number = 1, +): Array { + const result: Array = []; + const laySubtree = (nodes: HierarchyOfArrays, { x0, y0 }: Origin, depth: number) => { + let currentOffsetX = x0; + const nodeCount = nodes.length; + for (let i = 0; i < nodeCount; i++) { + const index = clockwiseSectors ? i : nodeCount - i - 1; + const node = nodes[depth === 1 && specialFirstInnermostSector ? (index + 1) % nodeCount : index]; + const area = areaAccessor(node); + result.push({ node, x0: currentOffsetX, y0, x1: currentOffsetX + area, y1: y0 + heightStep }); + const children = childrenAccessor(node); + if (children.length > 0) { + laySubtree(children, { x0: currentOffsetX, y0: y0 + heightStep }, depth + 1); + } + currentOffsetX += area; + } + }; + laySubtree(outerNodes, { x0: outerX0, y0: outerY0 }, 0); + return result; +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/utils/treemap.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/utils/treemap.ts new file mode 100644 index 000000000000..cb1d45b2d794 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/utils/treemap.ts @@ -0,0 +1,188 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $Values as Values } from 'utility-types'; + +import { GOLDEN_RATIO } from '../../../../common/constants'; +import { Pixels } from '../../../../common/geometry'; +import { Part } from '../../../../common/text_utils'; +import { ArrayEntry, CHILDREN_KEY, DEPTH_KEY, entryValue, HierarchyOfArrays } from './group_by_rollup'; + +const MAX_U_PADDING_RATIO = 0.0256197; // this limits area distortion to <10% (which occurs due to pixel padding) with very small rectangles +const MAX_TOP_PADDING_RATIO = 0.33; // this limits further area distortion to ~33% + +interface LayoutElement { + nodes: HierarchyOfArrays; + dependentSize: number; + sectionSizes: number[]; + sectionOffsets: number[]; +} + +function layVector( + nodes: HierarchyOfArrays, + independentSize: number, + areaAccessor: (e: ArrayEntry) => number, +): LayoutElement { + const area = nodes.reduce((p, n) => p + areaAccessor(n), 0); + const dependentSize = area / independentSize; // here we lose a bit of accuracy + let currentOffset = 0; + const sectionOffsets = [currentOffset]; + const sectionSizes = nodes.map((e, i) => { + const sectionSize = areaAccessor(e) / dependentSize; // here we gain back a bit of accuracy + if (i < nodes.length - 1) sectionOffsets.push((currentOffset += sectionSize)); + return sectionSize; + }); + + return { nodes, dependentSize, sectionSizes, sectionOffsets }; // descriptor for a vector (column or row) of elements (nodes) +} + +/** @internal */ +export function leastSquarishAspectRatio({ sectionSizes, dependentSize }: LayoutElement) { + return sectionSizes.reduce((p, n) => Math.min(p, n / dependentSize, dependentSize / n), 1); +} + +const NullLayoutElement: LayoutElement = { + nodes: [], + dependentSize: NaN, + sectionSizes: [], + sectionOffsets: [], +}; + +/** + * Specifies whether partitions are laid out horizontally, vertically or treemap-like tiling for preferably squarish aspect ratios + * @public + */ +export const LayerLayout = Object.freeze({ + horizontal: 'horizontal' as const, + vertical: 'vertical' as const, + squarifying: 'squarifying' as const, +}); + +/** + * Specifies whether partitions are laid out horizontally, vertically or treemap-like tiling for preferably squarish aspect ratios + * @public + */ +export type LayerLayout = Values; // could use ValuesType + +function bestVector( + nodes: HierarchyOfArrays, + height: number, + areaAccessor: (e: ArrayEntry) => number, + layout: LayerLayout, +): LayoutElement { + let previousWorstAspectRatio = -1; + let currentWorstAspectRatio = 0; + + let previousVectorLayout: LayoutElement = NullLayoutElement; + let currentVectorLayout: LayoutElement = NullLayoutElement; + let currentCount = 1; + + do { + previousVectorLayout = currentVectorLayout; + previousWorstAspectRatio = currentWorstAspectRatio; + currentVectorLayout = layVector(nodes.slice(0, currentCount), height, areaAccessor); + currentWorstAspectRatio = leastSquarishAspectRatio(currentVectorLayout); + } while (currentCount++ < nodes.length && (layout || currentWorstAspectRatio > previousWorstAspectRatio)); + + return layout || currentWorstAspectRatio >= previousWorstAspectRatio ? currentVectorLayout : previousVectorLayout; +} + +function vectorNodeCoordinates(vectorLayout: LayoutElement, x0Base: number, y0Base: number, vertical: boolean) { + const { nodes, dependentSize, sectionSizes, sectionOffsets } = vectorLayout; + return nodes.map((e: ArrayEntry, i: number) => { + const x0 = vertical ? x0Base + sectionOffsets[i] : x0Base; + const y0 = vertical ? y0Base : y0Base + sectionOffsets[i]; + const x1 = vertical ? x0 + sectionSizes[i] : x0 + dependentSize; + const y1 = vertical ? y0 + dependentSize : y0 + sectionSizes[i]; + return { node: e, x0, y0, x1, y1 }; + }); +} + +/** @internal */ +export const getTopPadding = (requestedTopPadding: number, fullHeight: Pixels) => + Math.min(requestedTopPadding, fullHeight * MAX_TOP_PADDING_RATIO); + +/** @internal */ +export function treemap( + nodes: HierarchyOfArrays, + areaAccessor: (e: ArrayEntry) => number, + topPaddingAccessor: (e: ArrayEntry) => number, + paddingAccessor: (e: ArrayEntry) => number, + { + x0: outerX0, + y0: outerY0, + width: outerWidth, + height: outerHeight, + }: { x0: number; y0: number; width: number; height: number }, + layouts: LayerLayout[], +): Array { + if (nodes.length === 0) return []; + // some bias toward horizontal rectangles with a golden ratio of width to height + const depth = nodes[0][1][DEPTH_KEY] - 1; + const layerLayout = layouts[depth] ?? null; + const vertical = layerLayout === LayerLayout.vertical || (!layerLayout && outerWidth / GOLDEN_RATIO <= outerHeight); + const independentSize = vertical ? outerWidth : outerHeight; + const vectorElements = bestVector(nodes, independentSize, areaAccessor, layerLayout); + const vector = vectorNodeCoordinates(vectorElements, outerX0, outerY0, vertical); + const { dependentSize } = vectorElements; + return vector + .concat( + ...vector.map(({ node, x0, y0, x1, y1 }) => { + const childrenNodes = entryValue(node)[CHILDREN_KEY]; + if (childrenNodes.length === 0) { + return []; + } + const fullWidth = x1 - x0; + const fullHeight = y1 - y0; + const uPadding = Math.min( + paddingAccessor(node), + fullWidth * MAX_U_PADDING_RATIO * 2, + fullHeight * MAX_U_PADDING_RATIO * 2, + ); + const topPadding = getTopPadding(topPaddingAccessor(node), fullHeight); + const width = fullWidth - 2 * uPadding; + const height = fullHeight - uPadding - topPadding; + return treemap( + childrenNodes, + (d) => ((width * height) / (fullWidth * fullHeight)) * areaAccessor(d), + topPaddingAccessor, + paddingAccessor, + { + x0: x0 + uPadding, + y0: y0 + topPadding, + width, + height, + }, + layouts, + ); + }), + ) + .concat( + treemap( + nodes.slice(vector.length), + areaAccessor, + topPaddingAccessor, + paddingAccessor, + vertical + ? { x0: outerX0, y0: outerY0 + dependentSize, width: outerWidth, height: outerHeight - dependentSize } + : { x0: outerX0 + dependentSize, y0: outerY0, width: outerWidth - dependentSize, height: outerHeight }, + layouts, + ), + ); +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.test.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.test.ts new file mode 100644 index 000000000000..0c87d51ca20f --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.test.ts @@ -0,0 +1,300 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { fillTextColor } from '../../../../common/fill_text_color'; +import { getRectangleRowGeometry } from './fill_text_layout'; + +describe('Test that getRectangleRowGeometry works with:', () => { + const container = { x0: 0, y0: 0, x1: 200, y1: 100 }; + const cx = 0; + const cy = 0; + const totalRowCount = 1; + const linePitch = 50; + const rowIndex = 0; + const fontSize = 50; + const rotation = 0; + const verticalAlignment = 'top'; + + const defaultPadding = 2; + const overhangOffset = -4.5; + + test('scalar, zero padding', () => { + const padding = 0; + const result = getRectangleRowGeometry( + container, + cx, + cy, + totalRowCount, + linePitch, + rowIndex, + fontSize, + rotation, + verticalAlignment, + padding, + ); + // full container width is available; small Y offset for overhang + expect(result).toEqual({ + maximumRowLength: 200, + rowAnchorX: 0, + rowAnchorY: overhangOffset, + }); + }); + + test('scalar, nonzero padding', () => { + const padding = 10; + const result = getRectangleRowGeometry( + container, + cx, + cy, + totalRowCount, + linePitch, + rowIndex, + fontSize, + rotation, + verticalAlignment, + padding, + ); + // full container width is available; small Y offset for overhang + expect(result).toEqual({ + maximumRowLength: 200 - padding * 2, + rowAnchorX: 0, + rowAnchorY: overhangOffset - padding, + }); + }); + + test('per-side, fully specified padding', () => { + const padding = { top: 5, bottom: 10, left: 20, right: 30 }; + const result = getRectangleRowGeometry( + container, + cx, + cy, + totalRowCount, + linePitch, + rowIndex, + fontSize, + rotation, + verticalAlignment, + padding, + ); + // full container width is available; small Y offset for overhang + expect(result).toEqual({ + maximumRowLength: 200 - padding.left - padding.right, + rowAnchorX: -5, // (left - right) / 2 + rowAnchorY: overhangOffset - padding.top, + }); + }); + + test('per-side, partially specified padding', () => { + const padding = { bottom: 10, right: 30 }; + const result = getRectangleRowGeometry( + container, + cx, + cy, + totalRowCount, + linePitch, + rowIndex, + fontSize, + rotation, + verticalAlignment, + padding, + ); + // full container width is available; small Y offset for overhang + expect(result).toEqual({ + maximumRowLength: 200 - defaultPadding - padding.right, + rowAnchorX: -(30 /* right padding */ / 2 - 2 /* 2: default left padding */ / 2), + rowAnchorY: overhangOffset - defaultPadding, + }); + }); + + test('not enough height with per-side, partially specified padding', () => { + const padding = { top: 80, bottom: 80 }; + const result = getRectangleRowGeometry( + container, + cx, + cy, + totalRowCount, + linePitch, + rowIndex, + fontSize, + rotation, + verticalAlignment, + padding, + ); + // full container width is available; small Y offset for overhang + expect(result).toEqual({ + maximumRowLength: 0, // Height of 100 - 2 * 80 < 50 + rowAnchorX: NaN, // if text can't be placed, what is its anchor? + rowAnchorY: NaN, // if text can't be placed, what is its anchor? + }); + }); + + test('not enough height with per-side, asymmetric padding', () => { + const padding = { top: 10, bottom: 50 }; + const result = getRectangleRowGeometry( + container, + cx, + cy, + totalRowCount, + linePitch, + rowIndex, + fontSize, + rotation, + verticalAlignment, + padding, + ); + // full container width is available; small Y offset for overhang + expect(result).toEqual({ + maximumRowLength: 0, + rowAnchorX: NaN, // if text can't be placed, what is its anchor? + rowAnchorY: NaN, // if text can't be placed, what is its anchor? + }); + }); + + test('just enough height to fit row with per-side, asymmetric padding', () => { + const padding = { top: 10, bottom: 30 }; + const result = getRectangleRowGeometry( + container, + cx, + cy, + totalRowCount, + linePitch, + rowIndex, + fontSize, + rotation, + verticalAlignment, + padding, + ); + // full container width is available; small Y offset for overhang + expect(result).toEqual({ + maximumRowLength: 200 - 2 * defaultPadding, // Height of 100 - 2 * 80 < 50 + rowAnchorX: 0, + rowAnchorY: overhangOffset - padding.top, + }); + }); + + test('two half-height rows also fit into the same area', () => { + const padding = { top: 10, bottom: 30 }; + const smallFontSize = 25; + const smallLinePitch = 25; + const totalRowCount2 = 2; + const rowIndex = 0; + const smallOverhangOffset = -2.8125; + const result = getRectangleRowGeometry( + container, + cx, + cy, + totalRowCount2, + smallLinePitch, + rowIndex, + smallFontSize, + rotation, + verticalAlignment, + padding, + ); + // full container width is available; small Y offset for overhang + expect(result).toEqual({ + maximumRowLength: 200 - 2 * defaultPadding, // Height of 100 - 2 * 80 < 50 + rowAnchorX: 0, + rowAnchorY: smallOverhangOffset - padding.top, + }); + }); + + test('two half-height rows do not fit into the a slightly less high area', () => { + const padding = { top: 10, bottom: 45 }; + const smallFontSize = 25; + const smallLinePitch = 25; + const totalRowCount2 = 2; + const rowIndex = 0; + const result = getRectangleRowGeometry( + container, + cx, + cy, + totalRowCount2, + smallLinePitch, + rowIndex, + smallFontSize, + rotation, + verticalAlignment, + padding, + ); + // full container width is available; small Y offset for overhang + expect(result).toEqual({ + maximumRowLength: 0, // Height of 100 - (10 + smallOverhangOffset) - 45 < totalRowCount2 * smallLinePitch + rowAnchorX: NaN, + rowAnchorY: NaN, + }); + }); + + test('paddingBottom correctly moves the row anchor with bottom alignment', () => { + const padding = { top: 0, right: 0, bottom: 20, left: 0 }; + const smallFontSize = 25; + const smallLinePitch = 25; + const totalRowCount2 = 2; + const rowIndex = 0; + const result = getRectangleRowGeometry( + container, + cx, + cy, + totalRowCount2, + smallLinePitch, + rowIndex, + smallFontSize, + rotation, + 'bottom', + padding, + ); + // full container width is available; small Y offset for overhang + expect(result).toEqual({ + maximumRowLength: 200, + rowAnchorX: 0, + rowAnchorY: -( + ( + 100 - + smallLinePitch * (totalRowCount2 - 1 - rowIndex) - + padding.bottom - + smallFontSize * 0.05 + ) /* 0.05 = 5%: default overhang multiplier */ + ), + }); + }); +}); +describe('Test getTextColor function', () => { + test('getTextColor works with textContrast greater than default ratio', () => { + const textColor = 'black'; + const textInvertible = true; + const textContrast = 6; + const fillColor = 'rgba(55, 126, 184, 0.7)'; + const containerBackgroundColor = 'white'; + const expectedAdjustedTextColor = 'black'; + expect(fillTextColor(textColor, textInvertible, textContrast, fillColor, containerBackgroundColor)).toEqual( + expectedAdjustedTextColor, + ); + }); + test('getTextColor works with textContrast not defined', () => { + const textColor = 'black'; + const textInvertible = true; + const textContrast = false; + const fillColor = 'rgba(55, 126, 184, 0.7)'; + const containerBackgroundColor = 'white'; + const expectedAdjustedTextColor = 'black'; + expect(fillTextColor(textColor, textInvertible, textContrast, fillColor, containerBackgroundColor)).toEqual( + expectedAdjustedTextColor, + ); + }); +}); diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.ts new file mode 100644 index 000000000000..b943550d145a --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.ts @@ -0,0 +1,543 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TAU } from '../../../../common/constants'; +import { + Coordinate, + Distance, + Pixels, + PointTuple, + Radian, + Ratio, + RingSectorConstruction, + trueBearingToStandardPositionAngle, + wrapToTau, +} from '../../../../common/geometry'; +import { logarithm } from '../../../../common/math'; +import { Box, Font, PartialFont, TextMeasure, VerticalAlignments } from '../../../../common/text_utils'; +import { integerSnap, monotonicHillClimb } from '../../../../solvers/monotonic_hill_climb'; +import { ValueFormatter } from '../../../../utils/common'; +import { Layer } from '../../specs'; +import { Config, Padding } from '../types/config_types'; +import { + QuadViewModel, + RawTextGetter, + RowBox, + RowSet, + RowSpace, + ShapeTreeNode, + ValueGetterFunction, +} from '../types/viewmodel_types'; +import { conjunctiveConstraint, INFINITY_RADIUS, makeRowCircline } from '../utils/circline_geometry'; +import { RectangleConstruction } from './viewmodel'; + +/** + * todo pick a better unique key for the slices (D3 doesn't keep track of an index) + * @internal + */ +export function nodeId(node: ShapeTreeNode): string { + return `${node.x0}|${node.y0}`; +} + +/** @internal */ +export const getSectorRowGeometry: GetShapeRowGeometry = ( + ringSector, + cx, + cy, + totalRowCount, + linePitch, + rowIndex, + fontSize, + rotation, +) => { + const offset = (totalRowCount / 2) * fontSize + fontSize / 2 - linePitch * rowIndex; + + const topCircline = makeRowCircline(cx, cy, offset, rotation, fontSize, 1); + const bottomCircline = makeRowCircline(cx, cy, offset, rotation, fontSize, -1); + const midCircline = makeRowCircline(cx, cy, offset, rotation, 0, 0); + + const valid1 = conjunctiveConstraint(ringSector, { ...topCircline, from: 0, to: TAU })[0]; + if (!valid1) return { rowAnchorX: cx, rowAnchorY: cy, maximumRowLength: 0 }; + const valid2 = conjunctiveConstraint(ringSector, { ...bottomCircline, from: 0, to: TAU })[0]; + if (!valid2) return { rowAnchorX: cx, rowAnchorY: cy, maximumRowLength: 0 }; + const from = Math.max(valid1.from, valid2.from); + const to = Math.min(valid1.to, valid2.to); + const midAngle = (from + to) / 2; + const cheapTangent = Math.max(0, to - from); /* Math.tan(Math.max(0, to - from)) */ // https://en.wikipedia.org/wiki/Small-angle_approximation + const rowAnchorX = midCircline.r * Math.cos(midAngle) + midCircline.x; + const rowAnchorY = midCircline.r * Math.sin(midAngle) + midCircline.y; + const maximumRowLength = cheapTangent * INFINITY_RADIUS; + return { rowAnchorX, rowAnchorY, maximumRowLength }; +}; + +function getVerticalAlignment( + container: RectangleConstruction, + verticalAlignment: VerticalAlignments, + linePitch: Pixels, + totalRowCount: number, + rowIndex: number, + paddingTop: Pixels, + paddingBottom: Pixels, + fontSize: Pixels, + overhang: Ratio, +) { + switch (verticalAlignment) { + case VerticalAlignments.top: + return -(container.y0 + linePitch * rowIndex + paddingTop + fontSize * overhang); + case VerticalAlignments.bottom: + return -(container.y1 - linePitch * (totalRowCount - 1 - rowIndex) - paddingBottom - fontSize * overhang); + default: + return -((container.y0 + container.y1) / 2 + (linePitch * (rowIndex + 1 - totalRowCount)) / 2); + } +} + +/** @internal */ +export const getRectangleRowGeometry: GetShapeRowGeometry = ( + container, + cx, + cy, + totalRowCount, + linePitch, + rowIndex, + fontSize, + _rotation, + verticalAlignment, + padding, +) => { + const defaultPad: Pixels = 2; + const { top, right, bottom, left } = + typeof padding === 'number' + ? { top: padding, right: padding, bottom: padding, left: padding } + : { + ...{ top: defaultPad, right: defaultPad, bottom: defaultPad, left: defaultPad }, + ...padding, + }; + + const overhang = 0.05; + const topPaddingAdjustment = fontSize < 6 ? 0 : Math.max(1, Math.min(2, fontSize / 16)); + const adjustedTop = top + topPaddingAdjustment; // taper out paddingTop with small fonts + if ((container.y1 - container.y0 - adjustedTop - bottom) / totalRowCount < linePitch) { + return { + rowAnchorX: NaN, + rowAnchorY: NaN, + maximumRowLength: 0, + }; + } + + const rowAnchorY = getVerticalAlignment( + container, + verticalAlignment, + linePitch, + totalRowCount, + rowIndex, + adjustedTop, + bottom, + fontSize, + overhang, + ); + return { + rowAnchorX: cx + left / 2 - right / 2, + rowAnchorY, + maximumRowLength: container.x1 - container.x0 - left - right, + }; +}; + +function rowSetComplete(rowSet: RowSet, measuredBoxes: RowBox[]) { + return ( + measuredBoxes.length === 0 && + !rowSet.rows.some( + (r) => isNaN(r.length) || r.rowWords.length === 0 || r.rowWords.every((rw) => rw.text.length === 0), + ) + ); +} + +function identityRowSet(): RowSet { + return { + id: '', + rows: [], + fontSize: NaN, + fillTextColor: '', + rotation: NaN, + verticalAlignment: VerticalAlignments.middle, + leftAlign: false, + }; +} + +function getAllBoxes( + rawTextGetter: RawTextGetter, + valueGetter: ValueGetterFunction, + valueFormatter: ValueFormatter, + sizeInvariantFontShorthand: Font, + valueFont: PartialFont, + node: ShapeTreeNode, +): Box[] { + return rawTextGetter(node) + .split(' ') + .filter(Boolean) + .map((text) => ({ text, ...sizeInvariantFontShorthand })) + .concat( + valueFormatter(valueGetter(node)) + .split(' ') + .filter(Boolean) + .map((text) => ({ text, ...sizeInvariantFontShorthand, ...valueFont })), + ); +} + +function getWordSpacing(fontSize: number) { + return fontSize / 4; +} + +type GetShapeRowGeometry = ( + container: C, + cx: Distance, + cy: Distance, + targetRowCount: number, + linePitch: Pixels, + currentRowIndex: number, + fontSize: Pixels, + rotation: Radian, + verticalAlignment: VerticalAlignments, + padding: Padding, +) => RowSpace; + +type ShapeConstructor = (n: ShapeTreeNode) => C; + +type NodeWithOrigin = { node: QuadViewModel; origin: PointTuple }; + +function fill( + shapeConstructor: ShapeConstructor, + getShapeRowGeometry: GetShapeRowGeometry, + getRotation: GetRotation, +) { + return function fillClosure( + config: Config, + layers: Layer[], + measure: TextMeasure, + rawTextGetter: RawTextGetter, + valueGetter: ValueGetterFunction, + formatter: ValueFormatter, + leftAlign: boolean, + middleAlign: boolean, + ) { + const { maxRowCount, fillLabel } = config; + return (allFontSizes: Pixels[][], textFillOrigin: PointTuple, node: QuadViewModel): RowSet => { + const container = shapeConstructor(node); + const rotation = getRotation(node); + + const layer = layers[node.depth - 1] || {}; + const verticalAlignment = middleAlign + ? VerticalAlignments.middle + : node.depth < layers.length + ? VerticalAlignments.bottom + : VerticalAlignments.top; + const fontSizes = allFontSizes[Math.min(node.depth, allFontSizes.length) - 1]; + const { fontStyle, fontVariant, fontFamily, fontWeight, valueFormatter, padding, textOpacity, clipText } = { + ...fillLabel, + valueFormatter: formatter, + ...layer.fillLabel, + ...layer.shape, + }; + + const valueFont = { + ...fillLabel, + ...fillLabel.valueFont, + ...layer.fillLabel, + ...layer.fillLabel?.valueFont, + }; + + const sizeInvariantFont: Font = { + fontStyle, + fontVariant, + fontWeight, + fontFamily, + textColor: node.textColor, + textOpacity, + }; + const allBoxes = getAllBoxes(rawTextGetter, valueGetter, valueFormatter, sizeInvariantFont, valueFont, node); + const [cx, cy] = textFillOrigin; + + return { + ...getRowSet( + allBoxes, + maxRowCount, + fontSizes, + measure, + rotation, + verticalAlignment, + leftAlign, + container, + getShapeRowGeometry, + cx, + cy, + padding, + node, + clipText, + ), + fillTextColor: node.textColor, + }; + }; + }; +} + +function tryFontSize( + measure: TextMeasure, + rotation: Radian, + verticalAlignment: VerticalAlignments, + leftAlign: boolean, + container: C, + getShapeRowGeometry: GetShapeRowGeometry, + cx: Coordinate, + cy: Coordinate, + padding: Padding, + node: ShapeTreeNode, + boxes: Box[], + maxRowCount: number, + clipText?: boolean, +) { + return function tryFontSizeFn(initialRowSet: RowSet, fontSize: Pixels): { rowSet: RowSet; completed: boolean } { + let rowSet: RowSet = initialRowSet; + + const wordSpacing = getWordSpacing(fontSize); + + // model text pieces, obtaining their width at the current font size + const measurements = measure(fontSize, boxes); + const allMeasuredBoxes: RowBox[] = measurements.map( + ({ width, emHeightDescent, emHeightAscent }: TextMetrics, i: number) => ({ + width, + verticalOffset: -(emHeightDescent + emHeightAscent) / 2, // meaning, `middle`, + wordBeginning: NaN, + ...boxes[i], + fontSize, // iterated fontSize overrides a possible more global fontSize + }), + ); + const linePitch = fontSize; + + // rowSet building starts + let targetRowCount = 0; + let measuredBoxes = allMeasuredBoxes.slice(); + let innerCompleted = false; + + // iterate through possible target row counts + while (++targetRowCount <= maxRowCount && !innerCompleted) { + measuredBoxes = allMeasuredBoxes.slice(); + rowSet = { + id: nodeId(node), + fontSize, + fillTextColor: '', + rotation, + verticalAlignment, + leftAlign, + clipText, + rows: [...new Array(targetRowCount)].map(() => ({ + rowWords: [], + rowAnchorX: NaN, + rowAnchorY: NaN, + maximumLength: NaN, + length: NaN, + })), + container, + }; + + let currentRowIndex = 0; + + // iterate through rows + while (currentRowIndex < targetRowCount) { + const currentRow = rowSet.rows[currentRowIndex]; + const currentRowWords = currentRow.rowWords; + + // current row geometries + const { maximumRowLength, rowAnchorX, rowAnchorY } = getShapeRowGeometry( + container, + cx, + cy, + targetRowCount, + linePitch, + currentRowIndex, + fontSize, + rotation, + verticalAlignment, + padding, + ); + + currentRow.rowAnchorX = rowAnchorX; + currentRow.rowAnchorY = rowAnchorY; + currentRow.maximumLength = maximumRowLength; + + // row building starts + let currentRowLength = 0; + let rowHasRoom = true; + + // iterate through words: keep adding words while there's room + while (measuredBoxes.length > 0 && rowHasRoom) { + // adding box to row + const [currentBox] = measuredBoxes; + + const wordBeginning = currentRowLength; + currentRowLength += currentBox.width + wordSpacing; + + if (clipText || currentRowLength <= currentRow.maximumLength) { + currentRowWords.push({ ...currentBox, wordBeginning }); + currentRow.length = currentRowLength; + measuredBoxes.shift(); + } else { + rowHasRoom = false; + } + } + + currentRowIndex++; + } + + innerCompleted = rowSetComplete(rowSet, measuredBoxes); + } + const completed = measuredBoxes.length === 0; + return { rowSet, completed }; + }; +} + +function getRowSet( + boxes: Box[], + maxRowCount: number, + fontSizes: Pixels[], + measure: TextMeasure, + rotation: Radian, + verticalAlignment: VerticalAlignments, + leftAlign: boolean, + container: C, + getShapeRowGeometry: GetShapeRowGeometry, + cx: Coordinate, + cy: Coordinate, + padding: Padding, + node: ShapeTreeNode, + clipText: boolean, +) { + const tryFunction = tryFontSize( + measure, + rotation, + verticalAlignment, + leftAlign, + container, + getShapeRowGeometry, + cx, + cy, + padding, + node, + boxes, + maxRowCount, + clipText, + ); + + // find largest fitting font size + const largestIndex = fontSizes.length - 1; + const response = (i: number) => i + (tryFunction(identityRowSet(), fontSizes[i]).completed ? 0 : largestIndex + 1); + const fontSizeIndex = monotonicHillClimb(response, largestIndex, largestIndex, integerSnap); + + if (!(fontSizeIndex >= 0)) { + return identityRowSet(); + } + + const { rowSet, completed } = tryFunction(identityRowSet(), fontSizes[fontSizeIndex]); // todo in the future, make the hill climber also yield the result to avoid this +1 call + return { ...rowSet, rows: rowSet.rows.filter((r) => completed && !isNaN(r.length)) }; +} + +/** @internal */ +export function inSectorRotation(horizontalTextEnforcer: number, horizontalTextAngleThreshold: number) { + return (node: ShapeTreeNode): Radian => { + let rotation = trueBearingToStandardPositionAngle((node.x0 + node.x1) / 2); + if (Math.abs(node.x1 - node.x0) > horizontalTextAngleThreshold && horizontalTextEnforcer > 0) + rotation *= 1 - horizontalTextEnforcer; + if (TAU / 4 < rotation && rotation < (3 * TAU) / 4) rotation = wrapToTau(rotation - TAU / 2); + return rotation; + }; +} + +type GetRotation = (node: ShapeTreeNode) => Radian; + +/** @internal */ +export function fillTextLayout( + shapeConstructor: ShapeConstructor, + getShapeRowGeometry: GetShapeRowGeometry, + getRotation: GetRotation, +) { + const specificFiller = fill(shapeConstructor, getShapeRowGeometry, getRotation); + return function fillTextLayoutClosure( + measure: TextMeasure, + rawTextGetter: RawTextGetter, + valueGetter: ValueGetterFunction, + valueFormatter: ValueFormatter, + childNodes: QuadViewModel[], + config: Config, + layers: Layer[], + textFillOrigins: PointTuple[], + leftAlign: boolean, + middleAlign: boolean, + ): RowSet[] { + const allFontSizes: Pixels[][] = []; + for (let l = 0; l <= layers.length; l++) { + // get font size spec from config, which layer.fillLabel properties can override + const { minFontSize, maxFontSize, idealFontSizeJump } = { + ...config, + ...(l < layers.length && layers[l].fillLabel), + }; + const fontSizeMagnification = maxFontSize / minFontSize; + const fontSizeJumpCount = Math.round(logarithm(idealFontSizeJump, fontSizeMagnification)); + const realFontSizeJump = Math.pow(fontSizeMagnification, 1 / fontSizeJumpCount); + const fontSizes: Pixels[] = []; + for (let i = 0; i <= fontSizeJumpCount; i++) { + const fontSize = Math.round(minFontSize * Math.pow(realFontSizeJump, i)); + if (!fontSizes.includes(fontSize)) { + fontSizes.push(fontSize); + } + } + allFontSizes.push(fontSizes); + } + + const filler = specificFiller( + config, + layers, + measure, + rawTextGetter, + valueGetter, + valueFormatter, + leftAlign, + middleAlign, + ); + + return childNodes + .map((node: QuadViewModel, i: number) => ({ node, origin: textFillOrigins[i] })) + .sort((a: NodeWithOrigin, b: NodeWithOrigin) => b.node.value - a.node.value) + .reduce( + ( + { rowSets, fontSizes }: { rowSets: RowSet[]; fontSizes: Pixels[][] }, + { node, origin }: { node: QuadViewModel; origin: [Pixels, Pixels] }, + ) => { + const nextRowSet = filler(fontSizes, origin, node); + const layerIndex = node.depth - 1; + return { + rowSets: [...rowSets, nextRowSet], + fontSizes: fontSizes.map((layerFontSizes: Pixels[], index: number) => + !isNaN(nextRowSet.fontSize) && index === layerIndex && !layers[layerIndex]?.fillLabel?.maximizeFontSize + ? layerFontSizes.filter((size: Pixels) => size <= nextRowSet.fontSize) + : layerFontSizes, + ), + }; + }, + { rowSets: [] as RowSet[], fontSizes: allFontSizes }, + ).rowSets; + }; +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/hierarchy_of_arrays.test.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/hierarchy_of_arrays.test.ts new file mode 100644 index 000000000000..d4e529421180 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/hierarchy_of_arrays.test.ts @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getHierarchyOfArrays } from './hierarchy_of_arrays'; + +const rawFacts = [ + { sitc1: '7', exportVal: 0 }, + { sitc1: '3', exportVal: 3 }, + { sitc1: 'G', exportVal: 1 }, + { sitc1: '5', exportVal: -8 }, +]; + +const valueAccessor = (d: any) => d.exportVal; + +const groupByRollupAccessors = [() => null, (d: any) => d.sitc1]; + +describe('Test', () => { + test('getHierarchyOfArrays should omit zero and negative values', () => { + const outerResult = getHierarchyOfArrays(rawFacts, valueAccessor, groupByRollupAccessors, []); + expect(outerResult.length).toBe(1); + + const results = outerResult[0]; + expect(results.length).toBe(2); + expect(results[0]).toBeNull(); + + const result = results[1]; + const expectedLength = rawFacts.filter((d: any) => valueAccessor(d) > 0).length; + expect(expectedLength).toBe(2); + expect(result.children.length).toBe(expectedLength); + }); +}); diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/hierarchy_of_arrays.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/hierarchy_of_arrays.ts new file mode 100644 index 000000000000..9e5f5043db1f --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/hierarchy_of_arrays.ts @@ -0,0 +1,139 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegendItemExtraValues } from '../../../../common/legend'; +import { SeriesKey } from '../../../../common/series_id'; +import { Relation } from '../../../../common/text_utils'; +import { IndexedAccessorFn } from '../../../../utils/accessor'; +import { Datum, ValueAccessor, ValueFormatter } from '../../../../utils/common'; +import { Layer } from '../../specs'; +import { PartitionLayout } from '../types/config_types'; +import { + aggregators, + CHILDREN_KEY, + groupByRollup, + HIERARCHY_ROOT_KEY, + HierarchyOfArrays, + mapEntryValue, + mapsToArrays, + NodeSorter, + Sorter, +} from '../utils/group_by_rollup'; +import { isMosaic, isSunburst, isTreemap } from './viewmodel'; + +function aggregateComparator(accessor: (v: any) => any, sorter: Sorter): NodeSorter { + return (a, b) => sorter(accessor(a), accessor(b)); +} + +const ascending: Sorter = (a, b) => a - b; +const descending: Sorter = (a, b) => b - a; + +const childOrders = { + ascending, + descending, +}; + +const descendingValueNodes = aggregateComparator(mapEntryValue, childOrders.descending); +const ascendingValueNodes = aggregateComparator(mapEntryValue, childOrders.ascending); + +/** + * @internal + */ +export function getHierarchyOfArrays( + rawFacts: Relation, + valueAccessor: ValueAccessor, + groupByRollupAccessors: IndexedAccessorFn[], + sortSpecs: (NodeSorter | null)[], +): HierarchyOfArrays { + const aggregator = aggregators.sum; + + const facts = rawFacts.filter((n) => { + const value = valueAccessor(n); + return Number.isFinite(value) && value > 0; + }); + + // don't render anything if the total, the width or height is not positive + if (facts.reduce((p: number, n) => aggregator.reducer(p, valueAccessor(n)), aggregator.identity()) <= 0) { + return []; + } + + // We can precompute things invariant of how the rectangle is divvied up. + // By introducing `scale`, we no longer need to deal with the dichotomy of + // size as data value vs size as number of pixels in the rectangle + return mapsToArrays(groupByRollup(groupByRollupAccessors, valueAccessor, aggregator, facts), sortSpecs); +} + +const sorter = (layout: PartitionLayout) => ({ sortPredicate }: Layer, i: number) => + sortPredicate || + (isTreemap(layout) || isSunburst(layout) + ? descendingValueNodes + : isMosaic(layout) + ? i === 2 + ? ascendingValueNodes + : descendingValueNodes + : null); + +/** @internal */ +export function partitionTree( + data: Datum[], + valueAccessor: ValueAccessor, + layers: Layer[], + defaultLayout: PartitionLayout, + partitionLayout: PartitionLayout = defaultLayout, +) { + return getHierarchyOfArrays( + data, + valueAccessor, + // eslint-disable-next-line no-shadow + [() => HIERARCHY_ROOT_KEY, ...layers.map(({ groupByRollup }) => groupByRollup)], + [null, ...layers.map(sorter(partitionLayout))], + ); +} + +/** + * Creates flat extra value map from nested key path + * @internal + */ +export function getExtraValueMap( + layers: Layer[], + valueFormatter: ValueFormatter, + tree: HierarchyOfArrays, + maxDepth: number, + depth: number = 0, + keys: Map = new Map(), +): Map { + for (let i = 0; i < tree.length; i++) { + const branch = tree[i]; + const [key, arrayNode] = branch; + const { value, path, [CHILDREN_KEY]: children } = arrayNode; + + if (key != null) { + const values: LegendItemExtraValues = new Map(); + const formattedValue = valueFormatter ? valueFormatter(value) : value; + + values.set(key, formattedValue); + keys.set(path.map(({ index }) => index).join('__'), values); + } + + if (depth < maxDepth) { + getExtraValueMap(layers, valueFormatter, children, maxDepth, depth + 1, keys); + } + } + return keys; +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/link_text_layout.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/link_text_layout.ts new file mode 100644 index 000000000000..d6492f87b563 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/link_text_layout.ts @@ -0,0 +1,202 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getOnPaperColorSet } from '../../../../common/color_calcs'; +import { TAU } from '../../../../common/constants'; +import { + Distance, + meanAngle, + Pixels, + PointTuple, + PointTuples, + trueBearingToStandardPositionAngle, +} from '../../../../common/geometry'; +import { cutToLength, fitText, Font, measureOneBoxWidth, TextMeasure } from '../../../../common/text_utils'; +import { Color, ValueFormatter } from '../../../../utils/common'; +import { Point } from '../../../../utils/point'; +import { Config, LinkLabelConfig } from '../types/config_types'; +import { LinkLabelVM, RawTextGetter, ShapeTreeNode, ValueGetterFunction } from '../types/viewmodel_types'; + +/** @internal */ +export interface LinkLabelsViewModelSpec { + linkLabels: LinkLabelVM[]; + labelFontSpec: Font; + valueFontSpec: Font; + strokeColor?: Color; +} + +/** @internal */ +export function linkTextLayout( + rectWidth: Distance, + rectHeight: Distance, + measure: TextMeasure, + config: Config, + nodesWithoutRoom: ShapeTreeNode[], + currentY: Distance[], + anchorRadius: Distance, + rawTextGetter: RawTextGetter, + valueGetter: ValueGetterFunction, + valueFormatter: ValueFormatter, + maxTextLength: number, + diskCenter: Point, + containerBackgroundColor?: Color, +): LinkLabelsViewModelSpec { + const { linkLabel, sectorLineStroke } = config; + const maxDepth = nodesWithoutRoom.reduce((p: number, n: ShapeTreeNode) => Math.max(p, n.depth), 0); + const yRelativeIncrement = Math.sin(linkLabel.stemAngle) * linkLabel.minimumStemLength; + const rowPitch = linkLabel.fontSize + linkLabel.spacing; + + const { contrastTextColor, strokeColor } = getOnPaperColorSet( + linkLabel.textColor, + sectorLineStroke, + containerBackgroundColor, + ); + const labelFontSpec: Font = { ...linkLabel, textColor: contrastTextColor }; + const valueFontSpec: Font = { ...linkLabel, ...linkLabel.valueFont, textColor: contrastTextColor }; + + const linkLabels: LinkLabelVM[] = nodesWithoutRoom + .filter((n: ShapeTreeNode) => n.depth === maxDepth) // only the outermost ring can have links + .sort((n1: ShapeTreeNode, n2: ShapeTreeNode) => Math.abs(n2.x0 - n2.x1) - Math.abs(n1.x0 - n1.x1)) + .slice(0, linkLabel.maxCount) // largest linkLabel.MaxCount slices + .sort(linkLabelCompare) + .map( + nodeToLinkLabel({ + linkLabel, + anchorRadius, + currentY, + rowPitch, + yRelativeIncrement, + rawTextGetter, + maxTextLength, + valueFormatter, + valueGetter, + measure, + rectWidth, + rectHeight, + diskCenter, + }), + ) + .filter(({ text }) => text !== ''); // cull linked labels whose text was truncated to nothing; + + return { linkLabels, valueFontSpec, labelFontSpec, strokeColor }; +} + +function linkLabelCompare(n1: ShapeTreeNode, n2: ShapeTreeNode) { + const mid1 = meanAngle(n1.x0, n1.x1); + const mid2 = meanAngle(n2.x0, n2.x1); + const dist1 = Math.min(Math.abs(mid1 - TAU / 4), Math.abs(mid1 - (3 * TAU) / 4)); + const dist2 = Math.min(Math.abs(mid2 - TAU / 4), Math.abs(mid2 - (3 * TAU) / 4)); + return dist1 - dist2; +} + +function nodeToLinkLabel({ + linkLabel, + anchorRadius, + currentY, + rowPitch, + yRelativeIncrement, + rawTextGetter, + maxTextLength, + valueFormatter, + valueGetter, + measure, + rectWidth, + rectHeight, + diskCenter, +}: { + linkLabel: LinkLabelConfig; + anchorRadius: Distance; + currentY: Distance[]; + rowPitch: Pixels; + yRelativeIncrement: Distance; + rawTextGetter: RawTextGetter; + maxTextLength: number; + valueFormatter: ValueFormatter; + valueGetter: ValueGetterFunction; + measure: TextMeasure; + rectWidth: Distance; + rectHeight: Distance; + diskCenter: Point; +}) { + const labelFont: Font = linkLabel; // only interested in the font properties + const valueFont: Font = { ...labelFont, ...linkLabel.valueFont }; // only interested in the font properties + return function nodeToLinkLabelMap(node: ShapeTreeNode): LinkLabelVM { + // geometry + const midAngle = trueBearingToStandardPositionAngle((node.x0 + node.x1) / 2); + const north = midAngle < TAU / 2 ? 1 : -1; + const rightSide = TAU / 4 < midAngle && midAngle < (3 * TAU) / 4 ? 0 : 1; + const west = rightSide ? 1 : -1; + const cos = Math.cos(midAngle); + const sin = Math.sin(midAngle); + const x0 = cos * anchorRadius; + const y0 = sin * anchorRadius; + const x = cos * (anchorRadius + linkLabel.radiusPadding); + const y = sin * (anchorRadius + linkLabel.radiusPadding); + const stemFromX = x; // might be different in the future, eg. to allow a small gap: doc purpose + const stemFromY = y; // might be different in the future, eg. to allow a small gap: doc purpose + + // calculate and remember vertical offset, as linked labels accrete + const poolIndex = rightSide + (1 - north); + const relativeY = north * y; + const yOffset = Math.max(currentY[poolIndex] + rowPitch, relativeY + yRelativeIncrement, rowPitch / 2); + currentY[poolIndex] = yOffset; + + // more geometry: the part that depends on vertical position + const cy = north * yOffset; + const stemToX = x + north * west * cy - west * relativeY; + const stemToY = cy; + const translateX = stemToX + west * (linkLabel.horizontalStemLength + linkLabel.gap); + const translate: PointTuple = [translateX, stemToY]; + + // the path points of the label link, ie. a polyline + const linkLabels: PointTuples = [ + [x0, y0], + [stemFromX, stemFromY], + [stemToX, stemToY], + [stemToX + west * linkLabel.horizontalStemLength, stemToY], + ]; + + // value text is simple: the full, formatted value is always shown, not truncated + const valueText = valueFormatter(valueGetter(node)); + const valueWidth = measureOneBoxWidth(measure, linkLabel.fontSize, { ...valueFont, text: valueText }); + const widthAdjustment = valueWidth + 2 * linkLabel.fontSize; // gap between label and value, plus possibly 2em wide ellipsis + + // label text removes space allotted for value and gaps, then tries to fit as much as possible + const labelText = cutToLength(rawTextGetter(node), maxTextLength); + const allottedLabelWidth = Math.max( + 0, + rightSide ? rectWidth - diskCenter.x - translateX - widthAdjustment : diskCenter.x + translateX - widthAdjustment, + ); + const { text, width, verticalOffset } = + linkLabel.fontSize / 2 <= cy + diskCenter.y && cy + diskCenter.y <= rectHeight - linkLabel.fontSize / 2 + ? fitText(measure, labelText, allottedLabelWidth, linkLabel.fontSize, labelFont) + : { text: '', width: 0, verticalOffset: 0 }; + + return { + linkLabels, + translate, + text, + valueText, + width, + valueWidth, + verticalOffset, + textAlign: rightSide ? 'left' : 'right', + }; + }; +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/picked_shapes.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/picked_shapes.ts new file mode 100644 index 000000000000..dae532926cd6 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/picked_shapes.ts @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LayerValue } from '../../../../specs'; +import { Point } from '../../../../utils/point'; +import { ContinuousDomainFocus } from '../../renderer/canvas/partition'; +import { MODEL_KEY } from '../config'; +import { QuadViewModel, ShapeViewModel } from '../types/viewmodel_types'; +import { AGGREGATE_KEY, DEPTH_KEY, getNodeName, PARENT_KEY, PATH_KEY, SORT_INDEX_KEY } from '../utils/group_by_rollup'; + +/** @internal */ +export const pickedShapes = ( + models: ShapeViewModel[], + { x, y }: Point, + foci: ContinuousDomainFocus[], +): QuadViewModel[] => + models.flatMap(({ diskCenter, pickQuads }) => pickQuads(x - diskCenter.x, y - diskCenter.y, foci[0])); + +/** @internal */ +export function pickShapesLayerValues(shapes: QuadViewModel[]): LayerValue[][] { + const maxDepth = shapes.reduce((acc, curr) => Math.max(acc, curr.depth), 0); + return shapes + .filter(({ depth }) => depth === maxDepth) // eg. lowest layer in a treemap, where layers overlap in screen space; doesn't apply to sunburst/flame + .map>((viewModel) => { + const values: Array = [ + { + smAccessorValue: viewModel.smAccessorValue, + groupByRollup: viewModel.dataName, + value: viewModel[AGGREGATE_KEY], + depth: viewModel[DEPTH_KEY], + sortIndex: viewModel[SORT_INDEX_KEY], + path: viewModel[PATH_KEY], + }, + ]; + let node = viewModel[MODEL_KEY]; + while (node[DEPTH_KEY] > 0) { + const value = node[AGGREGATE_KEY]; + const dataName = getNodeName(node); + values.push({ + smAccessorValue: viewModel.smAccessorValue, + groupByRollup: dataName, + value, + depth: node[DEPTH_KEY], + sortIndex: node[SORT_INDEX_KEY], + path: node[PATH_KEY], + }); + + node = node[PARENT_KEY]; + } + return values.reverse(); + }); +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/scenegraph.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/scenegraph.ts new file mode 100644 index 000000000000..21c5c4bf8623 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/scenegraph.ts @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { measureText } from '../../../../common/text_utils'; +import { SmallMultiplesStyle } from '../../../../specs'; +import { Color, identity, mergePartial, RecursivePartial } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { Layer, PartitionSpec } from '../../specs'; +import { config as defaultConfig, VALUE_GETTERS } from '../config'; +import { Config } from '../types/config_types'; +import { + nullShapeViewModel, + RawTextGetter, + ShapeTreeNode, + ShapeViewModel, + ValueGetter, +} from '../types/viewmodel_types'; +import { DEPTH_KEY, HierarchyOfArrays } from '../utils/group_by_rollup'; +import { PanelPlacement, shapeViewModel } from './viewmodel'; + +function rawTextGetter(layers: Layer[]): RawTextGetter { + return (node: ShapeTreeNode) => { + const accessorFn = layers[node[DEPTH_KEY] - 1].nodeLabel || identity; + return `${accessorFn(node.dataName)}`; + }; +} + +/** @internal */ +export function valueGetterFunction(valueGetter: ValueGetter) { + return typeof valueGetter === 'function' ? valueGetter : VALUE_GETTERS[valueGetter]; +} + +/** @internal */ +export function getShapeViewModel( + partitionSpec: PartitionSpec, + parentDimensions: Dimensions, + tree: HierarchyOfArrays, + containerBackgroundColor: Color, + smallMultiplesStyle: SmallMultiplesStyle, + panelPlacement: PanelPlacement, +): ShapeViewModel { + const { width, height } = parentDimensions; + const { layers, topGroove, config: specConfig } = partitionSpec; + const textMeasurer = document.createElement('canvas'); + const textMeasurerCtx = textMeasurer.getContext('2d'); + const partialConfig: RecursivePartial = { ...specConfig, width, height }; + const config: Config = mergePartial(defaultConfig, partialConfig); + if (!textMeasurerCtx) { + return nullShapeViewModel(config, { x: width / 2, y: height / 2 }); + } + const valueGetter = valueGetterFunction(partitionSpec.valueGetter); + + return shapeViewModel( + measureText(textMeasurerCtx), + config, + layers, + rawTextGetter(layers), + partitionSpec.valueFormatter, + partitionSpec.percentFormatter, + valueGetter, + tree, + topGroove, + containerBackgroundColor, + smallMultiplesStyle, + panelPlacement, + ); +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/tooltip_info.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/tooltip_info.ts new file mode 100644 index 000000000000..6f0e49dabd41 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/tooltip_info.ts @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TooltipInfo } from '../../../../components/tooltip/types'; +import { LabelAccessor, ValueFormatter } from '../../../../utils/common'; +import { percentValueGetter, sumValueGetter } from '../config'; +import { QuadViewModel, ValueGetter } from '../types/viewmodel_types'; +import { valueGetterFunction } from './scenegraph'; + +/** @internal */ +export const EMPTY_TOOLTIP = Object.freeze({ + header: null, + values: [], +}); + +/** @internal */ +export function getTooltipInfo( + pickedShapes: QuadViewModel[], + labelFormatters: (LabelAccessor | undefined)[], + valueGetter: ValueGetter, + valueFormatter: ValueFormatter, + percentFormatter: ValueFormatter, + id: string, +): TooltipInfo { + if (!valueFormatter || !labelFormatters) { + return EMPTY_TOOLTIP; + } + + const tooltipInfo: TooltipInfo = { + header: null, + values: [], + }; + + const valueGetterFun = valueGetterFunction(valueGetter); + const primaryValueGetterFun = valueGetterFun === percentValueGetter ? sumValueGetter : valueGetterFun; + pickedShapes.forEach((shape) => { + const formatter = labelFormatters[shape.depth - 1]; + const value = primaryValueGetterFun(shape); + + tooltipInfo.values.push({ + label: formatter ? formatter(shape.dataName) : shape.dataName, + color: shape.fillColor, + isHighlighted: false, + isVisible: true, + seriesIdentifier: { + specId: id, + key: id, + }, + value, + formattedValue: `${valueFormatter(value)} (${percentFormatter(percentValueGetter(shape))})`, + valueAccessor: shape.depth, + // the datum is omitted ATM due to the aggregated and nested nature of a partition section + }); + }); + + return tooltipInfo; +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts new file mode 100644 index 000000000000..b1d7c293a0f1 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts @@ -0,0 +1,525 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { argsToRGBString, stringToRGB } from '../../../../common/color_library_wrappers'; +import { TAU } from '../../../../common/constants'; +import { fillTextColor } from '../../../../common/fill_text_color'; +import { + Distance, + meanAngle, + Pixels, + PointTuple, + Radius, + trueBearingToStandardPositionAngle, +} from '../../../../common/geometry'; +import { Part, TextMeasure } from '../../../../common/text_utils'; +import { GroupByAccessor, SmallMultiplesStyle } from '../../../../specs'; +import { StrokeStyle, ValueFormatter, Color, RecursivePartial } from '../../../../utils/common'; +import { Layer } from '../../specs'; +import { config as defaultConfig, MODEL_KEY, percentValueGetter } from '../config'; +import { Config, FillLabelConfig, PartitionLayout } from '../types/config_types'; +import { + nullShapeViewModel, + OutsideLinksViewModel, + PartitionSmallMultiplesModel, + PickFunction, + QuadViewModel, + RawTextGetter, + RowSet, + ShapeTreeNode, + ShapeViewModel, + ValueGetterFunction, +} from '../types/viewmodel_types'; +import { ringSectorConstruction } from '../utils/circline_geometry'; +import { + aggregateAccessor, + ArrayEntry, + depthAccessor, + entryKey, + entryValue, + mapEntryValue, + parentAccessor, + sortIndexAccessor, + HierarchyOfArrays, + pathAccessor, +} from '../utils/group_by_rollup'; +import { sunburst } from '../utils/sunburst'; +import { getTopPadding, LayerLayout, treemap } from '../utils/treemap'; +import { + fillTextLayout, + getRectangleRowGeometry, + getSectorRowGeometry, + inSectorRotation, + nodeId, +} from './fill_text_layout'; +import { linkTextLayout } from './link_text_layout'; + +/** @internal */ +export const isMosaic = (p: PartitionLayout | undefined) => p === PartitionLayout.mosaic; + +/** @internal */ +export const isTreemap = (p: PartitionLayout | undefined) => p === PartitionLayout.treemap; + +/** @internal */ +export const isSunburst = (p: PartitionLayout | undefined) => p === PartitionLayout.sunburst; + +/** @internal */ +export const isIcicle = (p: PartitionLayout | undefined) => p === PartitionLayout.icicle; + +/** @internal */ +export const isFlame = (p: PartitionLayout | undefined) => p === PartitionLayout.flame; + +/** @internal */ +export const isLinear = (p: PartitionLayout | undefined) => isFlame(p) || isIcicle(p); + +/** @internal */ +export const isSimpleLinear = (config: RecursivePartial, layers: Layer[]) => + isLinear(config.partitionLayout) && layers.every((l) => l.fillLabel?.clipText ?? config.fillLabel?.clipText); + +function grooveAccessor(n: ArrayEntry) { + return entryValue(n).depth > 1 ? 1 : [0, 2][entryValue(n).depth]; +} + +function topGrooveAccessor(topGroovePx: Pixels) { + return (n: ArrayEntry) => (entryValue(n).depth > 0 ? topGroovePx : grooveAccessor(n)); +} + +function rectangleFillOrigins(n: ShapeTreeNode): PointTuple { + return [(n.x0 + n.x1) / 2, (n.y0 + n.y1) / 2]; +} + +/** + * @internal + */ +export const ringSectorInnerRadius = (n: ShapeTreeNode): Radius => n.y0px; + +/** + * @internal + */ +export const ringSectorOuterRadius = (n: ShapeTreeNode): Radius => n.y1px; + +/** + * @internal + */ +export const ringSectorMiddleRadius = (n: ShapeTreeNode): Radius => n.yMidPx; + +function sectorFillOrigins(fillOutside: boolean) { + return (node: ShapeTreeNode): [number, number] => { + const midAngle = (node.x0 + node.x1) / 2; + const divider = 10; + const innerBias = fillOutside ? 9 : 1; + const outerBias = divider - innerBias; + const radius = (innerBias * ringSectorInnerRadius(node) + outerBias * ringSectorOuterRadius(node)) / divider; + const cx = Math.cos(trueBearingToStandardPositionAngle(midAngle)) * radius; + const cy = Math.sin(trueBearingToStandardPositionAngle(midAngle)) * radius; + return [cx, cy]; + }; +} + +const minRectHeightForText: Pixels = 8; + +/** @internal */ +export function makeQuadViewModel( + childNodes: ShapeTreeNode[], + layers: Layer[], + sectorLineWidth: Pixels, + sectorLineStroke: StrokeStyle, + smAccessorValue: ReturnType, + index: number, + innerIndex: number, + fillLabel: FillLabelConfig, + isSunburstLayout: boolean, + containerBackgroundColor?: Color, +): Array { + return childNodes.map((node) => { + const opacityMultiplier = 1; // could alter in the future, eg. in response to interactions + const layer = layers[node.depth - 1]; + const fillColorSpec = layer && layer.shape && layer.shape.fillColor; + const fill = fillColorSpec ?? 'rgba(128,0,0,0.5)'; + const shapeFillColor = typeof fill === 'function' ? fill(node, node.sortIndex, node[MODEL_KEY].children) : fill; + const { r, g, b, opacity } = stringToRGB(shapeFillColor); + const fillColor = argsToRGBString(r, g, b, opacity * opacityMultiplier); + const strokeWidth = sectorLineWidth; + const strokeStyle = sectorLineStroke; + const textNegligible = node.y1px - node.y0px < minRectHeightForText; + const { textColor, textInvertible, textContrast } = { ...fillLabel, ...layer.fillLabel }; + const color = + !isSunburstLayout && textNegligible + ? 'transparent' + : fillTextColor(textColor, textInvertible, textContrast, fillColor, containerBackgroundColor); + return { index, innerIndex, smAccessorValue, strokeWidth, strokeStyle, fillColor, textColor: color, ...node }; + }); +} + +/** @internal */ +export function makeOutsideLinksViewModel( + outsideFillNodes: ShapeTreeNode[], + rowSets: RowSet[], + linkLabelRadiusPadding: Distance, +): OutsideLinksViewModel[] { + return outsideFillNodes + .map((node, i: number) => { + const rowSet = rowSets[i]; + if (!rowSet.rows.reduce((p, row) => p + row.rowWords.length, 0)) return { points: [] }; + const radius = ringSectorOuterRadius(node); + const midAngle = trueBearingToStandardPositionAngle(meanAngle(node.x0, node.x1)); + const cos = Math.cos(midAngle); + const sin = Math.sin(midAngle); + const x0 = cos * radius; + const y0 = sin * radius; + const x = cos * (radius + linkLabelRadiusPadding); + const y = sin * (radius + linkLabelRadiusPadding); + return { + points: [ + [x0, y0], + [x, y], + ], + }; + }) + .filter(({ points }: OutsideLinksViewModel) => points.length > 1); +} + +/** @internal */ +export interface RectangleConstruction { + x0: Pixels; + x1: Pixels; + y0: Pixels; + y1: Pixels; +} + +function rectangleConstruction(treeHeight: number, topGroove: number | null) { + return function rectangleConstructionClosure(node: ShapeTreeNode): RectangleConstruction { + return node.depth < treeHeight && topGroove !== null + ? { + x0: node.x0, + y0: node.y0px, + x1: node.x1, + y1: node.y0px + getTopPadding(topGroove, node.y1px - node.y0px), + } + : { + x0: node.x0, + y0: node.y0px, + x1: node.x1, + y1: node.y1px, + }; + }; +} + +const rawChildNodes = ( + partitionLayout: PartitionLayout, + tree: HierarchyOfArrays, + topGroove: number, + width: number, + height: number, + clockwiseSectors: boolean, + specialFirstInnermostSector: boolean, + maxDepth: number, +): Array => { + const totalValue = tree.reduce((p: number, n: ArrayEntry): number => p + mapEntryValue(n), 0); + switch (partitionLayout) { + case PartitionLayout.sunburst: + const sunburstValueToAreaScale = TAU / totalValue; + const sunburstAreaAccessor = (e: ArrayEntry) => sunburstValueToAreaScale * mapEntryValue(e); + return sunburst(tree, sunburstAreaAccessor, { x0: 0, y0: -1 }, clockwiseSectors, specialFirstInnermostSector); + + case PartitionLayout.treemap: + case PartitionLayout.mosaic: + const treemapInnerArea = width * height; // assuming 1 x 1 unit square + const treemapValueToAreaScale = treemapInnerArea / totalValue; + const treemapAreaAccessor = (e: ArrayEntry) => treemapValueToAreaScale * mapEntryValue(e); + return treemap( + tree, + treemapAreaAccessor, + topGrooveAccessor(topGroove), + grooveAccessor, + { + x0: 0, + y0: 0, + width, + height, + }, + isMosaic(partitionLayout) ? [LayerLayout.vertical, LayerLayout.horizontal] : [], + ); + + case PartitionLayout.icicle: + case PartitionLayout.flame: + const icicleLayout = isIcicle(partitionLayout); + const icicleValueToAreaScale = width / totalValue; + const icicleAreaAccessor = (e: ArrayEntry) => icicleValueToAreaScale * mapEntryValue(e); + const icicleRowHeight = height / maxDepth; + const result = sunburst(tree, icicleAreaAccessor, { x0: 0, y0: -icicleRowHeight }, true, false, icicleRowHeight); + return icicleLayout + ? result + : result.map(({ y0, y1, ...rest }) => ({ y0: height - y1, y1: height - y0, ...rest })); + + default: + // Let's ensure TS complains if we add a new PartitionLayout type in the future without creating a `case` for it + // Hopefully, a future TS version will do away with the need for this boilerplate `default`. Now TS even needs a `default` even if all possible cases are covered. + // Even in runtime it does something sensible (returns the empty set); explicit throwing is avoided as it can deopt the function + return ((layout: never) => layout ?? [])(partitionLayout); + } +}; + +/** @internal */ +export type PanelPlacement = PartitionSmallMultiplesModel; + +/** + * Todo move it to config + * @internal + */ +export const panelTitleFontSize = 16; + +/** @internal */ +export function shapeViewModel( + textMeasure: TextMeasure, + config: Config, + layers: Layer[], + rawTextGetter: RawTextGetter, + specifiedValueFormatter: ValueFormatter, + specifiedPercentFormatter: ValueFormatter, + valueGetter: ValueGetterFunction, + tree: HierarchyOfArrays, + topGroove: Pixels, + containerBackgroundColor: Color, + smallMultiplesStyle: SmallMultiplesStyle, + panel: PanelPlacement, +): ShapeViewModel { + const { + width, + height, + emptySizeRatio, + outerSizeRatio, + fillOutside, + linkLabel, + clockwiseSectors, + specialFirstInnermostSector, + minFontSize, + partitionLayout, + sectorLineWidth, + } = config; + + const { marginLeftPx, marginTopPx, panelInnerWidth, panelInnerHeight } = panel; + + const treemapLayout = isTreemap(partitionLayout); + const mosaicLayout = isMosaic(partitionLayout); + const sunburstLayout = isSunburst(partitionLayout); + const icicleLayout = isIcicle(partitionLayout); + const flameLayout = isFlame(partitionLayout); + const simpleLinear = isSimpleLinear(config, layers); + + const diskCenter = isSunburst(partitionLayout) + ? { + x: marginLeftPx + panelInnerWidth / 2, + y: marginTopPx + panelInnerHeight / 2, + } + : { + x: marginLeftPx, + y: marginTopPx, + }; + + // don't render anything if the total, the width or height is not positive + if (!(width > 0) || !(height > 0) || tree.length === 0) { + return nullShapeViewModel(config, diskCenter); + } + + const longestPath = ([, { children, path }]: ArrayEntry): number => + children.length > 0 ? children.reduce((p, n) => Math.max(p, longestPath(n)), 0) : path.length; + const maxDepth = longestPath(tree[0]) - 2; // don't include the root node + const childNodes = rawChildNodes( + partitionLayout, + tree, + topGroove, + panelInnerWidth, + panelInnerHeight, + clockwiseSectors, + specialFirstInnermostSector, + maxDepth, + ); + + const shownChildNodes = childNodes.filter((n: Part) => { + const layerIndex = entryValue(n.node).depth - 1; + const layer = layers[layerIndex]; + return !layer || !layer.showAccessor || layer.showAccessor(entryKey(n.node)); + }); + + // use the smaller of the two sizes, as a circle fits into a square + const circleMaximumSize = Math.min( + panelInnerWidth, + panelInnerHeight - (panel.panelTitle.length > 0 ? panelTitleFontSize * 2 : 0), + ); + const outerRadius: Radius = Math.min(outerSizeRatio * circleMaximumSize, circleMaximumSize - sectorLineWidth) / 2; + const innerRadius: Radius = outerRadius - (1 - emptySizeRatio) * outerRadius; + const treeHeight = shownChildNodes.reduce((p: number, n: Part) => Math.max(p, entryValue(n.node).depth), 0); // 1: pie, 2: two-ring donut etc. + const ringThickness = (outerRadius - innerRadius) / treeHeight; + const partToShapeFn = partToShapeTreeNode(!sunburstLayout, innerRadius, ringThickness); + const quadViewModel = makeQuadViewModel( + shownChildNodes.slice(1).map(partToShapeFn), + layers, + config.sectorLineWidth, + config.sectorLineStroke, + panel.smAccessorValue, + panel.index, + panel.innerIndex, + config.fillLabel, + sunburstLayout, + containerBackgroundColor, + ); + + // fill text + const roomCondition = (n: ShapeTreeNode) => { + const diff = n.x1 - n.x0; + return sunburstLayout + ? (diff < 0 ? TAU + diff : diff) * ringSectorMiddleRadius(n) > Math.max(minFontSize, linkLabel.maximumSection) + : n.x1 - n.x0 > minFontSize && n.y1px - n.y0px > minFontSize; + }; + + const nodesWithRoom = quadViewModel.filter(roomCondition); + const outsideFillNodes = fillOutside && sunburstLayout ? nodesWithRoom : []; + + const textFillOrigins = nodesWithRoom.map(sunburstLayout ? sectorFillOrigins(fillOutside) : rectangleFillOrigins); + + const valueFormatter = valueGetter === percentValueGetter ? specifiedPercentFormatter : specifiedValueFormatter; + + const getRowSets = sunburstLayout + ? fillTextLayout( + ringSectorConstruction(config, innerRadius, ringThickness), + getSectorRowGeometry, + inSectorRotation(config.horizontalTextEnforcer, config.horizontalTextAngleThreshold), + ) + : simpleLinear + ? () => [] // no multirow layout needed for simpleLinear partitions + : fillTextLayout( + rectangleConstruction(treeHeight, treemapLayout || mosaicLayout ? topGroove : null), + getRectangleRowGeometry, + () => 0, + ); + const rowSets: RowSet[] = getRowSets( + textMeasure, + rawTextGetter, + valueGetter, + valueFormatter, + nodesWithRoom, + config, + layers, + textFillOrigins, + !sunburstLayout, + !(treemapLayout || mosaicLayout), + ); + + // whiskers (ie. just lines, no text) for fill text outside the outer radius + const outsideLinksViewModel = makeOutsideLinksViewModel(outsideFillNodes, rowSets, linkLabel.radiusPadding); + + // linked text + const currentY = [-height, -height, -height, -height]; + + const nodesWithoutRoom = + fillOutside || treemapLayout || mosaicLayout || icicleLayout || flameLayout + ? [] // outsideFillNodes and linkLabels are in inherent conflict due to very likely overlaps + : quadViewModel.filter((n: ShapeTreeNode) => { + const id = nodeId(n); + const foundInFillText = rowSets.find((r: RowSet) => r.id === id); + // successful text render if found, and has some row(s) + return !(foundInFillText && foundInFillText.rows.length > 0); + }); + const maxLinkedLabelTextLength = config.linkLabel.maxTextLength; + const linkLabelViewModels = linkTextLayout( + panelInnerWidth, + panelInnerHeight, + textMeasure, + config, + nodesWithoutRoom, + currentY, + outerRadius, + rawTextGetter, + valueGetter, + valueFormatter, + maxLinkedLabelTextLength, + { + x: width * panel.left + panelInnerWidth / 2, + y: height * panel.top + panelInnerHeight / 2, + }, + containerBackgroundColor, + ); + + const pickQuads: PickFunction = sunburstLayout + ? (x, y) => { + return quadViewModel.filter(({ x0, y0px, x1, y1px }) => { + const angleX = (Math.atan2(y, x) + TAU / 4 + TAU) % TAU; + const yPx = Math.sqrt(x * x + y * y); + return x0 <= angleX && angleX <= x1 && y0px <= yPx && yPx <= y1px; + }); + } + : (x, y, { currentFocusX0, currentFocusX1 }) => { + return quadViewModel.filter(({ x0, y0px, x1, y1px }) => { + const scale = width / (currentFocusX1 - currentFocusX0); + const fx0 = Math.max((x0 - currentFocusX0) * scale, 0); + const fx1 = Math.min((x1 - currentFocusX0) * scale, width); + return fx0 <= x && x < fx1 && y0px < y && y <= y1px; + }); + }; + + // combined viewModel + return { + partitionLayout: config?.partitionLayout ?? defaultConfig.partitionLayout, + smAccessorValue: panel.smAccessorValue, + panelTitle: panel.panelTitle, + index: panel.index, + innerIndex: panel.innerIndex, + width: panel.width, + height: panel.height, + top: panel.top, + left: panel.left, + innerRowCount: panel.innerRowCount, + innerColumnCount: panel.innerColumnCount, + innerRowIndex: panel.innerRowIndex, + innerColumnIndex: panel.innerColumnIndex, + marginLeftPx: panel.marginLeftPx, + marginTopPx: panel.marginTopPx, + panelInnerWidth: panel.panelInnerWidth, + panelInnerHeight: panel.panelInnerHeight, + + config, + layers, + diskCenter, + quadViewModel, + rowSets, + linkLabelViewModels, + outsideLinksViewModel, + pickQuads, + outerRadius, + }; +} + +function partToShapeTreeNode(treemapLayout: boolean, innerRadius: Radius, ringThickness: number) { + return ({ node, x0, x1, y0, y1 }: Part): ShapeTreeNode => ({ + dataName: entryKey(node), + depth: depthAccessor(node), + value: aggregateAccessor(node), + [MODEL_KEY]: parentAccessor(node), + sortIndex: sortIndexAccessor(node), + path: pathAccessor(node), + x0, + x1, + y0, + y1, + y0px: treemapLayout ? y0 : innerRadius + y0 * ringThickness, + y1px: treemapLayout ? y1 : innerRadius + y1 * ringThickness, + yMidPx: treemapLayout ? (y0 + y1) / 2 : innerRadius + ((y0 + y1) / 2) * ringThickness, + }); +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/partition.test.tsx b/packages/osd-charts/src/chart_types/partition_chart/partition.test.tsx new file mode 100644 index 000000000000..f67e3dbec098 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/partition.test.tsx @@ -0,0 +1,359 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Store } from 'redux'; + +import { MockGlobalSpec, MockSeriesSpec } from '../../mocks/specs'; +import { MockStore } from '../../mocks/store'; +import { GlobalChartState } from '../../state/chart_state'; +import { LegendItemLabel } from '../../state/selectors/get_legend_items_labels'; +import { HIERARCHY_ROOT_KEY } from './layout/utils/group_by_rollup'; +import { computeLegendSelector } from './state/selectors/compute_legend'; +import { getLegendItemsLabels } from './state/selectors/get_legend_items_labels'; + +// sorting is useful to ensure tests pass even if order changes (where order doesn't matter) +const ascByLabel = (a: LegendItemLabel, b: LegendItemLabel) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0); + +describe('Retain hierarchy even with arbitrary names', () => { + type TestDatum = { cat1: string; cat2: string; val: number }; + const specJSON = { + data: [ + { cat1: 'A', cat2: 'A', val: 1 }, + { cat1: 'A', cat2: 'B', val: 1 }, + { cat1: 'B', cat2: 'A', val: 1 }, + { cat1: 'B', cat2: 'B', val: 1 }, + { cat1: 'C', cat2: 'A', val: 1 }, + { cat1: 'C', cat2: 'B', val: 1 }, + ], + valueAccessor: (d: TestDatum) => d.val, + layers: [ + { + groupByRollup: (d: TestDatum) => d.cat1, + }, + { + groupByRollup: (d: TestDatum) => d.cat2, + }, + ], + }; + let store: Store; + + beforeEach(() => { + store = MockStore.default(); + }); + + describe('getLegendItemsLabels', () => { + // todo discuss question marks about testing this selector, and also about unification with `get_legend_items_labels.test.ts` + + it('all distinct labels are present', () => { + MockStore.addSpecs([MockGlobalSpec.settings({ showLegend: true }), MockSeriesSpec.sunburst(specJSON)], store); + expect(getLegendItemsLabels(store.getState()).sort(ascByLabel)).toEqual([ + { depth: 2, label: 'A' }, + { depth: 2, label: 'B' }, + { depth: 1, label: 'C' }, + ]); + }); + + it('no labels are present if showLegend is false', () => { + MockStore.addSpecs([MockGlobalSpec.settings({ showLegend: false }), MockSeriesSpec.sunburst(specJSON)], store); + expect(getLegendItemsLabels(store.getState())).toEqual([]); + }); + + it('no labels are present if showLegend is missing', () => { + MockStore.addSpecs([MockSeriesSpec.sunburst(specJSON)], store); + expect(getLegendItemsLabels(store.getState())).toEqual([]); + }); + + it('special case: one input, one label', () => { + MockStore.addSpecs( + [ + MockGlobalSpec.settings({ showLegend: true }), + MockSeriesSpec.sunburst({ ...specJSON, data: [{ cat1: 'A', cat2: 'A', val: 1 }] }), + ], + store, + ); + expect(getLegendItemsLabels(store.getState())).toEqual([{ depth: 2, label: 'A' }]); + }); + + it('special case: one input, two labels', () => { + MockStore.addSpecs( + [ + MockGlobalSpec.settings({ showLegend: true }), + MockSeriesSpec.sunburst({ ...specJSON, data: [{ cat1: 'C', cat2: 'B', val: 1 }] }), + ], + store, + ); + expect(getLegendItemsLabels(store.getState()).sort(ascByLabel)).toEqual([ + { depth: 2, label: 'B' }, + { depth: 1, label: 'C' }, + ]); + }); + + it('special case: no labels', () => { + MockStore.addSpecs( + [MockGlobalSpec.settings({ showLegend: true }), MockSeriesSpec.sunburst({ ...specJSON, data: [] })], + store, + ); + expect(getLegendItemsLabels(store.getState()).map((l) => l.label)).toEqual([]); + }); + }); + + describe('getLegendItems', () => { + // todo discuss question marks about testing this selector, and also about unification with `get_legend_items_labels.test.ts` + + it('all distinct labels are present', () => { + MockStore.addSpecs( + [ + MockGlobalSpec.settings({ showLegend: true }), + MockGlobalSpec.settings({ showLegend: true }), + MockSeriesSpec.sunburst(specJSON), + ], + store, + ); + expect(computeLegendSelector(store.getState())).toEqual([ + { + childId: 'A', + color: 'rgba(128, 0, 0, 0.5)', + path: [ + { index: 0, value: HIERARCHY_ROOT_KEY }, + { index: 0, value: 'A' }, + ], + depth: 0, + label: 'A', + seriesIdentifiers: [{ key: 'A', specId: 'spec1' }], + keys: [], + }, + { + childId: 'A', + color: 'rgba(128, 0, 0, 0.5)', + path: [ + { index: 0, value: HIERARCHY_ROOT_KEY }, + { index: 0, value: 'A' }, + { index: 0, value: 'A' }, + ], + depth: 1, + label: 'A', + seriesIdentifiers: [{ key: 'A', specId: 'spec1' }], + keys: [], + }, + { + childId: 'B', + color: 'rgba(128, 0, 0, 0.5)', + path: [ + { index: 0, value: HIERARCHY_ROOT_KEY }, + { index: 0, value: 'A' }, + { index: 1, value: 'B' }, + ], + depth: 1, + label: 'B', + seriesIdentifiers: [{ key: 'B', specId: 'spec1' }], + keys: [], + }, + { + childId: 'B', + color: 'rgba(128, 0, 0, 0.5)', + path: [ + { index: 0, value: HIERARCHY_ROOT_KEY }, + { index: 1, value: 'B' }, + ], + depth: 0, + label: 'B', + seriesIdentifiers: [{ key: 'B', specId: 'spec1' }], + keys: [], + }, + { + childId: 'A', + color: 'rgba(128, 0, 0, 0.5)', + path: [ + { index: 0, value: HIERARCHY_ROOT_KEY }, + { index: 1, value: 'B' }, + { index: 0, value: 'A' }, + ], + depth: 1, + label: 'A', + seriesIdentifiers: [{ key: 'A', specId: 'spec1' }], + keys: [], + }, + { + childId: 'B', + color: 'rgba(128, 0, 0, 0.5)', + path: [ + { index: 0, value: HIERARCHY_ROOT_KEY }, + { index: 1, value: 'B' }, + { index: 1, value: 'B' }, + ], + depth: 1, + label: 'B', + seriesIdentifiers: [{ key: 'B', specId: 'spec1' }], + keys: [], + }, + { + childId: 'C', + color: 'rgba(128, 0, 0, 0.5)', + path: [ + { index: 0, value: HIERARCHY_ROOT_KEY }, + { index: 2, value: 'C' }, + ], + depth: 0, + label: 'C', + seriesIdentifiers: [{ key: 'C', specId: 'spec1' }], + keys: [], + }, + { + childId: 'A', + color: 'rgba(128, 0, 0, 0.5)', + path: [ + { index: 0, value: HIERARCHY_ROOT_KEY }, + { index: 2, value: 'C' }, + { index: 0, value: 'A' }, + ], + depth: 1, + label: 'A', + seriesIdentifiers: [{ key: 'A', specId: 'spec1' }], + keys: [], + }, + { + childId: 'B', + color: 'rgba(128, 0, 0, 0.5)', + path: [ + { index: 0, value: HIERARCHY_ROOT_KEY }, + { index: 2, value: 'C' }, + { index: 1, value: 'B' }, + ], + depth: 1, + label: 'B', + seriesIdentifiers: [{ key: 'B', specId: 'spec1' }], + keys: [], + }, + ]); + }); + + it('special case: one input, one label', () => { + MockStore.addSpecs( + [ + MockGlobalSpec.settings({ showLegend: true }), + MockSeriesSpec.sunburst({ ...specJSON, data: [{ cat1: 'A', cat2: 'A', val: 1 }] }), + ], + store, + ); + expect(computeLegendSelector(store.getState())).toEqual([ + { + childId: 'A', + color: 'rgba(128, 0, 0, 0.5)', + path: [ + { + index: 0, + value: HIERARCHY_ROOT_KEY, + }, + { + index: 0, + value: 'A', + }, + ], + depth: 0, + label: 'A', + seriesIdentifiers: [{ key: 'A', specId: 'spec1' }], + keys: [], + }, + { + childId: 'A', + color: 'rgba(128, 0, 0, 0.5)', + path: [ + { + index: 0, + value: HIERARCHY_ROOT_KEY, + }, + { + index: 0, + value: 'A', + }, + { + index: 0, + value: 'A', + }, + ], + + depth: 1, + label: 'A', + seriesIdentifiers: [{ key: 'A', specId: 'spec1' }], + keys: [], + }, + ]); + }); + + it('special case: one input, two labels', () => { + MockStore.addSpecs( + [ + MockGlobalSpec.settings({ showLegend: true }), + MockSeriesSpec.sunburst({ ...specJSON, data: [{ cat1: 'C', cat2: 'B', val: 1 }] }), + ], + store, + ); + expect(computeLegendSelector(store.getState())).toEqual([ + { + childId: 'C', + color: 'rgba(128, 0, 0, 0.5)', + path: [ + { + index: 0, + value: HIERARCHY_ROOT_KEY, + }, + { + index: 0, + value: 'C', + }, + ], + depth: 0, + label: 'C', + seriesIdentifiers: [{ key: 'C', specId: 'spec1' }], + keys: [], + }, + { + childId: 'B', + color: 'rgba(128, 0, 0, 0.5)', + path: [ + { + index: 0, + value: HIERARCHY_ROOT_KEY, + }, + { + index: 0, + value: 'C', + }, + { + index: 0, + value: 'B', + }, + ], + depth: 1, + label: 'B', + seriesIdentifiers: [{ key: 'B', specId: 'spec1' }], + keys: [], + }, + ]); + }); + + it('special case: no labels', () => { + MockStore.addSpecs( + [MockGlobalSpec.settings({ showLegend: true }), MockSeriesSpec.sunburst({ ...specJSON, data: [] })], + store, + ); + expect(getLegendItemsLabels(store.getState()).map((l) => l.label)).toEqual([]); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/partition_chart/renderer/_index.scss b/packages/osd-charts/src/chart_types/partition_chart/renderer/_index.scss new file mode 100644 index 000000000000..1ac4cfa0a97f --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/renderer/_index.scss @@ -0,0 +1,9 @@ +.echCanvasRenderer { + position: absolute; + top: 0; + left: 0; + padding: 0; + margin: 0; + border: 0; + background: transparent; +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/canvas_linear_renderers.ts b/packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/canvas_linear_renderers.ts new file mode 100644 index 000000000000..a7e976d718d6 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/canvas_linear_renderers.ts @@ -0,0 +1,119 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartId } from '../../../../state/chart_state'; +import { ShapeViewModel } from '../../layout/types/viewmodel_types'; +import { ContinuousDomainFocus } from './partition'; + +const linear = (x: number) => x; +const easeInOut = (alpha: number) => (x: number) => x ** alpha / (x ** alpha + (1 - x) ** alpha); + +const MAX_PADDING_RATIO = 0.25; + +const latestRafs: Map = new Map(); + +/** @internal */ +export function renderLinearPartitionCanvas2d( + ctx: CanvasRenderingContext2D, + dpr: number, + { + config: { sectorLineWidth: padding, width: containerWidth, height: containerHeight, animation }, + quadViewModel, + diskCenter, + width: panelWidth, + height: panelHeight, + layers, + }: ShapeViewModel, + { currentFocusX0, currentFocusX1, prevFocusX0, prevFocusX1 }: ContinuousDomainFocus, + chartId: ChartId, +) { + if (animation?.duration) { + const latestRaf = latestRafs.get(chartId); + if (latestRaf !== undefined) { + window.cancelAnimationFrame(latestRaf); + } + render(0); + const focusChanged = currentFocusX0 !== prevFocusX0 || currentFocusX1 !== prevFocusX1; + if (focusChanged) { + latestRafs.set( + chartId, + window.requestAnimationFrame((epochStartTime) => { + const anim = (t: number) => { + const unitNormalizedTime = Math.max(0, Math.min(1, (t - epochStartTime) / animation.duration)); + render(unitNormalizedTime); + if (unitNormalizedTime < 1) { + latestRafs.set(chartId, window.requestAnimationFrame(anim)); + } + }; + latestRafs.set(chartId, window.requestAnimationFrame(anim)); + }), + ); + } + } else { + render(1); + } + + function render( + logicalTime: number, + timeFunction: (time: number) => number = animation.duration + ? easeInOut(Math.min(5, animation.duration / 100)) + : linear, + ) { + const width = containerWidth * panelWidth; + const height = containerHeight * panelHeight; + const t = timeFunction(logicalTime); + const focusX0 = t * currentFocusX0 + (1 - t) * prevFocusX0 || 0; + const focusX1 = t * currentFocusX1 + (1 - t) * prevFocusX1 || 0; + const scale = containerWidth / (focusX1 - focusX0); + + ctx.save(); + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + ctx.scale(dpr, dpr); + ctx.translate(diskCenter.x, diskCenter.y); + ctx.clearRect(0, 0, width, height); + + quadViewModel.forEach(({ fillColor, x0, x1, y0px: y0, y1px: y1, dataName, textColor, depth }) => { + if (y1 - y0 <= padding) return; + + const fx0 = Math.max((x0 - focusX0) * scale, 0); + const fx1 = Math.min((x1 - focusX0) * scale, width); + + if (fx1 < 0 || fx0 > width) return; + + const formatter = layers[depth]?.nodeLabel ?? String; + const label = formatter(dataName); + const fWidth = fx1 - fx0; + const fPadding = Math.min(padding, MAX_PADDING_RATIO * fWidth); + + ctx.fillStyle = fillColor; + ctx.beginPath(); + ctx.rect(fx0 + fPadding, y0 + padding / 2, fWidth - fPadding, y1 - y0 - padding); + ctx.fill(); + if (textColor === 'transparent' || label === '' || fWidth < 4) return; + ctx.fillStyle = textColor; + ctx.save(); + ctx.clip(); // undoing a clip needs context save/restore, which is why it's wrapped in a save/restore + ctx.fillText(label, fx0 + 3 * fPadding, (y0 + y1) / 2); + ctx.restore(); + }); + + ctx.restore(); + } +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts b/packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts new file mode 100644 index 000000000000..7a2c5f5526d1 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts @@ -0,0 +1,317 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { addOpacity } from '../../../../common/color_calcs'; +import { TAU } from '../../../../common/constants'; +import { Pixels } from '../../../../common/geometry'; +import { cssFontShorthand } from '../../../../common/text_utils'; +import { renderLayers, withContext } from '../../../../renderers/canvas'; +import { Color } from '../../../../utils/common'; +import { + LinkLabelVM, + OutsideLinksViewModel, + QuadViewModel, + RowSet, + ShapeViewModel, + TextRow, +} from '../../layout/types/viewmodel_types'; +import { LinkLabelsViewModelSpec } from '../../layout/viewmodel/link_text_layout'; +import { isSunburst, panelTitleFontSize } from '../../layout/viewmodel/viewmodel'; + +// the burnout avoidance in the center of the pie +const LINE_WIDTH_MULT = 10; // border can be a maximum 1/LINE_WIDTH_MULT - th of the sector angle, otherwise the border would dominate +const TAPER_OFF_LIMIT = 50; // taper off within a radius of TAPER_OFF_LIMIT to avoid burnout in the middle of the pie when there are hundreds of pies + +function renderTextRow( + ctx: CanvasRenderingContext2D, + { fontSize, fillTextColor, rotation, verticalAlignment, leftAlign, container, clipText }: RowSet, + linkLabelTextColor: string, +) { + return (currentRow: TextRow) => { + const crx = leftAlign + ? currentRow.rowAnchorX - currentRow.maximumLength / 2 + : currentRow.rowAnchorX - (Math.cos(rotation) * currentRow.length) / 2; + const cry = -currentRow.rowAnchorY + (Math.sin(rotation) * currentRow.length) / 2; + if (!Number.isFinite(crx) || !Number.isFinite(cry)) { + return; + } + withContext(ctx, (ctx) => { + ctx.scale(1, -1); + if (clipText) { + ctx.rect(container.x0 + 1, container.y0 + 1, container.x1 - container.x0 - 2, container.y1 - container.y0 - 2); + ctx.clip(); + } + ctx.beginPath(); + ctx.translate(crx, cry); + ctx.rotate(-rotation); + ctx.fillStyle = fillTextColor ?? linkLabelTextColor; + ctx.textBaseline = verticalAlignment; + currentRow.rowWords.forEach((box) => { + ctx.font = cssFontShorthand(box, fontSize); + ctx.fillText(box.text, box.width / 2 + box.wordBeginning, 0); + }); + ctx.closePath(); + }); + // for debug use: this draws magenta boxes for where the text needs to fit + // note: `container` is a property of the RowSet, needs to be added + // withContext(ctx, (ctx) => { + // ctx.scale(1, -1); + // ctx.rotate(-rotation); + // ctx.beginPath(); + // ctx.strokeStyle = 'magenta'; + // ctx.fillStyle = 'magenta'; + // ctx.lineWidth = 1; + // ctx.rect(container.x0 + 1, container.y0 + 1, container.x1 - container.x0 - 2, container.y1 - container.y0 - 2); + // ctx.stroke(); + // }); + }; +} + +function renderTextRows(ctx: CanvasRenderingContext2D, rowSet: RowSet, linkLabelTextColor: string) { + rowSet.rows.forEach(renderTextRow(ctx, rowSet, linkLabelTextColor)); +} + +function renderRowSets(ctx: CanvasRenderingContext2D, rowSets: RowSet[], linkLabelTextColor: string) { + rowSets.forEach((rowSet: RowSet) => renderTextRows(ctx, rowSet, linkLabelTextColor)); +} + +function renderTaperedBorder( + ctx: CanvasRenderingContext2D, + { strokeWidth, strokeStyle, fillColor, x0, x1, y0px, y1px }: QuadViewModel, +) { + const X0 = x0 - TAU / 4; + const X1 = x1 - TAU / 4; + ctx.fillStyle = fillColor; + ctx.beginPath(); + // only draw circular arcs if it would be distinguishable from a straight line ie. angle is not very small + ctx.arc(0, 0, y0px, X0, X0); + ctx.arc(0, 0, y1px, X0, X1, false); + ctx.arc(0, 0, y0px, X1, X0, true); + + ctx.fill(); + if (strokeWidth > 0.001 && !(x0 === 0 && x1 === TAU)) { + // canvas2d uses a default of 1 if the lineWidth is assigned 0, so we use a small value to test, to avoid it + // ... and also don't draw a separator if we have a single sector that's the full ring (eg. single-fact-row pie) + // outer arc + ctx.lineWidth = strokeWidth; + const tapered = x1 - x0 < (15 * TAU) / 360; // burnout seems visible, and tapering invisible, with less than 15deg + if (tapered) { + ctx.beginPath(); + ctx.arc(0, 0, y1px, X0, X1, false); + ctx.stroke(); + + // inner arc + ctx.beginPath(); + ctx.arc(0, 0, y0px, X1, X0, true); + ctx.stroke(); + + ctx.fillStyle = strokeStyle; + + // each side (radial 'line') is modeled as a pentagon (some lines can be short arcs though) + ctx.beginPath(); + const yThreshold = Math.max(TAPER_OFF_LIMIT, (LINE_WIDTH_MULT * strokeWidth) / (X1 - X0)); + const beta = strokeWidth / yThreshold; // angle where strokeWidth equals the lineWidthMult limit at a radius of yThreshold + ctx.arc(0, 0, y0px, X0, X0 + beta * (yThreshold / y0px)); + ctx.arc(0, 0, Math.min(yThreshold, y1px), X0 + beta, X0 + beta); + ctx.arc(0, 0, y1px, X0 + beta * (yThreshold / y1px), X0, true); + ctx.arc(0, 0, y0px, X0, X0); + ctx.fill(); + } else { + ctx.strokeStyle = strokeStyle; + ctx.stroke(); + } + } +} + +function renderSectors(ctx: CanvasRenderingContext2D, quadViewModel: QuadViewModel[]) { + withContext(ctx, (ctx) => { + ctx.scale(1, -1); // D3 and Canvas2d use a left-handed coordinate system (+y = down) but the ViewModel uses +y = up, so we must locally invert Y + quadViewModel.forEach((quad: QuadViewModel) => { + if (quad.x0 === quad.x1) return; // no slice will be drawn, and it avoids some division by zero as well + renderTaperedBorder(ctx, quad); + }); + }); +} + +function renderRectangles(ctx: CanvasRenderingContext2D, quadViewModel: QuadViewModel[]) { + withContext(ctx, (ctx) => { + ctx.scale(1, -1); // D3 and Canvas2d use a left-handed coordinate system (+y = down) but the ViewModel uses +y = up, so we must locally invert Y + quadViewModel.forEach(({ strokeWidth, fillColor, x0, x1, y0px, y1px }) => { + // only draw a shape if it would show up at all + if (x1 - x0 >= 1 && y1px - y0px >= 1) { + ctx.fillStyle = fillColor; + ctx.beginPath(); + ctx.moveTo(x0, y0px); + ctx.lineTo(x0, y1px); + ctx.lineTo(x1, y1px); + ctx.lineTo(x1, y0px); + ctx.lineTo(x0, y0px); + ctx.fill(); + if (strokeWidth > 0.001) { + // Canvas2d stroke ignores an exact zero line width + ctx.lineWidth = strokeWidth; + ctx.stroke(); + } + } + }); + }); +} + +function renderFillOutsideLinks( + ctx: CanvasRenderingContext2D, + outsideLinksViewModel: OutsideLinksViewModel[], + linkLabelTextColor: string, + linkLabelLineWidth: Pixels, +) { + withContext(ctx, (ctx) => { + ctx.lineWidth = linkLabelLineWidth; + ctx.strokeStyle = linkLabelTextColor; + outsideLinksViewModel.forEach(({ points }) => { + ctx.beginPath(); + ctx.moveTo(points[0][0], points[0][1]); + for (let i = 1; i < points.length; i++) { + ctx.lineTo(points[i][0], points[i][1]); + } + ctx.stroke(); + }); + }); +} + +function renderLinkLabels( + ctx: CanvasRenderingContext2D, + linkLabelFontSize: Pixels, + linkLabelLineWidth: Pixels, + { linkLabels, labelFontSpec, valueFontSpec, strokeColor }: LinkLabelsViewModelSpec, + linkLineColor: Color, +) { + const labelColor = addOpacity(labelFontSpec.textColor, labelFontSpec.textOpacity); + const valueColor = addOpacity(valueFontSpec.textColor, valueFontSpec.textOpacity); + const labelValueGap = linkLabelFontSize / 2; // one en space + withContext(ctx, (ctx) => { + ctx.lineWidth = linkLabelLineWidth; + linkLabels.forEach(({ linkLabels, translate, textAlign, text, valueText, width, valueWidth }: LinkLabelVM) => { + // label lines + ctx.beginPath(); + ctx.moveTo(...linkLabels[0]); + linkLabels.slice(1).forEach((point) => ctx.lineTo(...point)); + ctx.strokeStyle = strokeColor ?? linkLineColor; + + ctx.stroke(); + withContext(ctx, (ctx) => { + ctx.translate(...translate); + ctx.scale(1, -1); // flip for text rendering not to be upside down + ctx.textAlign = textAlign; + // label text + ctx.strokeStyle = labelColor; + ctx.fillStyle = labelColor; + ctx.font = `${labelFontSpec.fontStyle} ${labelFontSpec.fontVariant} ${labelFontSpec.fontWeight} ${linkLabelFontSize}px ${labelFontSpec.fontFamily}`; + ctx.fillText(text, textAlign === 'right' ? -valueWidth - labelValueGap : 0, 0); + // value text + ctx.strokeStyle = valueColor; + ctx.fillStyle = valueColor; + ctx.font = `${valueFontSpec.fontStyle} ${valueFontSpec.fontVariant} ${valueFontSpec.fontWeight} ${linkLabelFontSize}px ${valueFontSpec.fontFamily}`; + ctx.fillText(valueText, textAlign === 'left' ? width + labelValueGap : 0, 0); + }); + }); + }); +} + +const midlineOffset = 0.35; // 0.35 is a [common constant](http://tavmjong.free.fr/SVG/TEXT_IN_A_BOX/index.html) representing half height +const innerPad = midlineOffset * panelTitleFontSize; // todo replace it with theme.axisPanelTitle.padding.inner + +/** @internal */ +export function renderPartitionCanvas2d( + ctx: CanvasRenderingContext2D, + dpr: number, + { + width, + height, + panelTitle, + config, + quadViewModel, + rowSets, + outsideLinksViewModel, + linkLabelViewModels, + diskCenter, + outerRadius, + }: ShapeViewModel, +) { + const { sectorLineWidth, sectorLineStroke, linkLabel } = config; + + const linkLineColor = addOpacity(linkLabel.textColor, linkLabel.textOpacity); + + withContext(ctx, (ctx) => { + // set some defaults for the overall rendering + + // let's set the devicePixelRatio once and for all; then we'll never worry about it again + ctx.scale(dpr, dpr); + + // all texts are currently center-aligned because + // - the calculations manually compute and lay out text (word) boxes, so we can choose whatever + // - but center/middle has mathematical simplicity and the most unassuming thing + // - due to using the math x/y convention (+y is up) while Canvas uses screen convention (+y is down) + // text rendering must be y-flipped, which is a bit easier this way + ctx.textAlign = 'center'; + ctx.textBaseline = 'bottom'; + + // panel titles + ctx.fillText( + panelTitle, + isSunburst(config.partitionLayout) ? diskCenter.x : diskCenter.x + (config.width * width) / 2, + isSunburst(config.partitionLayout) + ? config.linkLabel.maxCount > 0 + ? diskCenter.y - (config.height * height) / 2 + panelTitleFontSize + : diskCenter.y - outerRadius - innerPad + : diskCenter.y + 12, + ); + + ctx.textBaseline = 'middle'; + + ctx.translate(diskCenter.x, diskCenter.y); + // this applies the mathematical x/y conversion (+y is North) which is easier when developing geometry + // functions - also, all renderers have flexibility (eg. SVG scale) and WebGL NDC is also +y up + // - in any case, it's possible to refactor for a -y = North convention if that's deemed preferable + ctx.scale(1, -1); + + ctx.lineJoin = 'round'; + ctx.strokeStyle = sectorLineStroke; + ctx.lineWidth = sectorLineWidth; + + // painter's algorithm, like that of SVG: the sequence determines what overdraws what; first element of the array is drawn first + // (of course, with SVG, it's for ambiguous situations only, eg. when 3D transforms with different Z values aren't used, but + // unlike SVG and esp. WebGL, Canvas2d doesn't support the 3rd dimension well, see ctx.transform / ctx.setTransform). + // The layers are callbacks, because of the need to not bake in the `ctx`, it feels more composable and uncoupled this way. + renderLayers(ctx, [ + // bottom layer: sectors (pie slices, ring sectors etc.) + (ctx: CanvasRenderingContext2D) => + isSunburst(config.partitionLayout) ? renderSectors(ctx, quadViewModel) : renderRectangles(ctx, quadViewModel), + + // all the fill-based, potentially multirow text, whether inside or outside the sector + (ctx: CanvasRenderingContext2D) => renderRowSets(ctx, rowSets, linkLineColor), + + // the link lines for the outside-fill text + (ctx: CanvasRenderingContext2D) => + renderFillOutsideLinks(ctx, outsideLinksViewModel, linkLineColor, linkLabel.lineWidth), + + // all the text and link lines for single-row outside texts + (ctx: CanvasRenderingContext2D) => + renderLinkLabels(ctx, linkLabel.fontSize, linkLabel.lineWidth, linkLabelViewModels, linkLineColor), + ]); + }); +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/partition.tsx b/packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/partition.tsx new file mode 100644 index 000000000000..0477f1cc72b9 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/partition.tsx @@ -0,0 +1,241 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { MouseEvent, RefObject } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; + +import { ScreenReaderSummary } from '../../../../components/accessibility'; +import { clearCanvas } from '../../../../renderers/canvas'; +import { onChartRendered } from '../../../../state/actions/chart'; +import { ChartId, GlobalChartState } from '../../../../state/chart_state'; +import { + A11ySettings, + DEFAULT_A11Y_SETTINGS, + getA11ySettingsSelector, +} from '../../../../state/selectors/get_accessibility_config'; +import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; +import { Dimensions } from '../../../../utils/dimensions'; +import { MODEL_KEY } from '../../layout/config'; +import { + nullShapeViewModel, + QuadViewModel, + ShapeViewModel, + SmallMultiplesDescriptors, +} from '../../layout/types/viewmodel_types'; +import { INPUT_KEY } from '../../layout/utils/group_by_rollup'; +import { isSimpleLinear } from '../../layout/viewmodel/viewmodel'; +import { partitionDrilldownFocus, partitionMultiGeometries } from '../../state/selectors/geometries'; +import { renderLinearPartitionCanvas2d } from './canvas_linear_renderers'; +import { renderPartitionCanvas2d } from './canvas_renderers'; + +/** @internal */ +export interface ContinuousDomainFocus { + currentFocusX0: number; + currentFocusX1: number; + prevFocusX0: number; + prevFocusX1: number; +} + +/** @internal */ +export interface IndexedContinuousDomainFocus extends ContinuousDomainFocus, SmallMultiplesDescriptors {} + +interface ReactiveChartStateProps { + initialized: boolean; + geometries: ShapeViewModel; + geometriesFoci: ContinuousDomainFocus[]; + multiGeometries: ShapeViewModel[]; + chartContainerDimensions: Dimensions; + chartId: ChartId; + a11ySettings: A11ySettings; +} + +interface ReactiveChartDispatchProps { + onChartRendered: typeof onChartRendered; +} +interface ReactiveChartOwnProps { + forwardStageRef: RefObject; +} + +type PartitionProps = ReactiveChartStateProps & ReactiveChartDispatchProps & ReactiveChartOwnProps; + +class PartitionComponent extends React.Component { + static displayName = 'Partition'; + + // firstRender = true; // this will be useful for stable resizing of treemaps + private ctx: CanvasRenderingContext2D | null; + + // see example https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#Example + private readonly devicePixelRatio: number; // fixme this be no constant: multi-monitor window drag may necessitate modifying the `` dimensions + + constructor(props: Readonly) { + super(props); + this.ctx = null; + this.devicePixelRatio = window.devicePixelRatio; + } + + componentDidMount() { + /* + * the DOM element has just been appended, and getContext('2d') is always non-null, + * so we could use a couple of ! non-null assertions but no big plus + */ + this.tryCanvasContext(); + if (this.props.initialized) { + this.drawCanvas(); + this.props.onChartRendered(); + } + } + + componentDidUpdate() { + if (!this.ctx) { + this.tryCanvasContext(); + } + if (this.props.initialized) { + this.drawCanvas(); + this.props.onChartRendered(); + } + } + + handleMouseMove(e: MouseEvent) { + const { + initialized, + chartContainerDimensions: { width, height }, + forwardStageRef, + } = this.props; + if (!forwardStageRef.current || !this.ctx || !initialized || width === 0 || height === 0) { + return; + } + const picker = this.props.geometries.pickQuads; + const focus = this.props.geometriesFoci[0]; + const box = forwardStageRef.current.getBoundingClientRect(); + const { diskCenter } = this.props.geometries; + const x = e.clientX - box.left - diskCenter.x; + const y = e.clientY - box.top - diskCenter.y; + const pickedShapes: Array = picker(x, y, focus); + const datumIndices = new Set(); + pickedShapes.forEach((shape) => { + const node = shape[MODEL_KEY]; + const shapeNode = node.children.find(([key]) => key === shape.dataName); + if (shapeNode) { + const indices = shapeNode[1][INPUT_KEY] || []; + indices.forEach((i) => datumIndices.add(i)); + } + }); + + return pickedShapes; // placeholder + } + + render() { + const { + forwardStageRef, + initialized, + chartContainerDimensions: { width, height }, + a11ySettings, + } = this.props; + if (!initialized || width === 0 || height === 0) { + return null; + } + return ( +
+ + + +
+ ); + } + + private drawCanvas() { + if (this.ctx) { + const { width, height }: Dimensions = this.props.chartContainerDimensions; + clearCanvas(this.ctx, width * this.devicePixelRatio, height * this.devicePixelRatio); + const { + ctx, + devicePixelRatio, + props: { multiGeometries, geometriesFoci, chartId }, + } = this; + multiGeometries.forEach((geometries, geometryIndex) => { + const renderer = isSimpleLinear(geometries.config, geometries.layers) + ? renderLinearPartitionCanvas2d + : renderPartitionCanvas2d; + renderer(ctx, devicePixelRatio, geometries, geometriesFoci[geometryIndex], chartId); + }); + } + } + + private tryCanvasContext() { + const canvas = this.props.forwardStageRef.current; + this.ctx = canvas && canvas.getContext('2d'); + } +} + +const mapDispatchToProps = (dispatch: Dispatch): ReactiveChartDispatchProps => + bindActionCreators( + { + onChartRendered, + }, + dispatch, + ); + +const DEFAULT_PROPS: ReactiveChartStateProps = { + initialized: false, + chartId: '', + geometries: nullShapeViewModel(), + geometriesFoci: [], + multiGeometries: [], + chartContainerDimensions: { + width: 0, + height: 0, + left: 0, + top: 0, + }, + a11ySettings: DEFAULT_A11Y_SETTINGS, +}; + +const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return DEFAULT_PROPS; + } + const multiGeometries = partitionMultiGeometries(state); + return { + initialized: true, + geometries: multiGeometries.length > 0 ? multiGeometries[0] : nullShapeViewModel(), + multiGeometries, + chartContainerDimensions: getChartContainerDimensionsSelector(state), + geometriesFoci: partitionDrilldownFocus(state), + chartId: getChartIdSelector(state), + a11ySettings: getA11ySettingsSelector(state), + }; +}; + +/** @internal */ +export const Partition = connect(mapStateToProps, mapDispatchToProps)(PartitionComponent); diff --git a/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter.tsx b/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter.tsx new file mode 100644 index 000000000000..282194a954f1 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter.tsx @@ -0,0 +1,312 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { TAU } from '../../../../common/constants'; +import { PointObject } from '../../../../common/geometry'; +import { Dimensions } from '../../../../utils/dimensions'; +import { configMetadata } from '../../layout/config'; +import { PartitionLayout } from '../../layout/types/config_types'; +import { + nullPartitionSmallMultiplesModel, + PartitionSmallMultiplesModel, + QuadViewModel, + ShapeViewModel, +} from '../../layout/types/viewmodel_types'; +import { isSunburst, isTreemap, isMosaic } from '../../layout/viewmodel/viewmodel'; +import { ContinuousDomainFocus, IndexedContinuousDomainFocus } from '../canvas/partition'; + +interface HighlightSet extends PartitionSmallMultiplesModel { + geometries: QuadViewModel[]; + geometriesFoci: ContinuousDomainFocus[]; + diskCenter: PointObject; + outerRadius: number; + partitionLayout: PartitionLayout; +} + +/** @internal */ +export interface HighlighterProps { + chartId: string; + initialized: boolean; + canvasDimension: Dimensions; + renderAsOverlay: boolean; + highlightSets: HighlightSet[]; +} + +const EPSILON = 1e-6; + +interface SVGStyle { + color?: string; + fillClassName?: string; + strokeClassName?: string; +} + +/** + * This function return an SVG arc path from the same parameters of the canvas.arc function call + * @param x The horizontal coordinate of the arc's center + * @param y The vertical coordinate of the arc's center + * @param r The arc's radius. Must be positive + * @param a0 The angle at which the arc starts in radians, measured from the positive x-axis + * @param a1 The angle at which the arc ends in radians, measured from the positive x-axis + * @param ccw If true, draws the arc counter-clockwise between the start and end angles + */ +function getSectorShapeFromCanvasArc(x: number, y: number, r: number, a0: number, a1: number, ccw: boolean): string { + const cw = ccw ? 0 : 1; + const diff = a1 - a0; + const direction = ccw ? -1 : 1; + return `A${r},${r},0,${+(direction * diff >= Math.PI)},${cw},${x + r * Math.cos(a1)},${y + r * Math.sin(a1)}`; +} + +/** + * Renders an SVG Rect from a partition chart QuadViewModel + */ +function renderRectangles( + geometry: QuadViewModel, + key: string, + style: SVGStyle, + { currentFocusX0, currentFocusX1 }: ContinuousDomainFocus, + width: number, +) { + const { x0, x1, y0px, y1px } = geometry; + const props = style.color ? { fill: style.color } : { className: style.fillClassName }; + const scale = width / (currentFocusX1 - currentFocusX0); + const fx0 = Math.max((x0 - currentFocusX0) * scale, 0); + const fx1 = Math.min((x1 - currentFocusX0) * scale, width); + return ; +} + +/** + * Render an SVG path or circle from a partition chart QuadViewModel + */ +function renderSector(geometry: QuadViewModel, key: string, { color, fillClassName, strokeClassName }: SVGStyle) { + const { x0, x1, y0px, y1px } = geometry; + if ((Math.abs(x0 - x1) + TAU) % TAU < EPSILON) { + const props = + y0px === 0 + ? { + key, + r: y1px, + stroke: 'none', + ...(color ? { fill: color } : { className: fillClassName }), + } + : { + key, + r: (y0px + y1px) / 2, + strokeWidth: y1px - y0px, + fill: 'none', + ...(color ? { stroke: color } : { className: strokeClassName }), + }; + return ; + } + const X0 = x0 - TAU / 4; + const X1 = x1 - TAU / 4; + const path = [ + `M${y0px * Math.cos(X0)},${y0px * Math.sin(X0)}`, + getSectorShapeFromCanvasArc(0, 0, y0px, X0, X1, false), + `L${y1px * Math.cos(X1)},${y1px * Math.sin(X1)}`, + getSectorShapeFromCanvasArc(0, 0, y1px, X1, X0, true), + 'Z', + ].join(' '); + const props = color ? { fill: color } : { className: fillClassName }; + return ; +} + +function renderGeometries( + geoms: QuadViewModel[], + partitionLayout: PartitionLayout, + style: SVGStyle, + foci: ContinuousDomainFocus[], + width: number, +) { + const maxDepth = geoms.reduce((acc, geom) => Math.max(acc, geom.depth), 0); + // we should render only the deepest geometries of the tree to avoid overlaying highlighted geometries + const highlightedGeoms = + isTreemap(partitionLayout) || isMosaic(partitionLayout) ? geoms.filter((g) => g.depth >= maxDepth) : geoms; + const renderGeom = isSunburst(partitionLayout) ? renderSector : renderRectangles; + return highlightedGeoms.map((geometry, index) => + renderGeom( + geometry, + `${index}`, + style, + foci[0] ?? { + currentFocusX0: NaN, + currentFocusX1: NaN, + prevFocusX0: NaN, + prevFocusX1: NaN, + }, + width, + ), + ); +} + +/** @internal */ +export class HighlighterComponent extends React.Component { + static displayName = 'Highlighter'; + + renderAsMask() { + const { + chartId, + canvasDimension: { width }, + highlightSets, + } = this.props; + + const maskId = (ind: number, ind2: number) => `echHighlighterMask__${chartId}__${ind}__${ind2}`; + + const someGeometriesHighlighted = highlightSets.some(({ geometries }) => geometries.length > 0); + const renderedHighlightSet = someGeometriesHighlighted ? highlightSets : []; + + return ( + <> + + {renderedHighlightSet + .filter(({ geometries }) => geometries.length > 0) + .map( + ({ + geometries, + geometriesFoci, + diskCenter, + index, + innerIndex, + partitionLayout, + marginLeftPx, + marginTopPx, + panelInnerWidth, + panelInnerHeight, + }) => ( + + + + {renderGeometries(geometries, partitionLayout, { color: 'black' }, geometriesFoci, width)} + + + ), + )} + + {renderedHighlightSet.map( + ({ + diskCenter, + outerRadius, + index, + innerIndex, + partitionLayout, + marginLeftPx, + marginTopPx, + panelInnerWidth, + panelInnerHeight, + }) => + isSunburst(partitionLayout) ? ( + + ) : ( + + ), + )} + + ); + } + + renderAsOverlay() { + const { + canvasDimension: { width }, + } = this.props; + return this.props.highlightSets + .filter(({ geometries }) => geometries.length > 0) + .map(({ index, innerIndex, partitionLayout, geometries, diskCenter, geometriesFoci }) => ( + + {renderGeometries( + geometries, + partitionLayout, + { + fillClassName: 'echHighlighterOverlay__fill', + strokeClassName: 'echHighlighterOverlay__stroke', + }, + geometriesFoci, + width, + )} + + )); + } + + render() { + return ( + + {this.props.renderAsOverlay ? this.renderAsOverlay() : this.renderAsMask()} + + ); + } +} + +/** @internal */ +export const DEFAULT_PROPS: HighlighterProps = { + chartId: 'empty', + initialized: false, + renderAsOverlay: false, + canvasDimension: { + width: 0, + height: 0, + left: 0, + top: 0, + }, + highlightSets: [ + { + ...nullPartitionSmallMultiplesModel(configMetadata.partitionLayout.dflt), + geometries: [], + geometriesFoci: [], + diskCenter: { + x: 0, + y: 0, + }, + outerRadius: 10, + }, + ], +}; + +/** @internal */ +export function highlightSetMapper(geometries: QuadViewModel[], foci: IndexedContinuousDomainFocus[]) { + return (vm: ShapeViewModel): HighlightSet => { + const { index } = vm; + const { innerIndex } = vm; + return { + ...vm, + geometries: geometries.filter(({ index: i, innerIndex: ii }) => index === i && innerIndex === ii), + geometriesFoci: foci.filter(({ index: i, innerIndex: ii }) => index === i && innerIndex === ii), + }; + }; +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter_hover.tsx b/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter_hover.tsx new file mode 100644 index 000000000000..bd573e4f4aa4 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter_hover.tsx @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { connect } from 'react-redux'; + +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions'; +import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; +import { partitionDrilldownFocus, partitionMultiGeometries } from '../../state/selectors/geometries'; +import { getPickedShapes } from '../../state/selectors/picked_shapes'; +import { DEFAULT_PROPS, HighlighterComponent, HighlighterProps, highlightSetMapper } from './highlighter'; + +const hoverMapStateToProps = (state: GlobalChartState): HighlighterProps => { + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return DEFAULT_PROPS; + } + + const canvasDimension = getChartContainerDimensionsSelector(state); + const { chartId } = state; + + const allGeometries = partitionMultiGeometries(state); // .filter((g) => g.index === 0 && g.innerIndex === 0); + const geometriesFoci = partitionDrilldownFocus(state); + const pickedGeometries = getPickedShapes(state); + + const highlightSets = allGeometries.map(highlightSetMapper(pickedGeometries, geometriesFoci)); + + return { + chartId, + initialized: true, + renderAsOverlay: true, + canvasDimension, + highlightSets, + }; +}; + +/** + * Partition chart highlighter from mouse hover events + * @internal + */ +export const HighlighterFromHover = connect(hoverMapStateToProps)(HighlighterComponent); diff --git a/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter_legend.tsx b/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter_legend.tsx new file mode 100644 index 000000000000..a7cfb2b998df --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter_legend.tsx @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { connect } from 'react-redux'; + +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions'; +import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; +import { partitionDrilldownFocus, partitionMultiGeometries } from '../../state/selectors/geometries'; +import { legendHoverHighlightNodes } from '../../state/selectors/get_highlighted_shapes'; +import { HighlighterComponent, HighlighterProps, DEFAULT_PROPS, highlightSetMapper } from './highlighter'; + +const legendMapStateToProps = (state: GlobalChartState): HighlighterProps => { + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return DEFAULT_PROPS; + } + + const { chartId } = state; + + const geometries = legendHoverHighlightNodes(state); + const geometriesFoci = partitionDrilldownFocus(state); + const canvasDimension = getChartContainerDimensionsSelector(state); + const multiGeometries = partitionMultiGeometries(state); + const highlightMapper = highlightSetMapper(geometries, geometriesFoci); + const highlightSets = multiGeometries.map(highlightMapper); + + return { + chartId, + initialized: true, + renderAsOverlay: false, + canvasDimension, + highlightSets, + }; +}; + +/** + * Partition chart highlighter from legend events + * @internal + */ +export const HighlighterFromLegend = connect(legendMapStateToProps)(HighlighterComponent); diff --git a/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/layered_partition_chart.tsx b/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/layered_partition_chart.tsx new file mode 100644 index 000000000000..e155a0d673e2 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/layered_partition_chart.tsx @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { RefObject } from 'react'; + +import { Tooltip } from '../../../../components/tooltip'; +import { BackwardRef } from '../../../../state/chart_state'; +import { Partition } from '../canvas/partition'; +import { HighlighterFromHover } from './highlighter_hover'; +import { HighlighterFromLegend } from './highlighter_legend'; + +/** @internal */ +export function render(containerRef: BackwardRef, forwardStageRef: RefObject) { + return ( + <> + + + + + + ); +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/specs/index.ts b/packages/osd-charts/src/chart_types/partition_chart/specs/index.ts new file mode 100644 index 000000000000..744bf907a529 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/specs/index.ts @@ -0,0 +1,110 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { ChartType } from '../..'; +import { Pixels } from '../../../common/geometry'; +import { Spec } from '../../../specs'; +import { SpecType } from '../../../specs/constants'; // kept as unshortened import on separate line otherwise import circularity emerges +import { getConnect, specComponentFactory } from '../../../state/spec_factory'; +import { IndexedAccessorFn } from '../../../utils/accessor'; +import { + Datum, + LabelAccessor, + RecursivePartial, + ShowAccessor, + ValueAccessor, + ValueFormatter, +} from '../../../utils/common'; +import { config, percentFormatter } from '../layout/config'; +import { Config, FillFontSizeRange, FillLabelConfig } from '../layout/types/config_types'; +import { NodeColorAccessor, ShapeTreeNode, ValueGetter } from '../layout/types/viewmodel_types'; +import { AGGREGATE_KEY, NodeSorter, PrimitiveValue } from '../layout/utils/group_by_rollup'; + +interface ExtendedFillLabelConfig extends FillLabelConfig, FillFontSizeRange {} + +/** + * Specification for a given layer in the partition chart + * @public + */ +export interface Layer { + groupByRollup: IndexedAccessorFn; + sortPredicate?: NodeSorter | null; + nodeLabel?: LabelAccessor; + fillLabel?: Partial; + showAccessor?: ShowAccessor; + shape?: { fillColor: string | NodeColorAccessor }; +} + +const defaultProps = { + chartType: ChartType.Partition, + specType: SpecType.Series, + config, + valueAccessor: (d: Datum) => (typeof d === 'number' ? d : 0), + valueGetter: (n: ShapeTreeNode): number => n[AGGREGATE_KEY], + valueFormatter: (d: number): string => String(d), + percentFormatter, + topGroove: 20, + smallMultiples: null, + layers: [ + { + groupByRollup: (d: Datum, i: number) => i, + nodeLabel: (d: PrimitiveValue) => String(d), + showAccessor: () => true, + fillLabel: {}, + }, + ], +}; + +/** + * Specifies the partition chart + * @public + */ +export interface PartitionSpec extends Spec { + specType: typeof SpecType.Series; + chartType: typeof ChartType.Partition; + config: RecursivePartial; + data: Datum[]; + valueAccessor: ValueAccessor; + valueFormatter: ValueFormatter; + valueGetter: ValueGetter; + percentFormatter: ValueFormatter; + topGroove: Pixels; + smallMultiples: string | null; + layers: Layer[]; +} + +type SpecRequiredProps = Pick; +type SpecOptionalProps = Partial>; + +/** @public */ +export const Partition: React.FunctionComponent = getConnect()( + specComponentFactory< + PartitionSpec, + | 'valueAccessor' + | 'valueGetter' + | 'valueFormatter' + | 'layers' + | 'config' + | 'percentFormatter' + | 'topGroove' + | 'smallMultiples' + >(defaultProps), +); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/chart_state.tsx b/packages/osd-charts/src/chart_types/partition_chart/state/chart_state.tsx new file mode 100644 index 000000000000..5efd88edfcb1 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/chart_state.tsx @@ -0,0 +1,144 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RefObject } from 'react'; + +import { ChartType } from '../..'; +import { DEFAULT_CSS_CURSOR } from '../../../common/constants'; +import { BackwardRef, GlobalChartState, InternalChartState } from '../../../state/chart_state'; +import { InitStatus } from '../../../state/selectors/get_internal_is_intialized'; +import { DebugState } from '../../../state/types'; +import { Dimensions } from '../../../utils/dimensions'; +import { render } from '../renderer/dom/layered_partition_chart'; +import { computeLegendSelector } from './selectors/compute_legend'; +import { getChartTypeDescriptionSelector } from './selectors/get_chart_type_description'; +import { getDebugStateSelector } from './selectors/get_debug_state'; +import { getLegendItemsExtra } from './selectors/get_legend_items_extra'; +import { getLegendItemsLabels } from './selectors/get_legend_items_labels'; +import { isTooltipVisibleSelector } from './selectors/is_tooltip_visible'; +import { createOnElementClickCaller } from './selectors/on_element_click_caller'; +import { createOnElementOutCaller } from './selectors/on_element_out_caller'; +import { createOnElementOverCaller } from './selectors/on_element_over_caller'; +import { getPartitionSpec } from './selectors/partition_spec'; +import { getTooltipInfoSelector } from './selectors/tooltip'; + +/** @internal */ +export class PartitionState implements InternalChartState { + chartType = ChartType.Partition; + + onElementClickCaller: (state: GlobalChartState) => void; + + onElementOverCaller: (state: GlobalChartState) => void; + + onElementOutCaller: (state: GlobalChartState) => void; + + constructor() { + this.onElementClickCaller = createOnElementClickCaller(); + this.onElementOverCaller = createOnElementOverCaller(); + this.onElementOutCaller = createOnElementOutCaller(); + } + + isInitialized(globalState: GlobalChartState) { + return getPartitionSpec(globalState) !== null ? InitStatus.Initialized : InitStatus.SpecNotInitialized; + } + + isBrushAvailable() { + return false; + } + + isBrushing() { + return false; + } + + isChartEmpty() { + return false; + } + + getLegendItemsLabels(globalState: GlobalChartState) { + // order doesn't matter, but it needs to return the highest depth of the label occurrence so enough horizontal width is allocated + return getLegendItemsLabels(globalState); + } + + getLegendItems(globalState: GlobalChartState) { + return computeLegendSelector(globalState); + } + + getLegendExtraValues(globalState: GlobalChartState) { + return getLegendItemsExtra(globalState); + } + + chartRenderer(containerRef: BackwardRef, forwardStageRef: RefObject) { + return render(containerRef, forwardStageRef); + } + + getPointerCursor() { + return DEFAULT_CSS_CURSOR; + } + + isTooltipVisible(globalState: GlobalChartState) { + return { + visible: isTooltipVisibleSelector(globalState), + isExternal: false, + }; + } + + getTooltipInfo(globalState: GlobalChartState) { + return getTooltipInfoSelector(globalState); + } + + getTooltipAnchor(state: GlobalChartState) { + const { position } = state.interactions.pointer.current; + return { + isRotated: false, + x: position.x, + width: 0, + y: position.y, + height: 0, + }; + } + + eventCallbacks(globalState: GlobalChartState) { + this.onElementOverCaller(globalState); + this.onElementOutCaller(globalState); + this.onElementClickCaller(globalState); + } + + // TODO + getProjectionContainerArea(): Dimensions { + return { width: 0, height: 0, top: 0, left: 0 }; + } + + // TODO + getMainProjectionArea(): Dimensions { + return { width: 0, height: 0, top: 0, left: 0 }; + } + + // TODO + getBrushArea(): Dimensions | null { + return null; + } + + getDebugState(state: GlobalChartState): DebugState { + return getDebugStateSelector(state); + } + + getChartTypeDescription(state: GlobalChartState): string { + return getChartTypeDescriptionSelector(state); + } +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/__snapshots__/get_legend_items_extra.test.ts.snap b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/__snapshots__/get_legend_items_extra.test.ts.snap new file mode 100644 index 000000000000..7b7af039300f --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/__snapshots__/get_legend_items_extra.test.ts.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Partition - Legend item extra values should return all extra values in nested legend 1`] = `Object {}`; + +exports[`Partition - Legend item extra values should return extra values in nested legend within max depth of 1 1`] = `Object {}`; + +exports[`Partition - Legend item extra values should return extra values in nested legend within max depth of 2 1`] = `Object {}`; diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts new file mode 100644 index 000000000000..4a9bf1a72b86 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { LegendItem } from '../../../../common/legend'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getLegendConfigSelector } from '../../../../state/selectors/get_legend_config_selector'; +import { getLegendItems } from '../../layout/utils/legend'; +import { partitionMultiGeometries } from './geometries'; +import { getPartitionSpecs } from './get_partition_specs'; + +/** @internal */ +export const computeLegendSelector = createCachedSelector( + [getPartitionSpecs, getLegendConfigSelector, partitionMultiGeometries], + (specs, { flatLegend, legendMaxDepth, legendPosition }, geometries): LegendItem[] => + specs.flatMap((partitionSpec, i) => { + const quadViewModel = geometries.filter((g) => g.index === i).flatMap((g) => g.quadViewModel); + return getLegendItems( + partitionSpec.id, + partitionSpec.layers, + flatLegend, + legendMaxDepth, + legendPosition, + quadViewModel, + ); + }), +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/drilldown_active.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/drilldown_active.ts new file mode 100644 index 000000000000..99dce0081fd9 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/drilldown_active.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { isSimpleLinear } from '../../layout/viewmodel/viewmodel'; +import { getPartitionSpecs } from './partition_spec'; + +/** @internal */ +export const drilldownActive = createCachedSelector([getPartitionSpecs], (specs) => { + return specs.length === 1 && isSimpleLinear(specs[0].config, specs[0].layers); // singleton! +})((state) => state.chartId); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/geometries.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/geometries.ts new file mode 100644 index 000000000000..0a81254ec21c --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/geometries.ts @@ -0,0 +1,247 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { ChartType } from '../../..'; +import { CategoryKey } from '../../../../common/category'; +import { Pixels, Ratio } from '../../../../common/geometry'; +import { RelativeBandsPadding, SmallMultiplesSpec, SpecType } from '../../../../specs'; +import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { getSpecs } from '../../../../state/selectors/get_settings_specs'; +import { getSpecsFromStore } from '../../../../state/utils'; +import { Dimensions } from '../../../../utils/dimensions'; +import { config } from '../../layout/config'; +import { nullShapeViewModel, QuadViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; +import { getShapeViewModel } from '../../layout/viewmodel/scenegraph'; +import { IndexedContinuousDomainFocus } from '../../renderer/canvas/partition'; +import { getPartitionSpecs } from './get_partition_specs'; +import { getTrees, StyledTree } from './tree'; + +const horizontalSplit = (s?: SmallMultiplesSpec) => s?.splitHorizontally; +const verticalSplit = (s?: SmallMultiplesSpec) => s?.splitVertically; + +function getInterMarginSize(size: Pixels, startMargin: Ratio, endMargin: Ratio) { + return size * (1 - Math.min(1, startMargin + endMargin)); +} + +function bandwidth(range: Pixels, bandCount: number, { outer, inner }: RelativeBandsPadding) { + // same convention as d3.scaleBand https://observablehq.com/@d3/d3-scaleband + return range / (2 * outer + bandCount + bandCount * inner - inner); +} + +/** @internal */ +export const partitionMultiGeometries = createCachedSelector( + [getSpecs, getPartitionSpecs, getChartContainerDimensionsSelector, getTrees, getChartThemeSelector], + (specs, partitionSpecs, parentDimensions, trees, { background }): ShapeViewModel[] => { + const smallMultiplesSpecs = getSpecsFromStore(specs, ChartType.Global, SpecType.SmallMultiples); + + // todo make it part of configuration + const outerSpecDirection = ['horizontal', 'vertical', 'zigzag'][0]; + + const innerBreakdownDirection = horizontalSplit(smallMultiplesSpecs[0]) + ? 'horizontal' + : verticalSplit(smallMultiplesSpecs[0]) + ? 'vertical' + : 'zigzag'; + + const { width: marginedWidth, height: marginedHeight } = parentDimensions; + + const outerPanelCount = partitionSpecs.length; + const zigzagColumnCount = Math.ceil(Math.sqrt(outerPanelCount)); + const zigzagRowCount = Math.ceil(outerPanelCount / zigzagColumnCount); + + const outerWidthRatio = + outerSpecDirection === 'horizontal' + ? 1 / outerPanelCount + : outerSpecDirection === 'zigzag' + ? 1 / zigzagColumnCount + : 1; + const outerHeightRatio = + outerSpecDirection === 'vertical' + ? 1 / outerPanelCount + : outerSpecDirection === 'zigzag' + ? 1 / zigzagRowCount + : 1; + + const result = partitionSpecs.flatMap((spec, index) => { + const marginLeft = spec.config.margin?.left ?? config.margin.left; + const marginRight = spec.config.margin?.right ?? config.margin.right; + const chartWidth = getInterMarginSize(marginedWidth, marginLeft, marginRight); + const marginTop = spec.config.margin?.top ?? config.margin.top; + const marginBottom = spec.config.margin?.bottom ?? config.margin.bottom; + + const chartHeight = getInterMarginSize(marginedHeight, marginTop, marginBottom); + return trees.map(({ name, smAccessorValue, style, tree: t }: StyledTree, innerIndex, a) => { + const innerPanelCount = a.length; + const outerPanelWidth = chartWidth * outerWidthRatio; + const outerPanelHeight = chartHeight * outerHeightRatio; + const outerPanelArea = outerPanelWidth * outerPanelHeight; + const innerPanelTargetArea = outerPanelArea / innerPanelCount; + const innerPanelTargetHeight = Math.sqrt(innerPanelTargetArea); // attempting squarish inner panels + + const innerZigzagRowCountEstimate = Math.max(1, Math.floor(outerPanelHeight / innerPanelTargetHeight)); // err on the side of landscape aspect ratio + const innerZigzagColumnCount = Math.ceil(a.length / innerZigzagRowCountEstimate); + const innerZigzagRowCount = Math.ceil(a.length / innerZigzagColumnCount); + const innerRowCount = + innerBreakdownDirection === 'vertical' + ? a.length + : innerBreakdownDirection === 'zigzag' + ? innerZigzagRowCount + : 1; + const innerColumnCount = + innerBreakdownDirection === 'vertical' + ? 1 + : innerBreakdownDirection === 'zigzag' + ? innerZigzagColumnCount + : a.length; + const innerRowIndex = + innerBreakdownDirection === 'vertical' + ? innerIndex + : innerBreakdownDirection === 'zigzag' + ? Math.floor(innerIndex / innerZigzagColumnCount) + : 0; + const innerColumnIndex = + innerBreakdownDirection === 'vertical' + ? 0 + : innerBreakdownDirection === 'zigzag' + ? innerIndex % innerZigzagColumnCount + : innerIndex; + const topOuterRatio = + outerSpecDirection === 'vertical' + ? index / outerPanelCount + : outerSpecDirection === 'zigzag' + ? Math.floor(index / zigzagColumnCount) / zigzagRowCount + : 0; + const topInnerRatio = + outerHeightRatio * + (innerBreakdownDirection === 'vertical' + ? innerIndex / a.length + : innerBreakdownDirection === 'zigzag' + ? Math.floor(innerIndex / innerZigzagColumnCount) / innerZigzagRowCount + : 0); + const panelHeightRatio = + outerHeightRatio * + (innerBreakdownDirection === 'vertical' + ? 1 / a.length + : innerBreakdownDirection === 'zigzag' + ? 1 / innerZigzagRowCount + : 1); + const leftOuterRatio = + outerSpecDirection === 'horizontal' + ? index / outerPanelCount + : outerSpecDirection === 'zigzag' + ? (index % zigzagColumnCount) / zigzagColumnCount + : 0; + const leftInnerRatio = + outerWidthRatio * + (innerBreakdownDirection === 'horizontal' + ? innerIndex / a.length + : innerBreakdownDirection === 'zigzag' + ? (innerIndex % innerZigzagColumnCount) / innerZigzagColumnCount + : 0); + const panelWidthRatio = + outerWidthRatio * + (innerBreakdownDirection === 'horizontal' + ? 1 / a.length + : innerBreakdownDirection === 'zigzag' + ? 1 / innerZigzagColumnCount + : 1); + + const { width, height } = parentDimensions; + + const innerWidth = getInterMarginSize(width, marginLeft, marginRight); + const innerHeight = getInterMarginSize(height, marginTop, marginBottom); + + const panelInnerWidth = bandwidth(innerWidth, innerColumnCount, style.horizontalPanelPadding); + + const panelInnerHeight = bandwidth(innerHeight, innerRowCount, style.verticalPanelPadding); + + const marginLeftPx = + width * marginLeft + + panelInnerWidth * style.horizontalPanelPadding.outer + + innerColumnIndex * (panelInnerWidth * (1 + style.horizontalPanelPadding.inner)); + const marginTopPx = + height * marginTop + + panelInnerHeight * style.verticalPanelPadding.outer + + innerRowIndex * (panelInnerHeight * (1 + style.verticalPanelPadding.inner)); + + return getShapeViewModel(spec, parentDimensions, t, background.color, style, { + index, + innerIndex, + partitionLayout: spec.config.partitionLayout ?? config.partitionLayout, + panelTitle: String(name), + smAccessorValue, + top: topOuterRatio + topInnerRatio, + height: panelHeightRatio, + left: leftOuterRatio + leftInnerRatio, + width: panelWidthRatio, + innerRowCount, + innerColumnCount, + innerRowIndex, + innerColumnIndex, + marginLeftPx, + marginTopPx, + panelInnerWidth, + panelInnerHeight, + }); + }); + }); + + return result.length === 0 ? [nullShapeViewModel(config, { x: outerWidthRatio, y: outerHeightRatio })] : result; + }, +)(getChartIdSelector); + +function focusRect(quadViewModel: QuadViewModel[], { left, width }: Dimensions, drilldown: CategoryKey[]) { + return drilldown.length === 0 + ? { x0: left, x1: left + width } + : quadViewModel.find( + ({ path }) => path.length === drilldown.length && path.every(({ value }, i) => value === drilldown[i]), + ) ?? { x0: left, x1: left + width }; +} + +/** @internal */ +export const partitionDrilldownFocus = createCachedSelector( + [ + partitionMultiGeometries, + getChartContainerDimensionsSelector, + (state) => state.interactions.drilldown, + (state) => state.interactions.prevDrilldown, + ], + (multiGeometries, chartDimensions, drilldown, prevDrilldown): IndexedContinuousDomainFocus[] => + multiGeometries.map(({ quadViewModel, smAccessorValue, index, innerIndex }) => { + const { x0: currentFocusX0, x1: currentFocusX1 } = focusRect(quadViewModel, chartDimensions, drilldown); + const { x0: prevFocusX0, x1: prevFocusX1 } = focusRect(quadViewModel, chartDimensions, prevDrilldown); + return { currentFocusX0, currentFocusX1, prevFocusX0, prevFocusX1, smAccessorValue, index, innerIndex }; + }), +)((state) => state.chartId); + +/** @internal */ +export const partitionGeometries = createCachedSelector( + [partitionMultiGeometries], + (multiGeometries: ShapeViewModel[]) => { + return [ + multiGeometries.length > 0 // singleton! + ? multiGeometries[0] + : nullShapeViewModel(), + ]; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_chart_type_description.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_chart_type_description.ts new file mode 100644 index 000000000000..f2823f16affb --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_chart_type_description.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getPartitionSpec } from './partition_spec'; + +/** @internal */ +export const getChartTypeDescriptionSelector = createCachedSelector([getPartitionSpec], (partitionSpec): string => { + return `${partitionSpec?.config.partitionLayout} chart` ?? 'Partition chart'; +})(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_debug_state.test.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_debug_state.test.ts new file mode 100644 index 000000000000..aea457bbd7df --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_debug_state.test.ts @@ -0,0 +1,143 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Store } from 'redux'; + +import { MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs/specs'; +import { MockStore } from '../../../../mocks/store/store'; +import { + HeatmapElementEvent, + LayerValue, + PartitionElementEvent, + WordCloudElementEvent, + XYChartElementEvent, +} from '../../../../specs/settings'; +import { onMouseDown, onMouseUp, onPointerMove } from '../../../../state/actions/mouse'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { DebugState, PartitionDebugState, SinglePartitionDebugState } from '../../../../state/types'; +import { PartitionLayout } from '../../layout/types/config_types'; +import { isSunburst } from '../../layout/viewmodel/viewmodel'; +import { getDebugStateSelector } from './get_debug_state'; +import { createOnElementClickCaller } from './on_element_click_caller'; + +describe.each([ + [PartitionLayout.sunburst, 9, 9], + [PartitionLayout.treemap, 9, 6], + [PartitionLayout.flame, 9, 6], + [PartitionLayout.icicle, 9, 6], + [PartitionLayout.mosaic, 9, 6], +])('Partition - debug state %s', (partitionLayout, numberOfElements, numberOfCalls) => { + type TestDatum = { cat1: string; cat2: string; val: number }; + const specJSON = { + config: { + partitionLayout, + }, + data: [ + { cat1: 'Asia', cat2: 'Japan', val: 1 }, + { cat1: 'Asia', cat2: 'China', val: 1 }, + { cat1: 'Europe', cat2: 'Germany', val: 1 }, + { cat1: 'Europe', cat2: 'Italy', val: 1 }, + { cat1: 'North America', cat2: 'United States', val: 1 }, + { cat1: 'North America', cat2: 'Canada', val: 1 }, + ], + valueAccessor: (d: TestDatum) => d.val, + layers: [ + { + groupByRollup: (d: TestDatum) => d.cat1, + }, + { + groupByRollup: (d: TestDatum) => d.cat2, + }, + ], + }; + let store: Store; + let onClickListener: jest.Mock< + undefined, + Array<(XYChartElementEvent | PartitionElementEvent | HeatmapElementEvent | WordCloudElementEvent)[]> + >; + let debugState: DebugState; + + beforeEach(() => { + onClickListener = jest.fn((): undefined => undefined); + store = MockStore.default({ width: 500, height: 500, top: 0, left: 0 }); + const onElementClickCaller = createOnElementClickCaller(); + store.subscribe(() => { + onElementClickCaller(store.getState()); + }); + MockStore.addSpecs( + [ + MockSeriesSpec.sunburst(specJSON), + MockGlobalSpec.settings({ debugState: true, onElementClick: onClickListener }), + ], + store, + ); + debugState = getDebugStateSelector(store.getState()); + }); + + it('can compute debug state', () => { + // small multiple panels + expect(debugState.partition).toHaveLength(1); + // partition sectors + expect(debugState.partition![0].partitions).toHaveLength(numberOfElements); + }); + + it('can click on every sector', () => { + const [{ partitions }] = debugState.partition as PartitionDebugState[]; + let counter = 0; + for (let index = 0; index < partitions.length; index++) { + const partition = partitions[index]; + if (!isSunburst(partitionLayout) && partition.depth < 2) { + continue; + } + expectCorrectClickInfo(store, onClickListener, partition, counter); + counter++; + } + expect(onClickListener).toBeCalledTimes(numberOfCalls); + }); +}); + +function expectCorrectClickInfo( + store: Store, + onClickListener: jest.Mock< + undefined, + Array<(XYChartElementEvent | PartitionElementEvent | HeatmapElementEvent | WordCloudElementEvent)[]> + >, + partition: SinglePartitionDebugState, + index: number, +) { + const { + depth, + value, + name, + coords: [x, y], + } = partition; + + store.dispatch(onPointerMove({ x, y }, index * 3)); + store.dispatch(onMouseDown({ x, y }, index * 3 + 1)); + store.dispatch(onMouseUp({ x, y }, index * 3 + 2)); + + expect(onClickListener).toBeCalledTimes(index + 1); + const obj = onClickListener.mock.calls[index][0][0][0] as LayerValue[]; + // pick the last element of the path + expect(obj[obj.length - 1]).toMatchObject({ + depth, + groupByRollup: name, + value, + }); +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_debug_state.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_debug_state.ts new file mode 100644 index 000000000000..21d51190d115 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_debug_state.ts @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { TAU } from '../../../../common/constants'; +import { Pixels, PointObject } from '../../../../common/geometry'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { DebugState, PartitionDebugState } from '../../../../state/types'; +import { QuadViewModel } from '../../layout/types/viewmodel_types'; +import { isSunburst } from '../../layout/viewmodel/viewmodel'; +import { partitionMultiGeometries } from './geometries'; + +/** @internal */ +export const getDebugStateSelector = createCachedSelector( + [partitionMultiGeometries], + (geoms): DebugState => { + return { + partition: geoms.reduce((acc, { panelTitle, config, quadViewModel, diskCenter }) => { + const partitions: PartitionDebugState['partitions'] = quadViewModel.map((model) => { + const { dataName, depth, fillColor, value } = model; + return { + name: dataName, + depth, + color: fillColor, + value, + coords: isSunburst(config.partitionLayout) + ? getCoordsForSector(model, diskCenter) + : getCoordsForRectangle(model, diskCenter), + }; + }); + acc.push({ + panelTitle, + partitions, + }); + return acc; + }, []), + }; + }, +)(getChartIdSelector); + +function getCoordsForSector({ x0, x1, y1px, y0px }: QuadViewModel, diskCenter: PointObject): [Pixels, Pixels] { + const X0 = x0 - TAU / 4; + const X1 = x1 - TAU / 4; + const cr = y0px + (y1px - y0px) / 2; + const angle = X0 + (X1 - X0) / 2; + const x = Math.round(Math.cos(angle) * cr + diskCenter.x); + const y = Math.round(Math.sin(angle) * cr + diskCenter.y); + return [x, y]; +} + +function getCoordsForRectangle({ x0, x1, y1px, y0px }: QuadViewModel, diskCenter: PointObject): [Pixels, Pixels] { + const y = Math.round(y0px + (y1px - y0px) / 2 + diskCenter.y); + const x = Math.round(x0 + (x1 - x0) / 2 + diskCenter.x); + return [x, y]; +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_highlighted_shapes.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_highlighted_shapes.ts new file mode 100644 index 000000000000..3621de56c37c --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_highlighted_shapes.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { QuadViewModel } from '../../layout/types/viewmodel_types'; +import { highlightedGeoms } from '../../layout/utils/highlighted_geoms'; +import { partitionMultiGeometries } from './geometries'; + +const getHighlightedLegendItemPath = (state: GlobalChartState) => state.interactions.highlightedLegendPath; + +/** @internal */ +export const legendHoverHighlightNodes = createCachedSelector( + [getSettingsSpecSelector, getHighlightedLegendItemPath, partitionMultiGeometries], + ({ legendStrategy, flatLegend }, highlightedLegendItemPath, geometries): QuadViewModel[] => { + if (highlightedLegendItemPath.length === 0) return []; + return geometries.flatMap(({ quadViewModel }) => + highlightedGeoms(legendStrategy, flatLegend, quadViewModel, highlightedLegendItemPath), + ); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_extra.test.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_extra.test.ts new file mode 100644 index 000000000000..eead0e7371eb --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_extra.test.ts @@ -0,0 +1,137 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Store } from 'redux'; + +import { MockSeriesSpec, MockGlobalSpec } from '../../../../mocks/specs'; +import { MockStore } from '../../../../mocks/store'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { PrimitiveValue } from '../../layout/utils/group_by_rollup'; +import { getLegendItemsExtra } from './get_legend_items_extra'; + +describe('Partition - Legend item extra values', () => { + type TestDatum = [string, string, string, number]; + const spec = MockSeriesSpec.sunburst({ + data: [ + ['aaa', 'aa', '1', 1], + ['aaa', 'aa', '1', 2], + ['aaa', 'aa', '3', 1], + ['aaa', 'bb', '4', 1], + ['aaa', 'bb', '5', 1], + ['aaa', 'bb', '6', 1], + ['bbb', 'aa', '7', 1], + ['bbb', 'aa', '8', 1], + ['bbb', 'bb', '9', 1], + ['bbb', 'bb', '10', 1], + ['bbb', 'cc', '11', 1], + ['bbb', 'cc', '12', 1], + ], + valueAccessor: (d: TestDatum) => d[3], + layers: [ + { + groupByRollup: (datum: TestDatum) => datum[0], + nodeLabel: (d: PrimitiveValue) => String(d), + }, + { + groupByRollup: (datum: TestDatum) => datum[1], + nodeLabel: (d: PrimitiveValue) => String(d), + }, + { + groupByRollup: (datum: TestDatum) => datum[2], + nodeLabel: (d: PrimitiveValue) => String(d), + }, + ], + }); + let store: Store; + + beforeEach(() => { + store = MockStore.default(); + }); + + it('should return all extra values in nested legend', () => { + MockStore.addSpecs([spec], store); + + const extraValues = getLegendItemsExtra(store.getState()); + expect([...extraValues.keys()]).toEqual([ + '0', + '0__0', + '0__0__0', + '0__0__0__0', + '0__0__0__1', + '0__0__1', + '0__0__1__0', + '0__0__1__1', + '0__0__1__2', + '0__1', + '0__1__0', + '0__1__0__0', + '0__1__0__1', + '0__1__1', + '0__1__1__0', + '0__1__1__1', + '0__1__2', + '0__1__2__0', + '0__1__2__1', + ]); + expect(extraValues.values()).toMatchSnapshot(); + }); + + it('should return extra values in nested legend within max depth of 1', () => { + const settings = MockGlobalSpec.settings({ legendMaxDepth: 1 }); + MockStore.addSpecs([settings, spec], store); + + const extraValues = getLegendItemsExtra(store.getState()); + expect([...extraValues.keys()]).toEqual(['0', '0__0', '0__1']); + expect(extraValues.values()).toMatchSnapshot(); + }); + + it('should return extra values in nested legend within max depth of 2', () => { + const settings = MockGlobalSpec.settings({ legendMaxDepth: 2 }); + MockStore.addSpecs([settings, spec], store); + + const extraValues = getLegendItemsExtra(store.getState()); + expect([...extraValues.keys()]).toEqual([ + '0', + '0__0', + '0__0__0', + '0__0__1', + '0__1', + '0__1__0', + '0__1__1', + '0__1__2', + ]); + expect(extraValues.values()).toMatchSnapshot(); + }); + + it('filters all extraValues if depth is 0', () => { + const settings = MockGlobalSpec.settings({ legendMaxDepth: 0 }); + MockStore.addSpecs([settings, spec], store); + + const extraValues = getLegendItemsExtra(store.getState()); + expect([...extraValues.keys()]).toEqual([]); + }); + + it('filters all extraValues if depth is NaN', () => { + const settings = MockGlobalSpec.settings({ legendMaxDepth: NaN }); + MockStore.addSpecs([settings, spec], store); + + const extraValues = getLegendItemsExtra(store.getState()); + expect([...extraValues.keys()]).toEqual([]); + }); +}); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_extra.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_extra.ts new file mode 100644 index 000000000000..007664d16251 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_extra.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { LegendItemExtraValues } from '../../../../common/legend'; +import { SeriesKey } from '../../../../common/series_id'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getExtraValueMap } from '../../layout/viewmodel/hierarchy_of_arrays'; +import { getPartitionSpec } from './partition_spec'; +import { getTrees } from './tree'; + +/** @internal */ +export const getLegendItemsExtra = createCachedSelector( + [getPartitionSpec, getSettingsSpecSelector, getTrees], + (spec, { legendMaxDepth }, trees): Map => { + return spec && !Number.isNaN(legendMaxDepth) && legendMaxDepth > 0 + ? getExtraValueMap(spec.layers, spec.valueFormatter, trees[0].tree, legendMaxDepth) // singleton! wrt inner small multiples + : new Map(); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.test.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.test.ts new file mode 100644 index 000000000000..4e44cbab56ab --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.test.ts @@ -0,0 +1,200 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Store } from 'redux'; + +import { MockSeriesSpec, MockGlobalSpec } from '../../../../mocks/specs'; +import { MockStore } from '../../../../mocks/store'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { PrimitiveValue } from '../../layout/utils/group_by_rollup'; +import { getLegendItemsLabels } from './get_legend_items_labels'; + +describe('Partition - Legend items labels', () => { + type TestDatum = [string, string, string, number]; + const spec = MockSeriesSpec.sunburst({ + data: [ + ['aaa', 'aa', '1', 1], + ['aaa', 'aa', '1', 2], // this should be filtered out + ['aaa', 'aa', '3', 1], + ['aaa', 'bb', '4', 1], + ['aaa', 'bb', '5', 1], + ['aaa', 'bb', '6', 1], + ['bbb', 'aa', '7', 1], + ['bbb', 'aa', '8', 1], + ['bbb', 'bb', '9', 1], + ['bbb', 'bb', '10', 1], + ['bbb', 'cc', '11', 1], + ['bbb', 'cc', '12', 1], + ], + valueAccessor: (d: TestDatum) => d[3], + layers: [ + { + groupByRollup: (datum: TestDatum) => datum[0], + nodeLabel: (d: PrimitiveValue) => String(d), + }, + { + groupByRollup: (datum: TestDatum) => datum[1], + nodeLabel: (d: PrimitiveValue) => String(d), + }, + { + groupByRollup: (datum: TestDatum) => datum[2], + nodeLabel: (d: PrimitiveValue) => String(d), + }, + ], + }); + let store: Store; + + beforeEach(() => { + store = MockStore.default(); + }); + + it('no filter for no legendMaxDepth + filter duplicates', () => { + const settings = MockGlobalSpec.settings({ showLegend: true }); + MockStore.addSpecs([settings, spec], store); + + const labels = getLegendItemsLabels(store.getState()); + expect(labels).toEqual([ + { + depth: 1, + label: 'aaa', + }, + { + depth: 2, + label: 'aa', + }, + { + depth: 3, + label: '1', + }, + { + depth: 3, + label: '3', + }, + { + depth: 2, + label: 'bb', + }, + { + depth: 3, + label: '4', + }, + { + depth: 3, + label: '5', + }, + { + depth: 3, + label: '6', + }, + { + depth: 1, + label: 'bbb', + }, + { + depth: 3, + label: '7', + }, + { + depth: 3, + label: '8', + }, + { + depth: 3, + label: '9', + }, + { + depth: 3, + label: '10', + }, + { + depth: 2, + label: 'cc', + }, + { + depth: 3, + label: '11', + }, + { + depth: 3, + label: '12', + }, + ]); + }); + + it('filters labels at the first layer', () => { + const settings = MockGlobalSpec.settings({ showLegend: true, legendMaxDepth: 1 }); + MockStore.addSpecs([settings, spec], store); + + const labels = getLegendItemsLabels(store.getState()); + expect(labels).toEqual([ + { + depth: 1, + label: 'aaa', + }, + { + depth: 1, + label: 'bbb', + }, + ]); + }); + + it('filters labels at the second layer', () => { + const settings = MockGlobalSpec.settings({ showLegend: true, legendMaxDepth: 2 }); + MockStore.addSpecs([settings, spec], store); + + const labels = getLegendItemsLabels(store.getState()); + expect(labels).toEqual([ + { + depth: 1, + label: 'aaa', + }, + { + depth: 2, + label: 'aa', + }, + { + depth: 2, + label: 'bb', + }, + { + depth: 1, + label: 'bbb', + }, + { + depth: 2, + label: 'cc', + }, + ]); + }); + + it('filters all labels is depth is 0', () => { + const settings = MockGlobalSpec.settings({ showLegend: true, legendMaxDepth: 0 }); + MockStore.addSpecs([settings, spec], store); + + const labels = getLegendItemsLabels(store.getState()); + expect(labels).toEqual([]); + }); + it('filters all labels is depth is NaN', () => { + const settings = MockGlobalSpec.settings({ showLegend: true, legendMaxDepth: NaN }); + MockStore.addSpecs([settings, spec], store); + + const labels = getLegendItemsLabels(store.getState()); + expect(labels).toEqual([]); + }); +}); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.ts new file mode 100644 index 000000000000..9074e56305e6 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { LegendItemLabel } from '../../../../state/selectors/get_legend_items_labels'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getLegendLabels } from '../../layout/utils/legend_labels'; +import { getPartitionSpecs } from './get_partition_specs'; +import { getTrees } from './tree'; + +/** @internal */ +export const getLegendItemsLabels = createCachedSelector( + [getPartitionSpecs, getSettingsSpecSelector, getTrees], + (specs, { legendMaxDepth, showLegend }, trees): LegendItemLabel[] => + specs.flatMap((spec) => (showLegend ? getLegendLabels(spec.layers, trees[0].tree, legendMaxDepth) : [])), // singleton! wrt inner small multiples +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_partition_specs.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_partition_specs.ts new file mode 100644 index 000000000000..7b15661a1a81 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_partition_specs.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { ChartType } from '../../..'; +import { SpecType } from '../../../../specs'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSpecs } from '../../../../state/selectors/get_settings_specs'; +import { getSpecsFromStore } from '../../../../state/utils'; +import { PartitionSpec } from '../../specs'; + +/** @internal */ +export const getPartitionSpecs = createCachedSelector([getSpecs], (specs) => { + return getSpecsFromStore(specs, ChartType.Partition, SpecType.Series); +})(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/is_tooltip_visible.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/is_tooltip_visible.ts new file mode 100644 index 000000000000..02b5f9f690f0 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/is_tooltip_visible.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getTooltipType } from '../../../../specs'; +import { TooltipType } from '../../../../specs/constants'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getTooltipInfoSelector } from './tooltip'; + +/** + * The brush is available only for Ordinal xScales charts and + * if we have configured an onBrushEnd listener + * @internal + */ +export const isTooltipVisibleSelector = createCachedSelector( + [getSettingsSpecSelector, getTooltipInfoSelector], + (settingsSpec, tooltipInfo): boolean => { + return getTooltipType(settingsSpec) !== TooltipType.None && tooltipInfo.values.length > 0; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts new file mode 100644 index 000000000000..3aba10ca3e0d --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; +import { Selector } from 'reselect'; + +import { ChartType } from '../../..'; +import { getOnElementClickSelector } from '../../../../common/event_handler_selectors'; +import { GlobalChartState, PointerStates } from '../../../../state/chart_state'; +import { getLastClickSelector } from '../../../../state/selectors/get_last_click'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getPartitionSpec } from './partition_spec'; +import { getPickedShapesLayerValues } from './picked_shapes'; + +/** + * Will call the onElementClick listener every time the following preconditions are met: + * - the onElementClick listener is available + * - we have at least one highlighted geometry + * - the pointer state goes from down state to up state + * @internal + */ +export function createOnElementClickCaller(): (state: GlobalChartState) => void { + const prev: { click: PointerStates['lastClick'] } = { click: null }; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartType.Partition) { + selector = createCachedSelector( + [getPartitionSpec, getLastClickSelector, getSettingsSpecSelector, getPickedShapesLayerValues], + getOnElementClickSelector(prev), + )({ + keySelector: (s: GlobalChartState) => s.chartId, + }); + } + if (selector) { + selector(state); + } + }; +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_out_caller.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_out_caller.ts new file mode 100644 index 000000000000..5520ae54c7af --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_out_caller.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; +import { Selector } from 'react-redux'; + +import { ChartType } from '../../..'; +import { getOnElementOutSelector } from '../../../../common/event_handler_selectors'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getPartitionSpec } from './partition_spec'; +import { getPickedShapesLayerValues } from './picked_shapes'; + +/** + * Will call the onElementOut listener every time the following preconditions are met: + * - the onElementOut listener is available + * - the highlighted geometries list goes from a list of at least one object to an empty one + * @internal + */ +export function createOnElementOutCaller(): (state: GlobalChartState) => void { + const prev: { pickedShapes: number | null } = { pickedShapes: null }; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartType.Partition) { + selector = createCachedSelector( + [getPartitionSpec, getPickedShapesLayerValues, getSettingsSpecSelector], + getOnElementOutSelector(prev), + )({ + keySelector: getChartIdSelector, + }); + } + if (selector) { + selector(state); + } + }; +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_over_caller.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_over_caller.ts new file mode 100644 index 000000000000..580b6c7920fa --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_over_caller.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; +import { Selector } from 'react-redux'; + +import { ChartType } from '../../..'; +import { getOnElementOverSelector } from '../../../../common/event_handler_selectors'; +import { LayerValue } from '../../../../specs'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getPartitionSpec } from './partition_spec'; +import { getPickedShapesLayerValues } from './picked_shapes'; + +/** + * Will call the onElementOver listener every time the following preconditions are met: + * - the onElementOver listener is available + * - we have a new set of highlighted geometries on our state + * @internal + */ +export function createOnElementOverCaller(): (state: GlobalChartState) => void { + const prev: { pickedShapes: LayerValue[][] } = { pickedShapes: [] }; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartType.Partition) { + selector = createCachedSelector( + [getPartitionSpec, getPickedShapesLayerValues, getSettingsSpecSelector], + getOnElementOverSelector(prev), + )({ + keySelector: getChartIdSelector, + }); + } + if (selector) { + selector(state); + } + }; +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/partition_spec.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/partition_spec.ts new file mode 100644 index 000000000000..f76ac28eb37f --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/partition_spec.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartType } from '../../..'; +import { SpecType } from '../../../../specs'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getSpecsFromStore } from '../../../../state/utils'; +import { PartitionSpec } from '../../specs'; + +/** @internal */ +export function getPartitionSpecs(state: GlobalChartState): PartitionSpec[] { + return getSpecsFromStore(state.specs, ChartType.Partition, SpecType.Series); +} + +/** @internal */ +export function getPartitionSpec(state: GlobalChartState): PartitionSpec | null { + const partitionSpecs = getPartitionSpecs(state); + return partitionSpecs.length > 0 ? partitionSpecs[0] : null; // singleton! +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/picked_shapes.test.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/picked_shapes.test.ts new file mode 100644 index 000000000000..e21ee8c66153 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/picked_shapes.test.ts @@ -0,0 +1,276 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createStore, Store } from 'redux'; + +import { Predicate } from '../../../../common/predicate'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs'; +import { + SettingsSpec, + XYChartElementEvent, + PartitionElementEvent, + HeatmapElementEvent, + GroupBySpec, + SmallMultiplesSpec, + WordCloudElementEvent, +} from '../../../../specs'; +import { updateParentDimensions } from '../../../../state/actions/chart_settings'; +import { onMouseDown, onMouseUp, onPointerMove } from '../../../../state/actions/mouse'; +import { upsertSpec, specParsed } from '../../../../state/actions/specs'; +import { chartStoreReducer, GlobalChartState } from '../../../../state/chart_state'; +import { Datum } from '../../../../utils/common'; +import { HIERARCHY_ROOT_KEY } from '../../layout/utils/group_by_rollup'; +import { PartitionSpec } from '../../specs'; +import { partitionGeometries } from './geometries'; +import { createOnElementClickCaller } from './on_element_click_caller'; + +describe('Picked shapes selector', () => { + function initStore() { + const storeReducer = chartStoreReducer('chartId'); + return createStore(storeReducer); + } + function addSeries(store: Store, spec: PartitionSpec, settings?: Partial) { + store.dispatch(upsertSpec(MockGlobalSpec.settings(settings))); + store.dispatch(upsertSpec(spec)); + store.dispatch(specParsed()); + store.dispatch(updateParentDimensions({ width: 300, height: 300, top: 0, left: 0 })); + } + function addSmallMultiplesSeries( + store: Store, + groupBy: Partial, + sm: Partial, + spec: PartitionSpec, + settings?: Partial, + ) { + store.dispatch(upsertSpec(MockGlobalSpec.settings(settings))); + store.dispatch(upsertSpec(MockGlobalSpec.groupBy(groupBy))); + store.dispatch(upsertSpec(MockGlobalSpec.smallMultiple(sm))); + store.dispatch(upsertSpec(spec)); + store.dispatch(specParsed()); + store.dispatch(updateParentDimensions({ width: 300, height: 300, top: 0, left: 0 })); + } + let store: Store; + let treemapSpec: PartitionSpec; + let sunburstSpec: PartitionSpec; + beforeEach(() => { + store = initStore(); + const common = { + valueAccessor: (d: { v: number }) => d.v, + data: [ + { g1: 'a', g2: 'a', v: 1 }, + { g1: 'a', g2: 'b', v: 1 }, + { g1: 'b', g2: 'a', v: 1 }, + { g1: 'b', g2: 'b', v: 1 }, + ], + layers: [ + { + groupByRollup: (datum: { g1: string }) => datum.g1, + }, + { + groupByRollup: (datum: { g2: string }) => datum.g2, + }, + ], + }; + treemapSpec = MockSeriesSpec.treemap(common); + sunburstSpec = MockSeriesSpec.sunburst(common); + }); + test('check initial geoms', () => { + addSeries(store, treemapSpec); + const treemapGeometries = partitionGeometries(store.getState())[0]; + expect(treemapGeometries.quadViewModel).toHaveLength(6); + + addSeries(store, sunburstSpec); + const sunburstGeometries = partitionGeometries(store.getState())[0]; + expect(sunburstGeometries.quadViewModel).toHaveLength(6); + }); + test('treemap check picked geometries', () => { + const onClickListener = jest.fn< + undefined, + Array[] + >((): undefined => undefined); + addSeries(store, treemapSpec, { + onElementClick: onClickListener, + }); + const geometries = partitionGeometries(store.getState())[0]; + expect(geometries.quadViewModel).toHaveLength(6); + + const onElementClickCaller = createOnElementClickCaller(); + store.subscribe(() => { + onElementClickCaller(store.getState()); + }); + store.dispatch(onPointerMove({ x: 200, y: 200 }, 0)); + store.dispatch(onMouseDown({ x: 200, y: 200 }, 1)); + store.dispatch(onMouseUp({ x: 200, y: 200 }, 2)); + expect(onClickListener).toBeCalled(); + expect(onClickListener.mock.calls[0][0]).toEqual([ + [ + [ + { + smAccessorValue: '', + groupByRollup: 'b', + value: 2, + depth: 1, + sortIndex: 1, + path: [ + { index: 0, value: HIERARCHY_ROOT_KEY }, + { index: 1, value: 'b' }, + ], + }, + { + smAccessorValue: '', + groupByRollup: 'b', + value: 1, + depth: 2, + sortIndex: 1, + path: [ + { index: 0, value: HIERARCHY_ROOT_KEY }, + { index: 1, value: 'b' }, + { index: 1, value: 'b' }, + ], + }, + ], + { + specId: treemapSpec.id, + key: `spec{${treemapSpec.id}}`, + }, + ], + ]); + }); + test('small multiples pie chart check picked geometries', () => { + const onClickListener = jest.fn< + undefined, + Array[] + >((): undefined => undefined); + addSmallMultiplesSeries( + store, + { + id: 'splitGB', + by: (_, d: Datum) => d.g1, + sort: Predicate.AlphaAsc, + format: (d: Datum) => String(d), + }, + { id: 'sm', splitHorizontally: 'splitGB' }, + MockSeriesSpec.sunburst({ + smallMultiples: 'sm', + valueAccessor: (d: { v: number }) => d.v, + data: [ + { g1: 'a', g2: 'a', v: 1 }, + { g1: 'a', g2: 'b', v: 1 }, + { g1: 'b', g2: 'a', v: 1 }, + { g1: 'b', g2: 'b', v: 1 }, + ], + layers: [ + { + groupByRollup: (datum: { g2: string }) => datum.g2, + }, + ], + }), + { + onElementClick: onClickListener, + }, + ); + const geometries = partitionGeometries(store.getState())[0]; + expect(geometries.quadViewModel).toHaveLength(2); + + const onElementClickCaller = createOnElementClickCaller(); + store.subscribe(() => { + onElementClickCaller(store.getState()); + }); + const x = 50; + const y = 150; + store.dispatch(onPointerMove({ x, y }, 0)); + store.dispatch(onMouseDown({ x, y }, 1)); + store.dispatch(onMouseUp({ x, y }, 2)); + expect(onClickListener).toBeCalled(); + expect(onClickListener.mock.calls[0][0]).toEqual([ + [ + [ + { + smAccessorValue: 'a', + groupByRollup: 'a', + value: 1, + depth: 1, + sortIndex: 0, + path: [ + { index: 0, value: HIERARCHY_ROOT_KEY }, + { index: 0, value: 'a' }, + ], + }, + ], + { + specId: sunburstSpec.id, + key: `spec{${sunburstSpec.id}}`, + }, + ], + ]); + }); + test('sunburst check picked geometries', () => { + const onClickListener = jest.fn< + undefined, + Array[] + >((): undefined => undefined); + addSeries(store, sunburstSpec, { + onElementClick: onClickListener, + }); + const geometries = partitionGeometries(store.getState())[0]; + expect(geometries.quadViewModel).toHaveLength(6); + + const onElementClickCaller = createOnElementClickCaller(); + store.subscribe(() => { + onElementClickCaller(store.getState()); + }); + store.dispatch(onPointerMove({ x: 200, y: 200 }, 0)); + store.dispatch(onMouseDown({ x: 200, y: 200 }, 1)); + store.dispatch(onMouseUp({ x: 200, y: 200 }, 2)); + expect(onClickListener).toBeCalled(); + expect(onClickListener.mock.calls[0][0]).toEqual([ + [ + [ + { + smAccessorValue: '', + groupByRollup: 'b', + value: 2, + depth: 1, + sortIndex: 1, + path: [ + { index: 0, value: HIERARCHY_ROOT_KEY }, + { index: 1, value: 'b' }, + ], + }, + { + smAccessorValue: '', + groupByRollup: 'b', + value: 1, + depth: 2, + sortIndex: 1, + path: [ + { index: 0, value: HIERARCHY_ROOT_KEY }, + { index: 1, value: 'b' }, + { index: 1, value: 'b' }, + ], + }, + ], + { + specId: sunburstSpec.id, + key: `spec{${sunburstSpec.id}}`, + }, + ], + ]); + }); +}); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/picked_shapes.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/picked_shapes.ts new file mode 100644 index 000000000000..a42e3ec2c168 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/picked_shapes.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { pickedShapes, pickShapesLayerValues } from '../../layout/viewmodel/picked_shapes'; +import { partitionDrilldownFocus, partitionMultiGeometries } from './geometries'; + +function getCurrentPointerPosition(state: GlobalChartState) { + return state.interactions.pointer.current.position; +} + +/** @internal */ +export const getPickedShapes = createCachedSelector( + [partitionMultiGeometries, getCurrentPointerPosition, partitionDrilldownFocus], + pickedShapes, +)(getChartIdSelector); + +/** @internal */ +export const getPickedShapesLayerValues = createCachedSelector( + [getPickedShapes], + pickShapesLayerValues, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/tooltip.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/tooltip.ts new file mode 100644 index 000000000000..d70b7facba16 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/tooltip.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { TooltipInfo } from '../../../../components/tooltip/types'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { EMPTY_TOOLTIP, getTooltipInfo } from '../../layout/viewmodel/tooltip_info'; +import { getPartitionSpec } from './partition_spec'; +import { getPickedShapes } from './picked_shapes'; + +/** @internal */ +export const getTooltipInfoSelector = createCachedSelector( + [getPartitionSpec, getPickedShapes], + (spec, pickedShapes): TooltipInfo => { + return spec + ? getTooltipInfo( + pickedShapes, + spec.layers.map((l) => l.nodeLabel), + spec.valueGetter, + spec.valueFormatter, + spec.percentFormatter, + spec.id, + ) + : EMPTY_TOOLTIP; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/tree.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/tree.ts new file mode 100644 index 000000000000..e87a4ff04ba9 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/tree.ts @@ -0,0 +1,116 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { ChartType } from '../../..'; +import { getPredicateFn } from '../../../../common/predicate'; +import { + DEFAULT_SM_PANEL_PADDING, + GroupByAccessor, + GroupBySpec, + SmallMultiplesSpec, + SmallMultiplesStyle, + SpecType, +} from '../../../../specs'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSpecs } from '../../../../state/selectors/get_settings_specs'; +import { getSmallMultiplesSpecs } from '../../../../state/selectors/get_small_multiples_spec'; +import { getSpecsFromStore } from '../../../../state/utils'; +import { Datum } from '../../../../utils/common'; +import { configMetadata } from '../../layout/config'; +import { HierarchyOfArrays } from '../../layout/utils/group_by_rollup'; +import { partitionTree } from '../../layout/viewmodel/hierarchy_of_arrays'; +import { PartitionSpec } from '../../specs'; +import { getPartitionSpecs } from './get_partition_specs'; + +const getGroupBySpecs = createCachedSelector([getSpecs], (specs) => + getSpecsFromStore(specs, ChartType.Global, SpecType.IndexOrder), +)(getChartIdSelector); + +/** @internal */ +export type StyledTree = { + smAccessorValue: ReturnType; + name: string; + style: SmallMultiplesStyle; + tree: HierarchyOfArrays; +}; + +function getTreesForSpec( + spec: PartitionSpec, + smSpecs: SmallMultiplesSpec[], + groupBySpecs: GroupBySpec[], +): StyledTree[] { + const { data, valueAccessor, layers, config, smallMultiples: smId } = spec; + const smSpec = smSpecs.find((s) => s.id === smId); + const smStyle: SmallMultiplesStyle = { + horizontalPanelPadding: smSpec + ? smSpec.style?.horizontalPanelPadding ?? DEFAULT_SM_PANEL_PADDING + : { outer: 0, inner: 0 }, + verticalPanelPadding: smSpec + ? smSpec.style?.verticalPanelPadding ?? DEFAULT_SM_PANEL_PADDING + : { outer: 0, inner: 0 }, + }; + const groupBySpec = groupBySpecs.find( + (s) => s.id === smSpec?.splitHorizontally || s.id === smSpec?.splitVertically || s.id === smSpec?.splitZigzag, + ); + + if (groupBySpec) { + const { by, sort, format = (name) => String(name) } = groupBySpec; + const accessorSpec = { id: spec.id, chartType: spec.chartType, specType: SpecType.Series }; + const groups = data.reduce((map: Map, Datum[]>, next) => { + const groupingValue = by(accessorSpec, next); + const preexistingGroup = map.get(groupingValue); + const group = preexistingGroup ?? []; + if (!preexistingGroup) map.set(groupingValue, group); + group.push(next); + return map; + }, new Map()); + return Array.from(groups) + .sort(getPredicateFn(sort)) + .map(([groupKey, subData]) => ({ + name: format(groupKey), + smAccessorValue: groupKey, + style: smStyle, + tree: partitionTree( + subData, + valueAccessor, + layers, + configMetadata.partitionLayout.dflt, + config.partitionLayout, + ), + })); + } else { + return [ + { + name: '', + smAccessorValue: '', + style: smStyle, + tree: partitionTree(data, valueAccessor, layers, configMetadata.partitionLayout.dflt, config.partitionLayout), + }, + ]; + } +} + +/** @internal */ +export const getTrees = createCachedSelector( + [getPartitionSpecs, getSmallMultiplesSpecs, getGroupBySpecs], + (partitionSpecs, smallMultiplesSpecs, groupBySpecs): StyledTree[] => + partitionSpecs.length > 0 ? getTreesForSpec(partitionSpecs[0], smallMultiplesSpecs, groupBySpecs) : [], // singleton! +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/specs.ts b/packages/osd-charts/src/chart_types/specs.ts new file mode 100644 index 000000000000..ef094975bc8b --- /dev/null +++ b/packages/osd-charts/src/chart_types/specs.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + AreaSeries, + Axis, + BarSeries, + BubbleSeries, + HistogramBarSeries, + LineAnnotation, + LineSeries, + RectAnnotation, +} from './xy_chart/specs'; + +export * from './xy_chart/utils/specs'; + +export { Partition } from './partition_chart/specs'; + +export { Heatmap, HeatmapSpec } from './heatmap/specs'; diff --git a/packages/osd-charts/src/chart_types/wordcloud/layout/config/config.ts b/packages/osd-charts/src/chart_types/wordcloud/layout/config/config.ts new file mode 100644 index 000000000000..7539e4a4572f --- /dev/null +++ b/packages/osd-charts/src/chart_types/wordcloud/layout/config/config.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ConfigItem, configMap } from '../../../../common/config_objects'; +import { Config } from '../types/config_types'; + +/** @internal */ +export const configMetadata: Record = { + // shape geometry + width: { dflt: 300, min: 0, max: 1024, type: 'number', reconfigurable: false }, + height: { dflt: 150, min: 0, max: 1024, type: 'number', reconfigurable: false }, + margin: { + type: 'group', + values: { + left: { dflt: 0, min: -0.25, max: 0.25, type: 'number' }, + right: { dflt: 0, min: -0.25, max: 0.25, type: 'number' }, + top: { dflt: 0, min: -0.25, max: 0.25, type: 'number' }, + bottom: { dflt: 0, min: -0.25, max: 0.25, type: 'number' }, + }, + }, + + // general text config + fontFamily: { + dflt: 'Impact', + type: 'string', + }, + + // fill text config + minFontSize: { dflt: 10, min: 10, max: 50, type: 'number', reconfigurable: true }, + maxFontSize: { dflt: 70, min: 15, max: 150, type: 'number', reconfigurable: true }, + + backgroundColor: { dflt: '#ffffff', type: 'color' }, + sectorLineWidth: { dflt: 1, min: 0, max: 4, type: 'number' }, +}; + +/** @internal */ +export const config: Config = configMap((item: ConfigItem) => item.dflt, configMetadata); diff --git a/packages/osd-charts/src/chart_types/wordcloud/layout/types/config_types.ts b/packages/osd-charts/src/chart_types/wordcloud/layout/types/config_types.ts new file mode 100644 index 000000000000..34ce0acd8421 --- /dev/null +++ b/packages/osd-charts/src/chart_types/wordcloud/layout/types/config_types.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Pixels, SizeRatio } from '../../../../common/geometry'; +import { FontFamily } from '../../../../common/text_utils'; + +// todo switch to `io-ts` style, generic way of combining static and runtime type info +/** @public */ +export interface Config { + // shape geometry + width: number; + height: number; + margin: { left: SizeRatio; right: SizeRatio; top: SizeRatio; bottom: SizeRatio }; + + // general text config + fontFamily: FontFamily; + + // fill text config + minFontSize: Pixels; + maxFontSize: Pixels; +} diff --git a/packages/osd-charts/src/chart_types/wordcloud/layout/types/viewmodel_types.ts b/packages/osd-charts/src/chart_types/wordcloud/layout/types/viewmodel_types.ts new file mode 100644 index 000000000000..94b1291e08e6 --- /dev/null +++ b/packages/osd-charts/src/chart_types/wordcloud/layout/types/viewmodel_types.ts @@ -0,0 +1,164 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $Values as Values } from 'utility-types'; + +import { Pixels, PointObject } from '../../../../common/geometry'; +import { Color } from '../../../../utils/common'; +import { config } from '../config/config'; +import { Config } from './config_types'; + +/** @public */ +export interface WordModel { + text: string; + weight: number; + color: Color; +} + +/** @public */ +export const WeightFn = Object.freeze({ + log: 'log' as const, + linear: 'linear' as const, + exponential: 'exponential' as const, + squareRoot: 'squareRoot' as const, +}); + +/** @public */ +export type WeightFn = Values; + +/** @internal */ +export interface Word { + color: string; + font: string; + fontFamily: string; + fontWeight: number; + hasText: boolean; + height: number; + padding: number; + rotate: number; + size: number; + style: string; + text: string; + weight: number; + x: number; + x0: number; + x1: number; + xoff: number; + y: number; + y0: number; + y1: number; + yoff: number; + datum: WordModel; +} + +/** @public */ +export interface Configs { + count: number; + endAngle: number; + exponent: number; + fontFamily: string; + fontStyle: string; + fontWeight: number; + height: number; + maxFontSize: number; + minFontSize: number; + padding: number; + spiral: string; + startAngle: number; + weightFn: WeightFn; + width: number; +} + +/** @public */ +export type OutOfRoomCallback = (wordCount: number, renderedWordCount: number, renderedWords: string[]) => void; + +/** @internal */ +export interface WordcloudViewModel { + startAngle: number; + endAngle: number; + angleCount: number; + padding: number; + fontWeight: number; + fontFamily: string; + fontStyle: string; + minFontSize: number; + maxFontSize: number; + spiral: string; + exponent: number; + data: WordModel[]; + weightFn: WeightFn; + outOfRoomCallback: OutOfRoomCallback; + // specType: string; +} + +/** @internal */ +export interface Datum { + text: string; + weight: number; + color: string; +} + +/** @internal */ +export type PickFunction = (x: Pixels, y: Pixels) => Array; + +/** @internal */ +export type ShapeViewModel = { + config: Config; + wordcloudViewModel: WordcloudViewModel; + chartCenter: PointObject; + pickQuads: PickFunction; + specId: string; +}; + +const commonDefaults: WordcloudViewModel = { + startAngle: -20, + endAngle: 20, + angleCount: 5, + padding: 2, + fontWeight: 300, + fontFamily: 'Impact', + fontStyle: 'italic', + minFontSize: 10, + maxFontSize: 50, + spiral: 'archimedean', + exponent: 3, + data: [], + weightFn: 'exponential', + outOfRoomCallback: () => {}, +}; + +/** @internal */ +export const defaultWordcloudSpec = { + ...commonDefaults, +}; + +/** @internal */ +export const nullWordcloudViewModel: WordcloudViewModel = { + ...commonDefaults, + data: [], +}; + +/** @internal */ +export const nullShapeViewModel = (specifiedConfig?: Config, chartCenter?: PointObject): ShapeViewModel => ({ + config: specifiedConfig || config, + wordcloudViewModel: nullWordcloudViewModel, + chartCenter: chartCenter || { x: 0, y: 0 }, + pickQuads: () => [], + specId: 'empty', +}); diff --git a/packages/osd-charts/src/chart_types/wordcloud/layout/viewmodel/viewmodel.ts b/packages/osd-charts/src/chart_types/wordcloud/layout/viewmodel/viewmodel.ts new file mode 100644 index 000000000000..009e429796cc --- /dev/null +++ b/packages/osd-charts/src/chart_types/wordcloud/layout/viewmodel/viewmodel.ts @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { WordcloudSpec } from '../../specs'; +import { Config } from '../types/config_types'; +import { WordcloudViewModel, PickFunction, ShapeViewModel } from '../types/viewmodel_types'; + +/** @internal */ +export function shapeViewModel(spec: WordcloudSpec, config: Config): ShapeViewModel { + const { width, height, margin } = config; + + const innerWidth = width * (1 - Math.min(1, margin.left + margin.right)); + const innerHeight = height * (1 - Math.min(1, margin.top + margin.bottom)); + + const chartCenter = { + x: width * margin.left + innerWidth / 2, + y: height * margin.top + innerHeight / 2, + }; + + const { + id, + startAngle, + endAngle, + angleCount, + padding, + fontWeight, + fontFamily, + fontStyle, + minFontSize, + maxFontSize, + spiral, + exponent, + data, + weightFn, + outOfRoomCallback, + } = spec; + + const wordcloudViewModel: WordcloudViewModel = { + startAngle, + endAngle, + angleCount, + padding, + fontWeight, + fontFamily, + fontStyle, + minFontSize, + maxFontSize, + spiral, + exponent, + data, + weightFn, + outOfRoomCallback, + }; + + const pickQuads: PickFunction = (x, y) => + -innerWidth / 2 <= x && x <= innerWidth / 2 && -innerHeight / 2 <= y && y <= innerHeight / 2 + ? [wordcloudViewModel] + : []; + + // combined viewModel + return { + config, + chartCenter, + wordcloudViewModel, + pickQuads, + specId: id, + }; +} diff --git a/packages/osd-charts/src/chart_types/wordcloud/renderer/svg/connected_component.tsx b/packages/osd-charts/src/chart_types/wordcloud/renderer/svg/connected_component.tsx new file mode 100644 index 000000000000..47938253664f --- /dev/null +++ b/packages/osd-charts/src/chart_types/wordcloud/renderer/svg/connected_component.tsx @@ -0,0 +1,319 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-ignore +import d3TagCloud from 'd3-cloud'; +import React from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; + +import { ScreenReaderSummary } from '../../../../components/accessibility'; +import { SettingsSpec, WordCloudElementEvent } from '../../../../specs/settings'; +import { onChartRendered } from '../../../../state/actions/chart'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { + A11ySettings, + DEFAULT_A11Y_SETTINGS, + getA11ySettingsSelector, +} from '../../../../state/selectors/get_accessibility_config'; +import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { Dimensions } from '../../../../utils/dimensions'; +import { Configs, Datum, nullShapeViewModel, ShapeViewModel, Word } from '../../layout/types/viewmodel_types'; +import { geometries } from '../../state/selectors/geometries'; + +function seed() { + return 0.5; +} + +function getFont(d: Word) { + return d.fontFamily; +} + +function getFontStyle(d: Word) { + return d.style; +} + +function getFontWeight(d: Word) { + return d.fontWeight; +} + +function getWidth(conf: Configs) { + return conf.width ?? 500; +} + +function getHeight(conf: Configs) { + return conf.height ?? 500; +} + +function getFontSize(d: Word) { + return d.size; +} + +function hashWithinRange(str: string, max: number) { + str = JSON.stringify(str); + let hash = 0; + for (const ch of str) { + hash = (hash * 31 + ch.charCodeAt(0)) % max; + } + return Math.abs(hash) % max; +} + +function getRotation(startAngle: number, endAngle: number, count: number, text: string) { + const angleRange = endAngle - startAngle; + const angleCount = count ?? 360; + const interval = count - 1; + const angleStep = interval === 0 ? 0 : angleRange / interval; + const index = hashWithinRange(text, angleCount); + return index * angleStep + startAngle; +} + +function exponential(minFontSize: number, maxFontSize: number, exponent: number, weight: number) { + return minFontSize + (maxFontSize - minFontSize) * weight ** exponent; +} + +function linear(minFontSize: number, maxFontSize: number, _exponent: number, weight: number) { + return minFontSize + (maxFontSize - minFontSize) * weight; +} + +function squareRoot(minFontSize: number, maxFontSize: number, _exponent: number, weight: number) { + return minFontSize + (maxFontSize - minFontSize) * Math.sqrt(weight); +} + +function log(minFontSize: number, maxFontSize: number, _exponent: number, weight: number) { + return minFontSize + (maxFontSize - minFontSize) * Math.log2(weight + 1); +} + +const weightFnLookup = { linear, exponential, log, squareRoot }; + +function layoutMaker(config: Configs, data: Datum[]) { + const words = data.map((d) => { + const weightFn = weightFnLookup[config.weightFn]; + return { + datum: d, + text: d.text, + color: d.color, + fontFamily: config.fontFamily, + style: config.fontStyle, + fontWeight: config.fontWeight, + size: weightFn(config.minFontSize, config.maxFontSize, config.exponent, d.weight), + }; + }); + return d3TagCloud() + .random(seed) + .size([getWidth(config), getHeight(config)]) + .words(words) + .spiral(config.spiral ?? 'archimedean') + .padding(config.padding ?? 5) + .rotate((d: Word) => getRotation(config.startAngle, config.endAngle, config.count, d.text)) + .font(getFont) + .fontStyle(getFontStyle) + .fontWeight(getFontWeight) + .fontSize((d: Word) => getFontSize(d)); +} + +const View = ({ + words, + conf, + actions: { onElementClick, onElementOver, onElementOut }, + specId, +}: { + words: Word[]; + conf: Configs; + actions: { + onElementClick?: SettingsSpec['onElementClick']; + onElementOver?: SettingsSpec['onElementOver']; + onElementOut?: SettingsSpec['onElementOut']; + }; + specId: string; +}) => { + return ( + + + {words.map((d, i) => { + const elements: WordCloudElementEvent[] = [[d.datum, { specId, key: specId }]]; + const actions = { + ...(onElementClick && { + onClick: () => { + onElementClick(elements); + }, + }), + ...(onElementOver && { + onMouseOver: () => { + onElementOver(elements); + }, + }), + ...(onElementOut && { + onMouseOut: () => { + onElementOut(); + }, + }), + }; + return ( + + {d.text} + + ); + })} + + + ); +}; + +interface ReactiveChartStateProps { + initialized: boolean; + geometries: ShapeViewModel; + chartContainerDimensions: Dimensions; + a11ySettings: A11ySettings; + onElementClick?: SettingsSpec['onElementClick']; + onElementOver?: SettingsSpec['onElementOver']; + onElementOut?: SettingsSpec['onElementOut']; +} + +interface ReactiveChartDispatchProps { + onChartRendered: typeof onChartRendered; +} + +type Props = ReactiveChartStateProps & ReactiveChartDispatchProps; + +class Component extends React.Component { + static displayName = 'Wordcloud'; + + componentDidMount() { + if (this.props.initialized) { + this.props.onChartRendered(); + } + } + + componentDidUpdate() { + if (this.props.initialized) { + this.props.onChartRendered(); + } + } + + render() { + const { + initialized, + chartContainerDimensions: { width, height }, + geometries: { wordcloudViewModel, specId }, + a11ySettings, + onElementClick, + onElementOver, + onElementOut, + } = this.props; + if (!initialized || width === 0 || height === 0) { + return null; + } + const conf1: Configs = { + width, + height, + startAngle: wordcloudViewModel.startAngle, + endAngle: wordcloudViewModel.endAngle, + count: wordcloudViewModel.angleCount, + padding: wordcloudViewModel.padding, + fontWeight: wordcloudViewModel.fontWeight, + fontFamily: wordcloudViewModel.fontFamily, + fontStyle: wordcloudViewModel.fontStyle, + minFontSize: wordcloudViewModel.minFontSize, + maxFontSize: wordcloudViewModel.maxFontSize, + spiral: wordcloudViewModel.spiral, + exponent: wordcloudViewModel.exponent, + weightFn: wordcloudViewModel.weightFn, + }; + + const layout = layoutMaker(conf1, wordcloudViewModel.data); + + let ww; + layout.on('end', (w: Word[]) => (ww = w)).start(); + + const wordCount = wordcloudViewModel.data.length; + const renderedWordObjects = (ww as unknown) as Word[]; + const renderedWordCount: number = renderedWordObjects.length; + const notAllWordsFit = wordCount !== renderedWordCount; + if (notAllWordsFit && wordcloudViewModel.outOfRoomCallback instanceof Function) { + wordcloudViewModel.outOfRoomCallback( + wordCount, + renderedWordCount, + renderedWordObjects.map((word) => word.text), + ); + } + + return ( +
+ + +
+ ); + } +} + +const mapDispatchToProps = (dispatch: Dispatch): ReactiveChartDispatchProps => + bindActionCreators( + { + onChartRendered, + }, + dispatch, + ); + +const DEFAULT_PROPS: ReactiveChartStateProps = { + initialized: false, + geometries: nullShapeViewModel(), + chartContainerDimensions: { + width: 0, + height: 0, + left: 0, + top: 0, + }, + a11ySettings: DEFAULT_A11Y_SETTINGS, +}; + +const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return DEFAULT_PROPS; + } + return { + initialized: true, + geometries: geometries(state), + chartContainerDimensions: state.parentDimensions, + a11ySettings: getA11ySettingsSelector(state), + onElementClick: getSettingsSpecSelector(state).onElementClick, + onElementOver: getSettingsSpecSelector(state).onElementOver, + onElementOut: getSettingsSpecSelector(state).onElementOut, + }; +}; + +/** @internal */ +export const Wordcloud = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/packages/osd-charts/src/chart_types/wordcloud/specs/index.ts b/packages/osd-charts/src/chart_types/wordcloud/specs/index.ts new file mode 100644 index 000000000000..4864b785880c --- /dev/null +++ b/packages/osd-charts/src/chart_types/wordcloud/specs/index.ts @@ -0,0 +1,90 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { ChartType } from '../..'; +import { Spec } from '../../../specs'; +import { SpecType } from '../../../specs/constants'; +import { getConnect, specComponentFactory } from '../../../state/spec_factory'; +import { RecursivePartial } from '../../../utils/common'; +import { config } from '../layout/config/config'; +import { + WordModel, + defaultWordcloudSpec, + WeightFn, + OutOfRoomCallback, + Configs as WordcloudConfigs, +} from '../layout/types/viewmodel_types'; + +const defaultProps = { + chartType: ChartType.Wordcloud, + specType: SpecType.Series, + ...defaultWordcloudSpec, + config, +}; + +export { WordModel, WeightFn, OutOfRoomCallback, WordcloudConfigs }; + +/** @alpha */ +export interface WordcloudSpec extends Spec { + specType: typeof SpecType.Series; + chartType: typeof ChartType.Wordcloud; + config: RecursivePartial; + startAngle: number; + endAngle: number; + angleCount: number; + padding: number; + fontWeight: number; + fontFamily: string; + fontStyle: string; + minFontSize: number; + maxFontSize: number; + spiral: string; + exponent: number; + data: WordModel[]; + weightFn: WeightFn; + outOfRoomCallback: OutOfRoomCallback; +} + +type SpecRequiredProps = Pick; +type SpecOptionalProps = Partial>; + +/** @alpha */ +export const Wordcloud: React.FunctionComponent = getConnect()( + specComponentFactory< + WordcloudSpec, + | 'chartType' + | 'startAngle' + | 'config' + | 'endAngle' + | 'angleCount' + | 'padding' + | 'fontWeight' + | 'fontFamily' + | 'fontStyle' + | 'minFontSize' + | 'maxFontSize' + | 'spiral' + | 'exponent' + | 'data' + | 'weightFn' + | 'outOfRoomCallback' + >(defaultProps), +); diff --git a/packages/osd-charts/src/chart_types/wordcloud/state/chart_state.tsx b/packages/osd-charts/src/chart_types/wordcloud/state/chart_state.tsx new file mode 100644 index 000000000000..edaaeefb8257 --- /dev/null +++ b/packages/osd-charts/src/chart_types/wordcloud/state/chart_state.tsx @@ -0,0 +1,122 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { ChartType } from '../..'; +import { DEFAULT_CSS_CURSOR } from '../../../common/constants'; +import { LegendItem } from '../../../common/legend'; +import { InternalChartState, GlobalChartState } from '../../../state/chart_state'; +import { InitStatus } from '../../../state/selectors/get_internal_is_intialized'; +import { LegendItemLabel } from '../../../state/selectors/get_legend_items_labels'; +import { DebugState } from '../../../state/types'; +import { Dimensions } from '../../../utils/dimensions'; +import { EMPTY_TOOLTIP } from '../../partition_chart/layout/viewmodel/tooltip_info'; +import { Wordcloud } from '../renderer/svg/connected_component'; +import { getSpecOrNull } from './selectors/wordcloud_spec'; + +const EMPTY_MAP = new Map(); +const EMPTY_LEGEND_LIST: LegendItem[] = []; +const EMPTY_LEGEND_ITEM_LIST: LegendItemLabel[] = []; + +/** @internal */ +export class WordcloudState implements InternalChartState { + chartType = ChartType.Wordcloud; + + isInitialized(globalState: GlobalChartState) { + return getSpecOrNull(globalState) !== null ? InitStatus.Initialized : InitStatus.ChartNotInitialized; + } + + isBrushAvailable() { + return false; + } + + isBrushing() { + return false; + } + + isChartEmpty() { + return false; + } + + getLegendItems() { + return EMPTY_LEGEND_LIST; + } + + getLegendItemsLabels() { + return EMPTY_LEGEND_ITEM_LIST; + } + + getLegendExtraValues() { + return EMPTY_MAP; + } + + chartRenderer() { + return ; + } + + getPointerCursor() { + return DEFAULT_CSS_CURSOR; + } + + isTooltipVisible() { + return { visible: false, isExternal: false }; + } + + getTooltipInfo() { + return EMPTY_TOOLTIP; + } + + getTooltipAnchor(state: GlobalChartState) { + const { position } = state.interactions.pointer.current; + return { + isRotated: false, + x: position.x, + width: 0, + y: position.y, + height: 0, + }; + } + + eventCallbacks() {} + + getChartTypeDescription() { + return 'Word cloud chart'; + } + + // TODO + getProjectionContainerArea(): Dimensions { + return { width: 0, height: 0, top: 0, left: 0 }; + } + + // TODO + getMainProjectionArea(): Dimensions { + return { width: 0, height: 0, top: 0, left: 0 }; + } + + // TODO + getBrushArea(): Dimensions | null { + return null; + } + + // TODO + getDebugState(): DebugState { + return {}; + } +} diff --git a/packages/osd-charts/src/chart_types/wordcloud/state/selectors/geometries.ts b/packages/osd-charts/src/chart_types/wordcloud/state/selectors/geometries.ts new file mode 100644 index 000000000000..994e687d76ba --- /dev/null +++ b/packages/osd-charts/src/chart_types/wordcloud/state/selectors/geometries.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { ChartType } from '../../..'; +import { SpecType } from '../../../../specs/constants'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getSpecsFromStore } from '../../../../state/utils'; +import { nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; +import { WordcloudSpec } from '../../specs'; +import { render } from './scenegraph'; + +const getSpecs = (state: GlobalChartState) => state.specs; + +const getParentDimensions = (state: GlobalChartState) => state.parentDimensions; + +/** @internal */ +export const geometries = createCachedSelector( + [getSpecs, getParentDimensions], + (specs, parentDimensions): ShapeViewModel => { + const wordcloudSpecs = getSpecsFromStore(specs, ChartType.Wordcloud, SpecType.Series); + return wordcloudSpecs.length === 1 ? render(wordcloudSpecs[0], parentDimensions) : nullShapeViewModel(); + }, +)((state) => state.chartId); diff --git a/packages/osd-charts/src/chart_types/wordcloud/state/selectors/on_element_click_caller.ts b/packages/osd-charts/src/chart_types/wordcloud/state/selectors/on_element_click_caller.ts new file mode 100644 index 000000000000..88d12a4ce442 --- /dev/null +++ b/packages/osd-charts/src/chart_types/wordcloud/state/selectors/on_element_click_caller.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; +import { Selector } from 'reselect'; + +import { ChartType } from '../../..'; +import { getOnElementClickSelector } from '../../../../common/event_handler_selectors'; +import { GlobalChartState, PointerStates } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getLastClickSelector } from '../../../../state/selectors/get_last_click'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getPickedShapesLayerValues } from './picked_shapes'; +import { getSpecOrNull } from './wordcloud_spec'; + +/** + * Will call the onElementClick listener every time the following preconditions are met: + * - the onElementClick listener is available + * - we have at least one highlighted geometry + * - the pointer state goes from down state to up state + * @internal + */ +export function createOnElementClickCaller(): (state: GlobalChartState) => void { + const prev: { click: PointerStates['lastClick'] } = { click: null }; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartType.Wordcloud) { + selector = createCachedSelector( + [getSpecOrNull, getLastClickSelector, getSettingsSpecSelector, getPickedShapesLayerValues], + getOnElementClickSelector(prev), + )(getChartIdSelector); + } + if (selector) { + selector(state); + } + }; +} diff --git a/packages/osd-charts/src/chart_types/wordcloud/state/selectors/on_element_out_caller.ts b/packages/osd-charts/src/chart_types/wordcloud/state/selectors/on_element_out_caller.ts new file mode 100644 index 000000000000..be5b9abf418a --- /dev/null +++ b/packages/osd-charts/src/chart_types/wordcloud/state/selectors/on_element_out_caller.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; +import { Selector } from 'react-redux'; + +import { ChartType } from '../../..'; +import { getOnElementOutSelector } from '../../../../common/event_handler_selectors'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getPickedShapesLayerValues } from './picked_shapes'; +import { getSpecOrNull } from './wordcloud_spec'; + +/** + * Will call the onElementOut listener every time the following preconditions are met: + * - the onElementOut listener is available + * - the highlighted geometries list goes from a list of at least one object to an empty one + * @internal + */ +export function createOnElementOutCaller(): (state: GlobalChartState) => void { + const prev: { pickedShapes: number | null } = { pickedShapes: null }; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartType.Wordcloud) { + selector = createCachedSelector( + [getSpecOrNull, getPickedShapesLayerValues, getSettingsSpecSelector], + getOnElementOutSelector(prev), + )(getChartIdSelector); + } + if (selector) { + selector(state); + } + }; +} diff --git a/packages/osd-charts/src/chart_types/wordcloud/state/selectors/on_element_over_caller.ts b/packages/osd-charts/src/chart_types/wordcloud/state/selectors/on_element_over_caller.ts new file mode 100644 index 000000000000..49e2e4ad598e --- /dev/null +++ b/packages/osd-charts/src/chart_types/wordcloud/state/selectors/on_element_over_caller.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; +import { Selector } from 'react-redux'; + +import { ChartType } from '../../..'; +import { getOnElementOverSelector } from '../../../../common/event_handler_selectors'; +import { LayerValue } from '../../../../specs'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getPickedShapesLayerValues } from './picked_shapes'; +import { getSpecOrNull } from './wordcloud_spec'; + +/** + * Will call the onElementOver listener every time the following preconditions are met: + * - the onElementOver listener is available + * - we have a new set of highlighted geometries on our state + * @internal + */ +export function createOnElementOverCaller(): (state: GlobalChartState) => void { + const prev: { pickedShapes: LayerValue[][] } = { pickedShapes: [] }; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartType.Wordcloud) { + selector = createCachedSelector( + [getSpecOrNull, getPickedShapesLayerValues, getSettingsSpecSelector], + getOnElementOverSelector(prev), + )({ + keySelector: getChartIdSelector, + }); + } + if (selector) { + selector(state); + } + }; +} diff --git a/packages/osd-charts/src/chart_types/wordcloud/state/selectors/picked_shapes.ts b/packages/osd-charts/src/chart_types/wordcloud/state/selectors/picked_shapes.ts new file mode 100644 index 000000000000..e72255406da5 --- /dev/null +++ b/packages/osd-charts/src/chart_types/wordcloud/state/selectors/picked_shapes.ts @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { LayerValue } from '../../../../specs'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { WordcloudViewModel } from '../../layout/types/viewmodel_types'; +import { geometries } from './geometries'; + +function getCurrentPointerPosition(state: GlobalChartState) { + return state.interactions.pointer.current.position; +} + +/** @internal */ +export const getPickedShapes = createCachedSelector( + [geometries, getCurrentPointerPosition], + (geoms, pointerPosition): WordcloudViewModel[] => { + const picker = geoms.pickQuads; + const { chartCenter } = geoms; + const x = pointerPosition.x - chartCenter.x; + const y = pointerPosition.y - chartCenter.y; + return picker(x, y); + }, +)((state) => state.chartId); + +/** @internal */ +export const getPickedShapesLayerValues = createCachedSelector( + [getPickedShapes], + (pickedShapes): Array> => { + const elements = pickedShapes.map>((model) => { + const values: Array = []; + values.push({ + smAccessorValue: '', + groupByRollup: 'Word count', + value: model.data.length, + sortIndex: 0, + path: [], + depth: 0, + }); + return values.reverse(); + }); + return elements; + }, +)((state) => state.chartId); diff --git a/packages/osd-charts/src/chart_types/wordcloud/state/selectors/scenegraph.ts b/packages/osd-charts/src/chart_types/wordcloud/state/selectors/scenegraph.ts new file mode 100644 index 000000000000..2e97007f332e --- /dev/null +++ b/packages/osd-charts/src/chart_types/wordcloud/state/selectors/scenegraph.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mergePartial, RecursivePartial } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { config as defaultConfig } from '../../layout/config/config'; +import { Config } from '../../layout/types/config_types'; +import { ShapeViewModel } from '../../layout/types/viewmodel_types'; +import { shapeViewModel } from '../../layout/viewmodel/viewmodel'; +import { WordcloudSpec } from '../../specs'; + +/** @internal */ +export function render(spec: WordcloudSpec, parentDimensions: Dimensions): ShapeViewModel { + const { width, height } = parentDimensions; + const { config } = spec; + const partialConfig: RecursivePartial = { ...config, width, height }; + const cfg: Config = mergePartial(defaultConfig, partialConfig); + return shapeViewModel(spec, cfg); +} diff --git a/packages/osd-charts/src/chart_types/wordcloud/state/selectors/wordcloud_spec.ts b/packages/osd-charts/src/chart_types/wordcloud/state/selectors/wordcloud_spec.ts new file mode 100644 index 000000000000..6e6def5d542f --- /dev/null +++ b/packages/osd-charts/src/chart_types/wordcloud/state/selectors/wordcloud_spec.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartType } from '../../..'; +import { SpecType } from '../../../../specs/constants'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getSpecsFromStore } from '../../../../state/utils'; +import { WordcloudSpec } from '../../specs'; + +/** @internal */ +export function getSpecOrNull(state: GlobalChartState): WordcloudSpec | null { + const specs = getSpecsFromStore(state.specs, ChartType.Wordcloud, SpecType.Series); + return specs.length > 0 ? specs[0] : null; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/annotations/line/dimensions.integration.test.ts b/packages/osd-charts/src/chart_types/xy_chart/annotations/line/dimensions.integration.test.ts new file mode 100644 index 000000000000..bf24848f0f2b --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/annotations/line/dimensions.integration.test.ts @@ -0,0 +1,164 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MockAnnotationLineProps } from '../../../../mocks/annotations/annotations'; +import { MockSeriesSpec, MockAnnotationSpec, MockGlobalSpec } from '../../../../mocks/specs'; +import { MockStore } from '../../../../mocks/store'; +import { ScaleType } from '../../../../scales/constants'; +import { computeAnnotationDimensionsSelector } from '../../state/selectors/compute_annotations'; +import { AnnotationDomainType } from '../../utils/specs'; + +function expectAnnotationAtPosition( + data: Array<{ x: number; y: number }>, + type: 'line' | 'bar', + indexPosition: number, + expectedLinePosition: number, + numOfSpecs = 1, + xScaleType: typeof ScaleType.Ordinal | typeof ScaleType.Linear | typeof ScaleType.Time = ScaleType.Linear, +) { + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins(); + const specs = new Array(numOfSpecs).fill(0).map((d, i) => + MockSeriesSpec.byTypePartial(type)({ + id: `spec_${i}`, + xScaleType, + data, + }), + ); + const annotation = MockAnnotationSpec.line({ + dataValues: [ + { + dataValue: indexPosition, + }, + ], + }); + + MockStore.addSpecs([settings, ...specs, annotation], store); + const annotations = computeAnnotationDimensionsSelector(store.getState()); + expect(annotations.get(annotation.id)).toEqual([ + MockAnnotationLineProps.default({ + specId: 'line_annotation_1', + datum: { + dataValue: indexPosition, + }, + linePathPoints: { + x1: expectedLinePosition, + y1: 0, + x2: expectedLinePosition, + y2: 100, + }, + }), + ]); +} + +describe('Render vertical line annotation within', () => { + it.each([ + [0, 1, 12.5], // middle of 1st bar + [1, 1, 37.5], // middle of 2nd bar + [2, 1, 62.5], // middle of 3rd bar + [3, 1, 87.5], // middle of 4th bar + [1, 2, 37.5], // middle of 2nd bar + [1, 3, 37.5], // middle of 2nd bar + ])('a bar at position %i, %i specs, all scales', (dataValue, numOfSpecs, linePosition) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + ]; + expectAnnotationAtPosition(data, 'bar', dataValue, linePosition, numOfSpecs); + expectAnnotationAtPosition(data, 'bar', dataValue, linePosition, numOfSpecs, ScaleType.Ordinal); + expectAnnotationAtPosition(data, 'bar', dataValue, linePosition, numOfSpecs, ScaleType.Time); + }); + + it.each([ + [0, 1, 0], // the start of the chart + [1, 1, 50], // the middle of the chart + [2, 1, 100], // the end of the chart + [1, 2, 50], // the middle of the chart + [1, 3, 50], // the middle of the chart + ])('line point at position %i, %i specs, linear scale', (dataValue, numOfSpecs, linePosition) => { + const data = [ + { x: 0, y: 1 }, + { x: 1, y: 1 }, + { x: 2, y: 2 }, + ]; + expectAnnotationAtPosition(data, 'line', dataValue, linePosition, numOfSpecs); + }); + + it.each([ + [0, 1, 12.5], // 1st ordinal line point + [1, 1, 37.5], // 2nd ordinal line point + [2, 1, 62.5], // 3rd ordinal line point + [3, 1, 87.5], // 4th ordinal line point + [1, 2, 37.5], // 2nd ordinal line point + [1, 3, 37.5], // 2nd ordinal line point + ])('line point at position %i, %i specs, Ordinal scale', (dataValue, numOfSpecs, linePosition) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + ]; + expectAnnotationAtPosition(data, 'line', dataValue, linePosition, numOfSpecs, ScaleType.Ordinal); + }); + + it('histogramMode with line after the max value but before the max + minInterval', () => { + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ + xDomain: { + min: 0, + max: 9, + minInterval: 1, + }, + }); + const spec = MockSeriesSpec.histogramBar({ + xScaleType: ScaleType.Linear, + data: [ + { + x: 0, + y: 1, + }, + { + x: 9, + y: 20, + }, + ], + }); + const annotation = MockAnnotationSpec.line({ + domainType: AnnotationDomainType.XDomain, + dataValues: [{ dataValue: 9.5, details: 'foo' }], + }); + + MockStore.addSpecs([settings, spec, annotation], store); + const annotations = computeAnnotationDimensionsSelector(store.getState()); + expect(annotations.get(annotation.id)).toEqual([ + MockAnnotationLineProps.default({ + specId: 'line_annotation_1', + linePathPoints: { + x1: 95, + y1: 0, + x2: 95, + y2: 100, + }, + datum: { dataValue: 9.5, details: 'foo' }, + }), + ]); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/annotations/line/dimensions.test.ts b/packages/osd-charts/src/chart_types/xy_chart/annotations/line/dimensions.test.ts new file mode 100644 index 000000000000..6f3a97442ad0 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/annotations/line/dimensions.test.ts @@ -0,0 +1,741 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MockAnnotationLineProps, MockAnnotationRectProps } from '../../../../mocks/annotations/annotations'; +import { MockAnnotationSpec, MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs'; +import { MockStore } from '../../../../mocks/store'; +import { ScaleType } from '../../../../scales/constants'; +import { Position } from '../../../../utils/common'; +import { AnnotationId } from '../../../../utils/ids'; +import { DEFAULT_ANNOTATION_LINE_STYLE } from '../../../../utils/themes/merge_utils'; +import { computeAnnotationDimensionsSelector } from '../../state/selectors/compute_annotations'; +import { AnnotationDomainType } from '../../utils/specs'; +import { AnnotationDimensions } from '../types'; +import { AnnotationLineProps } from './types'; + +describe('Annotation utils', () => { + const groupId = 'foo-group'; + + const continuousBarChart = MockSeriesSpec.bar({ + xScaleType: ScaleType.Linear, + groupId, + data: [ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + { x: 3, y: 10 }, + { x: 4, y: 5 }, + { x: 9, y: 10 }, + ], + }); + + const ordinalBarChart = MockSeriesSpec.bar({ + xScaleType: ScaleType.Ordinal, + groupId, + data: [ + { x: 'a', y: 1 }, + { x: 'b', y: 0 }, + { x: 'c', y: 10 }, + { x: 'd', y: 5 }, + ], + }); + + const verticalAxisSpec = MockGlobalSpec.axis({ + id: 'vertical_axis', + groupId, + hide: false, + showOverlappingTicks: false, + showOverlappingLabels: false, + position: Position.Left, + showGridLines: true, + }); + + test('should compute line annotation in x ordinal scale', () => { + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins(); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo', + groupId, + domainType: AnnotationDomainType.YDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + }); + + const rectAnnotation = MockAnnotationSpec.rect({ + id: 'rect', + groupId, + dataValues: [{ coordinates: { x0: 'a', x1: 'b', y0: 3, y1: 5 } }], + }); + + MockStore.addSpecs([settings, ordinalBarChart, lineAnnotation, rectAnnotation], store); + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + + const expectedDimensions = new Map(); + expectedDimensions.set('foo', [ + MockAnnotationLineProps.default({ + specId: 'foo', + linePathPoints: { + x1: 0, + y1: 80, + x2: 100, + y2: 80, + }, + datum: { dataValue: 2, details: 'foo' }, + }), + ]); + expectedDimensions.set('rect', [ + MockAnnotationRectProps.default({ + rect: { x: 0, y: 50, width: 50, height: 20 }, + panel: { top: 0, left: 0, width: 100, height: 100 }, + datum: { coordinates: { x0: 'a', x1: 'b', y0: 3, y1: 5 } }, + }), + ]); + + expect(dimensions).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions also with missing axis', () => { + const store = MockStore.default({ width: 10, height: 20, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins(); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo', + domainType: AnnotationDomainType.YDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs([settings, ordinalBarChart, lineAnnotation], store); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + expect(dimensions.size).toEqual(1); + }); + + test('should compute line annotation dimensions for yDomain on a yScale (chartRotation 0, left axis)', () => { + const panel = { width: 10, height: 100, top: 0, left: 0 }; + const store = MockStore.default(panel); + const settings = MockGlobalSpec.settingsNoMargins(); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + domainType: AnnotationDomainType.YDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + ordinalBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + specId: 'foo-line', + linePathPoints: { + x1: 0, + y1: 80, + x2: 10, + y2: 80, + }, + panel, + datum: { dataValue: 2, details: 'foo' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions for yDomain on a yScale (chartRotation 0, right axis)', () => { + const store = MockStore.default({ width: 10, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins(); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + domainType: AnnotationDomainType.YDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + ordinalBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + position: Position.Right, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + specId: 'foo-line', + linePathPoints: { + x1: 0, + y1: 80, + x2: 10, + y2: 80, + }, + panel: { width: 10, height: 100, top: 0, left: 0 }, + datum: { dataValue: 2, details: 'foo' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions for yDomain on a yScale (chartRotation 90)', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 90 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + domainType: AnnotationDomainType.YDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + ordinalBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + specId: 'foo-line', + linePathPoints: { + x1: 0, + y1: 80, + x2: 100, + y2: 80, + }, + datum: { dataValue: 2, details: 'foo' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should not compute line annotation dimensions for yDomain if no corresponding yScale', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 0 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + groupId: 'other-group', + domainType: AnnotationDomainType.YDomain, + dataValues: [], + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + ordinalBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + + expect(dimensions.size).toEqual(0); + }); + + test('should compute line annotation dimensions for xDomain (chartRotation 0, ordinal scale)', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 0 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + groupId: 'other-group', + domainType: AnnotationDomainType.XDomain, + dataValues: [{ dataValue: 'a', details: 'foo' }], + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + ordinalBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + position: Position.Bottom, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + specId: 'foo-line', + linePathPoints: { + x1: 12.5, + y1: 0, + x2: 12.5, + y2: 100, + }, + datum: { dataValue: 'a', details: 'foo' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions for xDomain (chartRotation 0, continuous scale, top axis)', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 0 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + groupId: 'other-group', + domainType: AnnotationDomainType.XDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + continuousBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + position: Position.Top, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + specId: 'foo-line', + linePathPoints: { + x1: 25, + y1: 0, + x2: 25, + y2: 100, + }, + datum: { dataValue: 2, details: 'foo' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions for xDomain (chartRotation 0, continuous scale, bottom axis)', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 0 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + groupId: 'other-group', + domainType: AnnotationDomainType.XDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + continuousBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + position: Position.Bottom, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + specId: 'foo-line', + linePathPoints: { + x1: 25, + y1: 0, + x2: 25, + y2: 100, + }, + datum: { dataValue: 2, details: 'foo' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions for xDomain on a xScale (chartRotation 90, ordinal scale)', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 0 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + groupId: 'other-group', + domainType: AnnotationDomainType.XDomain, + dataValues: [{ dataValue: 'a', details: 'foo' }], + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + ordinalBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + position: Position.Top, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + specId: 'foo-line', + linePathPoints: { + x1: 12.5, + y1: 0, + x2: 12.5, + y2: 100, + }, + datum: { dataValue: 'a', details: 'foo' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions for xDomain on a xScale (chartRotation 90, continuous scale)', () => { + const panel = { width: 100, height: 50, top: 0, left: 0 }; + const store = MockStore.default(panel); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 90 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + groupId: 'other-group', + domainType: AnnotationDomainType.XDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + continuousBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + position: Position.Top, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + specId: 'foo-line', + linePathPoints: { + x1: 12.5, + y1: 0, + x2: 12.5, + y2: 100, + }, + panel, + datum: { dataValue: 2, details: 'foo' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions for xDomain on a xScale (chartRotation -90, continuous scale)', () => { + const panel = { width: 100, height: 50, top: 0, left: 0 }; + const store = MockStore.default(panel); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: -90 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + groupId: 'other-group', + domainType: AnnotationDomainType.XDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + continuousBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + position: Position.Top, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + specId: 'foo-line', + linePathPoints: { + x1: 12.5, + y1: 0, + x2: 12.5, + y2: 100, + }, + panel, + datum: { dataValue: 2, details: 'foo' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions for xDomain (chartRotation 180, continuous scale, top axis)', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 180 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + groupId: 'other-group', + domainType: AnnotationDomainType.XDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + continuousBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + position: Position.Top, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + specId: 'foo-line', + linePathPoints: { + x1: 25, + y1: 0, + x2: 25, + y2: 100, + }, + datum: { dataValue: 2, details: 'foo' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions for xDomain (chartRotation 180, continuous scale, bottom axis)', () => { + const panel = { width: 100, height: 50, top: 0, left: 0 }; + const store = MockStore.default(panel); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 180 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + groupId: 'other-group', + domainType: AnnotationDomainType.XDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + continuousBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + position: Position.Bottom, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + specId: 'foo-line', + linePathPoints: { + x1: 25, + y1: 0, + x2: 25, + y2: 50, + }, + panel, + datum: { dataValue: 2, details: 'foo' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + test('should not compute annotation line values for invalid data values or AnnotationSpec.hideLines', () => { + let store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 180 }); + + const annotationId = 'foo-line'; + const invalidXLineAnnotation = MockAnnotationSpec.line({ + id: annotationId, + domainType: AnnotationDomainType.XDomain, + dataValues: [{ dataValue: 'e', details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs([settings, continuousBarChart, invalidXLineAnnotation], store); + const emptyXDimensions = computeAnnotationDimensionsSelector(store.getState()); + + expect(emptyXDimensions.get('foo-line')).toHaveLength(0); + + const invalidStringXLineAnnotation = MockAnnotationSpec.line({ + id: annotationId, + domainType: AnnotationDomainType.XDomain, + dataValues: [{ dataValue: '', details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + store = MockStore.default({ width: 100, height: 50, top: 0, left: 0 }); + MockStore.addSpecs([settings, continuousBarChart, invalidStringXLineAnnotation], store); + + const invalidStringXDimensions = computeAnnotationDimensionsSelector(store.getState()); + + expect(invalidStringXDimensions.get('foo-line')).toHaveLength(0); + + const outOfBoundsXLineAnnotation = MockAnnotationSpec.line({ + id: annotationId, + domainType: AnnotationDomainType.XDomain, + dataValues: [{ dataValue: -999, details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + store = MockStore.default({ width: 100, height: 50, top: 0, left: 0 }); + MockStore.addSpecs([settings, continuousBarChart, outOfBoundsXLineAnnotation], store); + + const emptyOutOfBoundsXDimensions = computeAnnotationDimensionsSelector(store.getState()); + + expect(emptyOutOfBoundsXDimensions.get('foo-line')).toHaveLength(0); + + const invalidYLineAnnotation = MockAnnotationSpec.line({ + id: annotationId, + domainType: AnnotationDomainType.YDomain, + dataValues: [{ dataValue: 'e', details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + store = MockStore.default({ width: 100, height: 50, top: 0, left: 0 }); + MockStore.addSpecs([settings, continuousBarChart, invalidYLineAnnotation], store); + + const emptyOutOfBoundsYDimensions = computeAnnotationDimensionsSelector(store.getState()); + + expect(emptyOutOfBoundsYDimensions.get('foo-line')).toHaveLength(0); + + const outOfBoundsYLineAnnotation = MockAnnotationSpec.line({ + id: annotationId, + domainType: AnnotationDomainType.YDomain, + dataValues: [{ dataValue: -999, details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + store = MockStore.default({ width: 100, height: 50, top: 0, left: 0 }); + MockStore.addSpecs([settings, continuousBarChart, outOfBoundsYLineAnnotation], store); + + const outOfBoundsYAnn = computeAnnotationDimensionsSelector(store.getState()); + + expect(outOfBoundsYAnn.get('foo-line')).toHaveLength(0); + + const invalidStringYLineAnnotation = MockAnnotationSpec.line({ + id: annotationId, + domainType: AnnotationDomainType.YDomain, + dataValues: [{ dataValue: '', details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + store = MockStore.default({ width: 100, height: 50, top: 0, left: 0 }); + MockStore.addSpecs([settings, continuousBarChart, invalidStringYLineAnnotation], store); + + const invalidStringYDimensions = computeAnnotationDimensionsSelector(store.getState()); + + expect(invalidStringYDimensions.get('foo-line')).toHaveLength(0); + + const validHiddenAnnotation = MockAnnotationSpec.line({ + id: annotationId, + domainType: AnnotationDomainType.XDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + hideLines: true, + }); + + store = MockStore.default({ width: 100, height: 50, top: 0, left: 0 }); + MockStore.addSpecs([settings, continuousBarChart, validHiddenAnnotation], store); + + const hiddenAnnotationDimensions = computeAnnotationDimensionsSelector(store.getState()); + + expect(hiddenAnnotationDimensions.size).toBe(0); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/annotations/line/dimensions.ts b/packages/osd-charts/src/chart_types/xy_chart/annotations/line/dimensions.ts new file mode 100644 index 000000000000..d9c1f131b7f9 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/annotations/line/dimensions.ts @@ -0,0 +1,415 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Line } from '../../../../geoms/types'; +import { Scale } from '../../../../scales'; +import { isContinuousScale, isBandScale } from '../../../../scales/types'; +import { isNil, Position, Rotation } from '../../../../utils/common'; +import { Dimensions, Size } from '../../../../utils/dimensions'; +import { GroupId } from '../../../../utils/ids'; +import { mergeWithDefaultAnnotationLine } from '../../../../utils/themes/merge_utils'; +import { SmallMultipleScales } from '../../state/selectors/compute_small_multiple_scales'; +import { isHorizontalRotation, isVerticalRotation } from '../../state/utils/common'; +import { computeXScaleOffset } from '../../state/utils/utils'; +import { getPanelSize } from '../../utils/panel'; +import { AnnotationDomainType, LineAnnotationSpec, LineAnnotationDatum } from '../../utils/specs'; +import { AnnotationLineProps } from './types'; + +function computeYDomainLineAnnotationDimensions( + annotationSpec: LineAnnotationSpec, + yScale: Scale, + { vertical, horizontal }: SmallMultipleScales, + chartRotation: Rotation, + axisPosition?: Position, +): AnnotationLineProps[] { + const { + id: specId, + dataValues, + marker: icon, + markerBody: body, + markerDimensions: dimension, + markerPosition: specMarkerPosition, + style, + } = annotationSpec; + const lineStyle = mergeWithDefaultAnnotationLine(style); + const color = lineStyle?.line?.stroke ?? 'red'; + const isHorizontalChartRotation = isHorizontalRotation(chartRotation); + // let's use a default Bottom-X/Left-Y axis orientation if we are not showing an axis + // but we are displaying a line annotation + + const lineProps: AnnotationLineProps[] = []; + const [domainStart, domainEnd] = yScale.domain; + + const panelSize = getPanelSize({ vertical, horizontal }); + + dataValues.forEach((datum: LineAnnotationDatum, i) => { + const { dataValue } = datum; + + // avoid rendering invalid annotation value + if (dataValue === null || dataValue === undefined || dataValue === '') { + return; + } + + const annotationValueYPosition = yScale.scale(dataValue); + // avoid rendering non scalable annotation values + if (annotationValueYPosition === null) { + return; + } + + // avoid rendering annotation with values outside the scale domain + if (dataValue < domainStart || dataValue > domainEnd) { + return; + } + + vertical.domain.forEach((verticalValue) => { + horizontal.domain.forEach((horizontalValue) => { + const top = vertical.scaleOrThrow(verticalValue); + const left = horizontal.scaleOrThrow(horizontalValue); + + const width = isHorizontalChartRotation ? horizontal.bandwidth : vertical.bandwidth; + const height = isHorizontalChartRotation ? vertical.bandwidth : horizontal.bandwidth; + const linePathPoints = getYLinePath({ width, height }, annotationValueYPosition); + const alignment = getAnchorPosition(false, chartRotation, axisPosition, specMarkerPosition); + + const position = getMarkerPositionForYAnnotation( + panelSize, + chartRotation, + alignment, + annotationValueYPosition, + dimension, + ); + + const lineProp: AnnotationLineProps = { + specId, + id: getAnnotationLinePropsId(specId, datum, i, verticalValue, horizontalValue), + datum, + linePathPoints, + markers: icon + ? [ + { + icon, + body, + color, + dimension, + position, + alignment, + }, + ] + : [], + panel: { + ...panelSize, + top, + left, + }, + }; + + lineProps.push(lineProp); + }); + }); + }); + + return lineProps; +} + +function computeXDomainLineAnnotationDimensions( + annotationSpec: LineAnnotationSpec, + xScale: Scale, + { vertical, horizontal }: SmallMultipleScales, + chartRotation: Rotation, + isHistogramMode: boolean, + axisPosition?: Position, +): AnnotationLineProps[] { + const { + id: specId, + dataValues, + marker: icon, + markerBody: body, + markerDimensions: dimension, + markerPosition: specMarkerPosition, + style, + } = annotationSpec; + const lineStyle = mergeWithDefaultAnnotationLine(style); + const color = lineStyle?.line?.stroke ?? 'red'; + + const lineProps: AnnotationLineProps[] = []; + const isHorizontalChartRotation = isHorizontalRotation(chartRotation); + const panelSize = getPanelSize({ vertical, horizontal }); + + dataValues.forEach((datum: LineAnnotationDatum, i) => { + const { dataValue } = datum; + let annotationValueXPosition = xScale.scale(dataValue); + if (isNil(annotationValueXPosition)) { + return; + } + if (isContinuousScale(xScale) && typeof dataValue === 'number') { + const [minDomain] = xScale.domain; + const maxDomain = isHistogramMode ? xScale.domain[1] + xScale.minInterval : xScale.domain[1]; + if (dataValue < minDomain || dataValue > maxDomain) { + return; + } + if (isHistogramMode) { + const offset = computeXScaleOffset(xScale, true); + const pureScaledValue = xScale.pureScale(dataValue); + if (pureScaledValue == null) { + return; + } + annotationValueXPosition = pureScaledValue - offset; + } else { + annotationValueXPosition += (xScale.bandwidth * xScale.totalBarsInCluster) / 2; + } + } else if (isBandScale(xScale)) { + if (isHistogramMode) { + const padding = (xScale.step - xScale.originalBandwidth) / 2; + annotationValueXPosition -= padding; + } else { + annotationValueXPosition += xScale.originalBandwidth / 2; + } + } else { + return; + } + if (isNaN(annotationValueXPosition) || annotationValueXPosition == null) { + return; + } + + vertical.domain.forEach((verticalValue) => { + horizontal.domain.forEach((horizontalValue) => { + if (annotationValueXPosition == null) { + return; + } + + const top = vertical.scaleOrThrow(verticalValue); + const left = horizontal.scaleOrThrow(horizontalValue); + const width = isHorizontalChartRotation ? horizontal.bandwidth : vertical.bandwidth; + const height = isHorizontalChartRotation ? vertical.bandwidth : horizontal.bandwidth; + + const linePathPoints = getXLinePath({ width, height }, annotationValueXPosition); + const alignment = getAnchorPosition(true, chartRotation, axisPosition, specMarkerPosition); + + const position = getMarkerPositionForXAnnotation( + panelSize, + chartRotation, + alignment, + annotationValueXPosition, + dimension, + ); + + const lineProp: AnnotationLineProps = { + specId, + id: getAnnotationLinePropsId(specId, datum, i, verticalValue, horizontalValue), + datum, + linePathPoints, + markers: icon + ? [ + { + icon, + body, + color, + dimension, + position, + alignment, + }, + ] + : [], + panel: { + ...panelSize, + top, + left, + }, + }; + lineProps.push(lineProp); + }); + }); + }); + + return lineProps; +} + +/** @internal */ +export function computeLineAnnotationDimensions( + annotationSpec: LineAnnotationSpec, + chartRotation: Rotation, + yScales: Map, + xScale: Scale, + smallMultipleScales: SmallMultipleScales, + isHistogramMode: boolean, + axisPosition?: Position, +): AnnotationLineProps[] | null { + const { domainType, hideLines } = annotationSpec; + + if (hideLines) { + return null; + } + + if (domainType === AnnotationDomainType.XDomain) { + return computeXDomainLineAnnotationDimensions( + annotationSpec, + xScale, + smallMultipleScales, + chartRotation, + isHistogramMode, + axisPosition, + ); + } + + const { groupId } = annotationSpec; + const yScale = yScales.get(groupId); + if (!yScale) { + return null; + } + + return computeYDomainLineAnnotationDimensions( + annotationSpec, + yScale, + smallMultipleScales, + chartRotation, + axisPosition, + ); +} + +function getAnchorPosition( + isXDomain: boolean, + chartRotation: Rotation, + axisPosition?: Position, + specMarkerPosition?: Position, +): Position { + const dflPositionFromAxis = getDefaultMarkerPositionFromAxis(isXDomain, chartRotation, axisPosition); + if (specMarkerPosition !== undefined) { + // validate specMarkerPosition against domain + const validatedPosFromMarkerPos = validateMarkerPosition(isXDomain, chartRotation, specMarkerPosition); + return validatedPosFromMarkerPos ?? dflPositionFromAxis; + } + return dflPositionFromAxis; +} + +function validateMarkerPosition(isXDomain: boolean, chartRotation: Rotation, position: Position): Position | undefined { + if ((isXDomain && isHorizontalRotation(chartRotation)) || (!isXDomain && isVerticalRotation(chartRotation))) { + return position === Position.Top || position === Position.Bottom ? position : undefined; + } + return position === Position.Left || position === Position.Right ? position : undefined; +} + +function getDefaultMarkerPositionFromAxis( + isXDomain: boolean, + chartRotation: Rotation, + axisPosition?: Position, +): Position { + if (axisPosition) { + return axisPosition; + } + if ((isXDomain && isVerticalRotation(chartRotation)) || (!isXDomain && isHorizontalRotation(chartRotation))) { + return Position.Left; + } + return Position.Bottom; +} + +function getXLinePath({ height }: Size, value: number): Line { + return { + x1: value, + y1: 0, + x2: value, + y2: height, + }; +} + +function getYLinePath({ width }: Size, value: number): Line { + return { + x1: 0, + y1: value, + x2: width, + y2: value, + }; +} + +/** @internal */ +export function getMarkerPositionForXAnnotation( + { width, height }: Size, + rotation: Rotation, + position: Position, + value: number, + { width: mWidth, height: mHeight }: Size = { width: 0, height: 0 }, +): Pick { + switch (position) { + case Position.Right: + return { + top: rotation === -90 ? height - value - mHeight / 2 : value - mHeight / 2, + left: width, + }; + case Position.Left: + return { + top: rotation === -90 ? height - value - mHeight / 2 : value - mHeight / 2, + left: -mWidth, + }; + case Position.Top: + return { + top: 0 - mHeight, + left: rotation === 180 ? width - value - mWidth / 2 : value - mWidth / 2, + }; + case Position.Bottom: + default: + return { + top: height, + left: rotation === 180 ? width - value - mWidth / 2 : value - mWidth / 2, + }; + } +} + +function getMarkerPositionForYAnnotation( + { width, height }: Size, + rotation: Rotation, + position: Position, + value: number, + { width: mWidth, height: mHeight }: Size = { width: 0, height: 0 }, +): Pick { + switch (position) { + case Position.Right: + return { + top: rotation === 180 ? height - value - mHeight / 2 : value - mHeight / 2, + left: width, + }; + case Position.Left: + return { + top: rotation === 180 ? height - value - mHeight / 2 : value - mHeight / 2, + left: -mWidth, + }; + case Position.Top: + return { + top: -mHeight, + left: rotation === 90 ? width - value - mWidth / 2 : value - mWidth / 2, + }; + case Position.Bottom: + default: + return { + top: height, + left: rotation === 90 ? width - value - mWidth / 2 : value - mWidth / 2, + }; + } +} + +/** + * @internal + */ +export function getAnnotationLinePropsId( + specId: string, + datum: LineAnnotationDatum, + index: number, + verticalValue?: any, + horizontalValue?: any, +) { + return [specId, verticalValue, horizontalValue, datum.header, datum.details, index].join('__'); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/annotations/line/line.test.tsx b/packages/osd-charts/src/chart_types/xy_chart/annotations/line/line.test.tsx new file mode 100644 index 000000000000..10d514421aa8 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/annotations/line/line.test.tsx @@ -0,0 +1,176 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { Store } from 'redux'; + +import { MockAnnotationLineProps } from '../../../../mocks/annotations/annotations'; +import { MockAnnotationSpec, MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs'; +import { MockStore } from '../../../../mocks/store'; +import { ScaleType } from '../../../../scales/constants'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { Position } from '../../../../utils/common'; +import { DEFAULT_ANNOTATION_LINE_STYLE } from '../../../../utils/themes/merge_utils'; +import { computeAnnotationDimensionsSelector } from '../../state/selectors/compute_annotations'; +import { AnnotationDomainType } from '../../utils/specs'; +import { AnnotationLineProps } from './types'; + +describe('annotation marker', () => { + const id = 'foo-line'; + const spec = MockSeriesSpec.line({ + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 10, y: 10 }, + ], + }); + const lineYDomainAnnotation = MockAnnotationSpec.line({ + id, + domainType: AnnotationDomainType.YDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + style: DEFAULT_ANNOTATION_LINE_STYLE, + marker:
, + }); + + const lineXDomainAnnotation = MockAnnotationSpec.line({ + id, + domainType: AnnotationDomainType.XDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + style: DEFAULT_ANNOTATION_LINE_STYLE, + marker:
, + }); + + let store: Store; + beforeEach(() => { + store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + }); + + test('should compute line annotation dimensions with marker if defined (y domain)', () => { + MockStore.addSpecs( + [ + spec, + MockGlobalSpec.settingsNoMargins(), + MockGlobalSpec.axis({ position: Position.Left, hide: true }), + lineYDomainAnnotation, + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + linePathPoints: { + x1: 0, + y1: 80, + x2: 100, + y2: 80, + }, + specId: 'foo-line', + datum: { dataValue: 2, details: 'foo' }, + markers: [ + { + icon:
, + color: '#777', + position: { left: -0, top: 80 }, + alignment: 'left', + }, + ], + }), + ]; + expect(dimensions.get(id)).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions with marker if defined (y domain: 180 deg rotation)', () => { + MockStore.addSpecs( + [ + spec, + MockGlobalSpec.settingsNoMargins({ rotation: 180 }), + MockGlobalSpec.axis({ position: Position.Left, hide: true }), + lineYDomainAnnotation, + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + + // we should always consider that the line, contrary to the marker + // is always rotated, if specified, at rendering time, + // so this position at 80 pixel right now, is a 20 pixel from top + // when rotated 180 degrees + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + linePathPoints: { + x1: 0, + y1: 80, + x2: 100, + y2: 80, + }, + specId: 'foo-line', + datum: { dataValue: 2, details: 'foo' }, + markers: [ + { + icon:
, + color: '#777', + position: { left: -0, top: 20 }, + alignment: 'left', + }, + ], + }), + ]; + expect(dimensions.get(id)).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions with marker if defined (x domain)', () => { + MockStore.addSpecs( + [ + spec, + MockGlobalSpec.settingsNoMargins(), + MockGlobalSpec.axis({ position: Position.Bottom, hide: true }), + lineXDomainAnnotation, + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + specId: 'foo-line', + datum: { dataValue: 2, details: 'foo' }, + linePathPoints: { + x1: 20, + y1: 0, + x2: 20, + y2: 100, + }, + markers: [ + { + icon:
, + color: '#777', + position: { top: 100, left: 20 }, + alignment: 'bottom', + }, + ], + }), + ]; + expect(dimensions.get(id)).toEqual(expectedDimensions); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/annotations/line/tooltip.test.tsx b/packages/osd-charts/src/chart_types/xy_chart/annotations/line/tooltip.test.tsx new file mode 100644 index 000000000000..8fdf2a3e14a7 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/annotations/line/tooltip.test.tsx @@ -0,0 +1,195 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; + +import { ChartType } from '../../..'; +import { Chart } from '../../../../components/chart'; +import { MockAnnotationLineProps, MockAnnotationRectProps } from '../../../../mocks/annotations/annotations'; +import { ScaleType } from '../../../../scales/constants'; +import { SpecType } from '../../../../specs/constants'; +import { Settings } from '../../../../specs/settings'; +import { Rotation } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { AnnotationId } from '../../../../utils/ids'; +import { LineAnnotation } from '../../specs/line_annotation'; +import { LineSeries } from '../../specs/line_series'; +import { AnnotationDomainType, AnnotationType, AxisSpec, RectAnnotationSpec } from '../../utils/specs'; +import { computeRectAnnotationTooltipState } from '../tooltip'; +import { AnnotationDimensions } from '../types'; +import { AnnotationLineProps } from './types'; + +describe('Annotation tooltips', () => { + describe('Line annotation tooltips', () => { + test('should show tooltip on mouseenter', () => { + const wrapper = mount( + + + + } + /> + , + ); + const annotation = wrapper.find('.echAnnotation'); + expect(annotation).toHaveLength(1); + expect(wrapper.find('.echAnnotation__tooltip')).toHaveLength(0); + annotation.simulate('mouseenter'); + const header = wrapper.find('.echAnnotation__header'); + expect(header).toHaveLength(1); + expect(header.text()).toEqual('2'); + expect(wrapper.find('.echAnnotation__details').text()).toEqual('foo'); + annotation.simulate('mouseleave'); + expect(wrapper.find('.echAnnotation__header')).toHaveLength(0); + }); + + test('should now show tooltip if hidden', () => { + const wrapper = mount( + + + + } + hideTooltips + /> + , + ); + const annotation = wrapper.find('.echAnnotation'); + expect(wrapper.find('.echAnnotation__tooltip')).toHaveLength(0); + annotation.simulate('mouseenter'); + expect(wrapper.find('.echAnnotation__header')).toHaveLength(0); + }); + }); + + test('should compute the tooltip state for rect annotation', () => { + const groupId = 'foo-group'; + const chartDimensions: Dimensions = { + width: 10, + height: 20, + top: 5, + left: 15, + }; + const annotationLines: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + specId: 'foo', + linePathPoints: { + x1: 1, + y1: 2, + x2: 3, + y2: 4, + }, + markers: [ + { + icon: React.createElement('div'), + color: 'red', + dimension: { width: 10, height: 10 }, + position: { top: 0, left: 0 }, + }, + ], + }), + ]; + const chartRotation: Rotation = 0; + const localAxesSpecs: AxisSpec[] = []; + + const annotationDimensions = new Map(); + annotationDimensions.set('foo', annotationLines); + + // rect annotation tooltip + const annotationRectangle: RectAnnotationSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Annotation, + id: 'rect', + groupId, + annotationType: AnnotationType.Rectangle, + dataValues: [{ coordinates: { x0: 1, x1: 2, y0: 3, y1: 5 } }], + }; + + const rectAnnotations: RectAnnotationSpec[] = []; + rectAnnotations.push(annotationRectangle); + + annotationDimensions.set(annotationRectangle.id, [ + MockAnnotationRectProps.default({ rect: { x: 2, y: 3, width: 3, height: 5 } }), + ]); + + const rectTooltipState = computeRectAnnotationTooltipState( + { x: 18, y: 9 }, + annotationDimensions, + rectAnnotations, + chartRotation, + localAxesSpecs, + chartDimensions, + ); + + expect(rectTooltipState).toMatchObject({ + isVisible: true, + annotationType: AnnotationType.Rectangle, + anchor: { + x: 18, + y: 9, + width: 0, + height: 0, + }, + }); + annotationRectangle.hideTooltips = true; + + const rectHideTooltipState = computeRectAnnotationTooltipState( + { x: 3, y: 4 }, + annotationDimensions, + rectAnnotations, + chartRotation, + localAxesSpecs, + chartDimensions, + ); + + expect(rectHideTooltipState).toBe(null); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/annotations/line/types.ts b/packages/osd-charts/src/chart_types/xy_chart/annotations/line/types.ts new file mode 100644 index 000000000000..f736198cd83c --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/annotations/line/types.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Line } from '../../../../geoms/types'; +import { Dimensions } from '../../../../utils/dimensions'; +import { LineAnnotationDatum } from '../../utils/specs'; +import { AnnotationMarker } from '../types'; + +/** @internal */ +export interface AnnotationLineProps { + specId: string; + id: string; + datum: LineAnnotationDatum; + /** + * The path points of a line annotation + */ + linePathPoints: Line; + markers: Array; + panel: Dimensions; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/annotations/rect/dimensions.integration.test.ts b/packages/osd-charts/src/chart_types/xy_chart/annotations/rect/dimensions.integration.test.ts new file mode 100644 index 000000000000..1511b2c5b6d1 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/annotations/rect/dimensions.integration.test.ts @@ -0,0 +1,347 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MockSeriesSpec, MockAnnotationSpec, MockGlobalSpec } from '../../../../mocks/specs'; +import { MockStore } from '../../../../mocks/store'; +import { ScaleType } from '../../../../scales/constants'; +import { computeAnnotationDimensionsSelector } from '../../state/selectors/compute_annotations'; +import { RectAnnotationDatum } from '../../utils/specs'; +import { AnnotationRectProps } from './types'; + +function expectAnnotationAtPosition( + data: Array<{ x: number; y: number }>, + type: 'line' | 'bar' | 'histogram', + dataValues: RectAnnotationDatum[], + expectedRect: { + x: number; + y: number; + width: number; + height: number; + }, + numOfSpecs = 1, + xScaleType: typeof ScaleType.Ordinal | typeof ScaleType.Linear | typeof ScaleType.Time = ScaleType.Linear, +) { + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins(); + const specs = new Array(numOfSpecs).fill(0).map((d, i) => + MockSeriesSpec.byTypePartial(type)({ + id: `spec_${i}`, + xScaleType, + data, + }), + ); + const annotation = MockAnnotationSpec.rect({ + dataValues, + }); + + MockStore.addSpecs([settings, ...specs, annotation], store); + const annotations = computeAnnotationDimensionsSelector(store.getState()); + const renderedAnnotations = annotations.get(annotation.id)!; + expect(renderedAnnotations.length).toBe(1); + const { rect } = renderedAnnotations[0] as AnnotationRectProps; + expect(rect.x).toBeCloseTo(expectedRect.x, 3); + expect(rect.y).toBeCloseTo(expectedRect.y, 3); + expect(rect.width).toBeCloseTo(expectedRect.width, 3); + expect(rect.height).toBeCloseTo(expectedRect.height, 3); +} + +describe('Render rect annotation within', () => { + it.each` + x0 | numOfSpecs | x | width + ${0} | ${1} | ${0} | ${100} + ${1} | ${1} | ${25} | ${75} + ${2} | ${1} | ${50} | ${50} + ${3} | ${1} | ${75} | ${25} + ${1} | ${2} | ${25} | ${75} + ${2} | ${3} | ${50} | ${50} + `('bars starting from $x0, $numOfSpecs specs, all scales', ({ x0, numOfSpecs, x, width }) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x0 }, + }, + ]; + const rect = { x, width, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs, ScaleType.Ordinal); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs, ScaleType.Time); + }); + + it.each` + x1 | numOfSpecs | x | width + ${0} | ${1} | ${0} | ${25} + ${1} | ${1} | ${0} | ${50} + ${2} | ${1} | ${0} | ${75} + ${3} | ${1} | ${0} | ${100} + ${1} | ${2} | ${0} | ${50} + ${2} | ${2} | ${0} | ${75} + `('bars starting ending at $x1, $numOfSpecs specs, all scales', ({ x1, numOfSpecs, x, width }) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x1 }, + }, + ]; + const rect = { x, width, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs, ScaleType.Ordinal); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs, ScaleType.Time); + }); + + it.each` + x0 | x1 | numOfSpecs | x | width + ${0} | ${0} | ${1} | ${0} | ${25} + ${0} | ${1} | ${1} | ${0} | ${50} + ${1} | ${3} | ${1} | ${25} | ${75} + ${0} | ${1} | ${2} | ${0} | ${50} + ${1} | ${3} | ${3} | ${25} | ${75} + `('bars starting at $x0, ending at $x1, $numOfSpecs specs, all scales', ({ x0, x1, numOfSpecs, x, width }) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x0, x1 }, + }, + ]; + const rect = { x, width, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs, ScaleType.Ordinal); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs, ScaleType.Time); + }); + + it.each` + x0 | x1 | numOfSpecs | x | width + ${0} | ${0} | ${1} | ${0} | ${25} + ${0} | ${1} | ${1} | ${0} | ${50} + ${1} | ${3} | ${1} | ${25} | ${75} + ${0} | ${1} | ${2} | ${0} | ${50} + ${1} | ${3} | ${3} | ${25} | ${75} + `('lines starting at $x0, ending at $x1, $numOfSpecs specs, ordinal scale', ({ x0, x1, numOfSpecs, x, width }) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x0, x1 }, + }, + ]; + const rect = { x, width, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'line', dataValues, rect, numOfSpecs, ScaleType.Ordinal); + }); + + it.each` + x0 | x1 | numOfSpecs | x | width + ${0} | ${0} | ${1} | ${0} | ${0} + ${0} | ${1} | ${1} | ${0} | ${50} + ${1} | ${2} | ${1} | ${50} | ${50} + ${0} | ${2} | ${1} | ${0} | ${100} + ${0} | ${1} | ${2} | ${0} | ${50} + ${1} | ${2} | ${3} | ${50} | ${50} + `( + 'on line starting at $x0, ending at $x1, $numOfSpecs specs, continuous scale', + ({ x0, x1, numOfSpecs, x, width }) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x0, x1 }, + }, + ]; + const rect = { x, width, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'line', dataValues, rect, numOfSpecs, ScaleType.Linear); + }, + ); + it.each` + x0 | x1 | numOfSpecs | x | width + ${0} | ${0} | ${1} | ${0} | ${0} + ${0} | ${1} | ${1} | ${0} | ${25} + ${1} | ${2} | ${1} | ${25} | ${25} + ${0} | ${2} | ${1} | ${0} | ${50} + ${0} | ${1} | ${2} | ${0} | ${25} + ${1} | ${2} | ${3} | ${25} | ${25} + `( + 'on histogram starting at $x0, ending at $x1, $numOfSpecs specs, continuous scale', + ({ x0, x1, numOfSpecs, x, width }) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 3 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x0, x1 }, + }, + ]; + const rect = { x, width, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'histogram', dataValues, rect, numOfSpecs, ScaleType.Linear); + }, + ); + + it.each` + prop | x | y | width | height + ${'x0'} | ${50} | ${0} | ${50} | ${100} + ${'x1'} | ${0} | ${0} | ${50} | ${100} + ${'y0'} | ${0} | ${0} | ${100} | ${75} + ${'y1'} | ${0} | ${75} | ${100} | ${25} + `('expand annotation with only one prop configured: $prop', ({ prop, x, y, width, height }) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 2 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { [prop]: 1 }, + }, + ]; + const rect = { x, width, y, height }; + expectAnnotationAtPosition(data, 'line', dataValues, rect, 1, ScaleType.Linear); + }); + + it.each` + value | prop + ${10} | ${'y1'} + ${-4} | ${'y0'} + ${-4} | ${'x0'} + ${5} | ${'x1'} + `('out of bound annotations for $prop', ({ prop, value }) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { [prop]: value }, + }, + ]; + const rect = { x: 0, width: 100, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'line', dataValues, rect, 1, ScaleType.Linear); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, 1, ScaleType.Linear); + }); + + it('annotation with no height will take the chart dimension height', () => { + const height = 200; + const store = MockStore.default({ top: 0, left: 0, width: 20, height }); + const settings = MockGlobalSpec.settingsNoMargins(); + const annotation = MockAnnotationSpec.rect({ + dataValues: [{ coordinates: { x0: 2, x1: 4 } }], + }); + const barWithYNoHeight = MockSeriesSpec.bar({ + data: [ + { x: 1, y: 0 }, + { x: 2, y: 0 }, + { x: 4, y: 0 }, + ], + }); + MockStore.addSpecs([settings, annotation, barWithYNoHeight], store); + const expected = computeAnnotationDimensionsSelector(store.getState()); + const [resultAnnotation] = expected.get('rect_annotation_1') ?? []; + expect(resultAnnotation).toMatchObject({ + rect: { height }, + }); + }); + it('annotation with fit domain will render', () => { + const heightFromStore = 200; + const store = MockStore.default({ top: 0, left: 0, width: 20, height: heightFromStore }); + const settings = MockGlobalSpec.settingsNoMargins(); + const yDomainFitted = MockGlobalSpec.axis({ domain: { fit: true } }); + const annotation = MockAnnotationSpec.rect({ + dataValues: [{ coordinates: { x0: 2, x1: 4 } }], + }); + const bar = MockSeriesSpec.bar({ + data: [ + { x: 1, y: 0 }, + { x: 2, y: 5 }, + { x: 4, y: 10 }, + ], + }); + MockStore.addSpecs([settings, yDomainFitted, annotation, bar], store); + const expected = computeAnnotationDimensionsSelector(store.getState()); + const [resultAnnotation] = expected.get('rect_annotation_1') ?? []; + expect(resultAnnotation).toMatchObject({ + rect: { height: 190 }, + }); + }); + it('annotation with group id should render with x0 and x1 values', () => { + const height = 200; + const store = MockStore.default({ top: 0, left: 0, width: 20, height }); + const settings = MockGlobalSpec.settingsNoMargins(); + const annotation = MockAnnotationSpec.rect({ + groupId: 'group1', + dataValues: [{ coordinates: { x0: 2, x1: 4 } }], + }); + const bar = MockSeriesSpec.bar({ + data: [ + { x: 1, y: 0 }, + { x: 2, y: 5 }, + { x: 4, y: 10 }, + ], + }); + MockStore.addSpecs([settings, annotation, bar], store); + const expected = computeAnnotationDimensionsSelector(store.getState()); + const [resultAnnotation] = expected.get('rect_annotation_1') ?? []; + expect(resultAnnotation).toMatchObject({ + rect: { height }, + }); + }); + it('annotation with no group id should render', () => { + const height = 200; + const store = MockStore.default({ top: 0, left: 0, width: 20, height }); + const settings = MockGlobalSpec.settingsNoMargins(); + const annotation = MockAnnotationSpec.rect({ + groupId: undefined, + dataValues: [{ coordinates: { x0: 2, x1: 4 } }], + }); + const bar = MockSeriesSpec.bar({ + data: [ + { x: 1, y: 0 }, + { x: 2, y: 5 }, + { x: 4, y: 10 }, + ], + }); + MockStore.addSpecs([settings, annotation, bar], store); + const expected = computeAnnotationDimensionsSelector(store.getState()); + const [resultAnnotation] = expected.get('rect_annotation_1') ?? []; + expect(resultAnnotation).toMatchObject({ + rect: { height }, + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/annotations/rect/dimensions.test.ts b/packages/osd-charts/src/chart_types/xy_chart/annotations/rect/dimensions.test.ts new file mode 100644 index 000000000000..1eb90d7d9f7a --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/annotations/rect/dimensions.test.ts @@ -0,0 +1,105 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MockAnnotationSpec, MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs/specs'; +import { MockStore } from '../../../../mocks/store/store'; +import { ScaleType } from '../../../../scales/constants'; +import { computeAnnotationDimensionsSelector } from '../../state/selectors/compute_annotations'; +import { isWithinRectBounds } from './dimensions'; +import { AnnotationRectProps } from './types'; + +describe('Rect Annotation Dimensions', () => { + const continuousBarChart = MockSeriesSpec.area({ + xScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + { x: 3, y: 10 }, + { x: 4, y: 5 }, + { x: 10, y: 10 }, + ], + }); + + test('should skip computing rectangle annotation dimensions when annotation data invalid', () => { + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins(); + + const annotationRectangle = MockAnnotationSpec.rect({ + id: 'rect', + dataValues: [ + { coordinates: { x0: 1, x1: 2, y0: -10, y1: 5 } }, + { coordinates: { x0: null, x1: null, y0: null, y1: null } }, + ], + }); + + MockStore.addSpecs([settings, continuousBarChart, annotationRectangle], store); + const skippedInvalid = computeAnnotationDimensionsSelector(store.getState()); + expect(skippedInvalid.size).toBe(1); + }); + + test('should compute rectangle dimensions shifted for histogram mode', () => { + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins(); + + const annotationRectangle = MockAnnotationSpec.rect({ + id: 'rect', + dataValues: [ + { coordinates: { x0: 1, x1: null, y0: null, y1: null } }, + { coordinates: { x0: null, x1: 1, y0: null, y1: null } }, + { coordinates: { x0: null, x1: null, y0: 1, y1: null } }, + { coordinates: { x0: null, x1: null, y0: null, y1: 1 } }, + ], + }); + + MockStore.addSpecs([settings, continuousBarChart, annotationRectangle], store); + const [dims1, dims2, dims3, dims4] = computeAnnotationDimensionsSelector(store.getState()).get( + 'rect', + ) as AnnotationRectProps[]; + + expect(dims1.rect.x).toBe(10); + expect(dims1.rect.width).toBeCloseTo(90); + expect(dims1.rect.y).toBe(0); + expect(dims1.rect.height).toBe(100); + + expect(dims2.rect.x).toBe(0); + expect(dims2.rect.width).toBe(10); + expect(dims2.rect.y).toBe(0); + expect(dims2.rect.height).toBe(100); + + expect(dims3.rect.x).toBe(0); + expect(dims3.rect.width).toBe(100); + expect(dims3.rect.y).toBe(0); + expect(dims3.rect.height).toBe(90); + + expect(dims4.rect.x).toBe(0); + expect(dims4.rect.width).toBeCloseTo(100); + expect(dims4.rect.y).toBe(90); + expect(dims4.rect.height).toBe(10); + }); + + test('should determine if a point is within a rectangle annotation', () => { + expect(isWithinRectBounds({ x: 3, y: 4 }, { startX: 2, endX: 4, startY: 3, endY: 5 })).toBe(true); + // TODO check I've a doubt that this should be an error + expect(isWithinRectBounds({ x: 3, y: 4 }, { startX: 2, endX: 4, startY: 5, endY: 3 })).toBe(false); + expect(isWithinRectBounds({ x: 3, y: 4 }, { startX: 2, endX: 4, startY: 5, endY: 6 })).toBe(false); + + expect(isWithinRectBounds({ x: 3, y: 4 }, { startX: 4, endX: 5, startY: 3, endY: 5 })).toBe(false); + expect(isWithinRectBounds({ x: 3, y: 4 }, { startX: 4, endX: 2, startY: 3, endY: 5 })).toBe(false); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/annotations/rect/dimensions.ts b/packages/osd-charts/src/chart_types/xy_chart/annotations/rect/dimensions.ts new file mode 100644 index 000000000000..a312412d2965 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/annotations/rect/dimensions.ts @@ -0,0 +1,244 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Scale, ScaleBand, ScaleContinuous } from '../../../../scales'; +import { isBandScale, isContinuousScale } from '../../../../scales/types'; +import { isDefined } from '../../../../utils/common'; +import { GroupId } from '../../../../utils/ids'; +import { Point } from '../../../../utils/point'; +import { PrimitiveValue } from '../../../partition_chart/layout/utils/group_by_rollup'; +import { SmallMultipleScales } from '../../state/selectors/compute_small_multiple_scales'; +import { getPanelSize } from '../../utils/panel'; +import { RectAnnotationDatum, RectAnnotationSpec } from '../../utils/specs'; +import { Bounds } from '../types'; +import { AnnotationRectProps } from './types'; + +/** @internal */ +export function isWithinRectBounds({ x, y }: Point, { startX, endX, startY, endY }: Bounds): boolean { + const withinXBounds = x >= startX && x <= endX; + const withinYBounds = y >= startY && y <= endY; + + return withinXBounds && withinYBounds; +} + +/** @internal */ +export function computeRectAnnotationDimensions( + annotationSpec: RectAnnotationSpec, + yScales: Map, + xScale: Scale, + smallMultiplesScales: SmallMultipleScales, + isHistogram: boolean = false, +): AnnotationRectProps[] | null { + const { dataValues, groupId } = annotationSpec; + const yScale = yScales.get(groupId); + + const rectsProps: Omit[] = []; + const panelSize = getPanelSize(smallMultiplesScales); + dataValues.forEach((datum: RectAnnotationDatum) => { + const { x0: initialX0, x1: initialX1, y0: initialY0, y1: initialY1 } = datum.coordinates; + + // if everything is null, return; otherwise we coerce the other coordinates + if (initialX0 == null && initialX1 == null && initialY0 == null && initialY1 == null) { + return; + } + let height: number | undefined; + + const [x0, x1] = limitValueToDomainRange(xScale, initialX0, initialX1, isHistogram); + // something is wrong with the data types, don't draw this annotation + if (x0 == null || x1 == null) { + return; + } + + let xAndWidth: { x: number; width: number } | null = null; + + if (isBandScale(xScale)) { + xAndWidth = scaleXonBandScale(xScale, x0, x1); + } else if (isContinuousScale(xScale)) { + xAndWidth = scaleXonContinuousScale(xScale, x0, x1, isHistogram); + } + // something is wrong with scales, don't draw + if (!xAndWidth) { + return; + } + if (!yScale) { + if (!isDefined(initialY0) && !isDefined(initialY1)) { + const rectDimensions = { + ...xAndWidth, + y: 0, + height: panelSize.height, + }; + + rectsProps.push({ + rect: rectDimensions, + datum, + }); + } + return; + } + + const [y0, y1] = limitValueToDomainRange(yScale, initialY0, initialY1); + // something is wrong with the data types, don't draw this annotation + if (y0 == null || y1 == null) { + return; + } + + let scaledY1 = yScale.pureScale(y1); + const scaledY0 = yScale.pureScale(y0); + if (scaledY1 == null || scaledY0 == null) { + return; + } + height = Math.abs(scaledY0 - scaledY1); + // if the annotation height is 0 override it with the height from chart dimension and if the values in the domain are the same + if (height === 0 && yScale.domain.length === 2 && yScale.domain[0] === yScale.domain[1]) { + // eslint-disable-next-line prefer-destructuring + height = panelSize.height; + scaledY1 = 0; + } + + const rectDimensions = { + ...xAndWidth, + y: scaledY1, + height, + }; + + rectsProps.push({ + rect: rectDimensions, + datum, + }); + }); + + return rectsProps.reduce((acc, props) => { + const duplicated: AnnotationRectProps[] = []; + smallMultiplesScales.vertical.domain.forEach((vDomainValue) => { + smallMultiplesScales.horizontal.domain.forEach((hDomainValue) => { + const panel = { + ...panelSize, + top: smallMultiplesScales.vertical.scaleOrThrow(vDomainValue), + left: smallMultiplesScales.horizontal.scaleOrThrow(hDomainValue), + }; + duplicated.push({ ...props, panel }); + }); + }); + return [...acc, ...duplicated]; + }, []); +} + +function scaleXonBandScale( + xScale: ScaleBand, + x0: PrimitiveValue, + x1: PrimitiveValue, +): { x: number; width: number } | null { + // the band scale return the start of the band, we need to cover + // also the inner padding of the bar + const padding = (xScale.step - xScale.originalBandwidth) / 2; + let scaledX1 = xScale.scale(x1); + let scaledX0 = xScale.scale(x0); + if (scaledX1 == null || scaledX0 == null) { + return null; + } + // extend the x1 scaled value to fully cover the last bar + scaledX1 += xScale.originalBandwidth + padding; + // give the x1 value a maximum of the chart range + if (scaledX1 > xScale.range[1]) { + [, scaledX1] = xScale.range; + } + + scaledX0 -= padding; + if (scaledX0 < xScale.range[0]) { + [scaledX0] = xScale.range; + } + const width = Math.abs(scaledX1 - scaledX0); + return { + x: scaledX0, + width, + }; +} + +function scaleXonContinuousScale( + xScale: ScaleContinuous, + x0: PrimitiveValue, + x1: PrimitiveValue, + isHistogramModeEnabled: boolean = false, +): { x: number; width: number } | null { + if (typeof x1 !== 'number' || typeof x0 !== 'number') { + return null; + } + const scaledX0 = xScale.scale(x0); + const scaledX1: number | null = + xScale.totalBarsInCluster > 0 && !isHistogramModeEnabled ? xScale.scale(x1 + xScale.minInterval) : xScale.scale(x1); + if (scaledX1 == null || scaledX0 == null) { + return null; + } + // the width needs to be computed before adjusting the x anchor + const width = Math.abs(scaledX1 - scaledX0); + return { + x: scaledX0 - (xScale.bandwidthPadding / 2) * xScale.totalBarsInCluster, + width, + }; +} + +/** + * This function extend and limits the values in a scale domain + * @param scale the scale + * @param minValue a min value + * @param maxValue a max value + * @param isHistogram + */ +function limitValueToDomainRange( + scale: Scale, + minValue?: PrimitiveValue, + maxValue?: PrimitiveValue, + isHistogram = false, +): [PrimitiveValue, PrimitiveValue] { + const [domainStartValue] = scale.domain; + // this fix the case where rendering on categorical scale and we have only one element + const domainEndValue = scale.domain.length > 0 ? scale.domain[scale.domain.length - 1] : scale.domain[0]; + + const min = getMin(domainStartValue, minValue); + + const max = getMax(isHistogram ? domainEndValue + scale.minInterval : domainEndValue, maxValue); + // extend to edge values if values are null/undefined + if (!isContinuousScale(scale)) { + return [min, max]; + } + if (min !== null && max !== null && min > max) { + return [null, null]; + } + return [min, max]; +} + +function getMax(max: number, value?: number | string | null) { + if (value == null) { + return max; + } + if (typeof value === 'number') { + return Math.min(value, max); + } + return value; +} + +function getMin(min: number, value?: number | string | null) { + if (value == null) { + return min; + } + if (typeof value === 'number') { + return Math.max(value, min); + } + return value; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/annotations/rect/tooltip.test.ts b/packages/osd-charts/src/chart_types/xy_chart/annotations/rect/tooltip.test.ts new file mode 100644 index 000000000000..b4a35ccdb31a --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/annotations/rect/tooltip.test.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MockAnnotationRectProps } from '../../../../mocks/annotations/annotations'; +import { Dimensions } from '../../../../utils/dimensions'; +import { AnnotationType } from '../../utils/specs'; +import { AnnotationTooltipState } from '../types'; +import { getRectAnnotationTooltipState } from './tooltip'; +import { AnnotationRectProps } from './types'; + +describe('Rect annotation tooltip', () => { + test('should compute tooltip state for rect annotation', () => { + const chartDimensions: Dimensions = { + width: 10, + height: 20, + top: 5, + left: 15, + }; + const cursorPosition = { x: 18, y: 9 }; + const annotationRects: AnnotationRectProps[] = [ + MockAnnotationRectProps.default({ + rect: { x: 2, y: 3, width: 3, height: 5 }, + panel: { top: 0, left: 0, width: 10, height: 20 }, + datum: { coordinates: { x0: 0, x1: 10, y0: 0, y1: 10 } }, + }), + ]; + + const visibleTooltip = getRectAnnotationTooltipState(cursorPosition, annotationRects, 0, chartDimensions); + const expectedVisibleTooltipState: AnnotationTooltipState = { + isVisible: true, + annotationType: AnnotationType.Rectangle, + anchor: { + x: cursorPosition.x, + y: cursorPosition.y, + width: 0, + height: 0, + }, + datum: { coordinates: { x0: 0, x1: 10, y0: 0, y1: 10 } }, + }; + + expect(visibleTooltip).toEqual(expectedVisibleTooltipState); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/annotations/rect/tooltip.ts b/packages/osd-charts/src/chart_types/xy_chart/annotations/rect/tooltip.ts new file mode 100644 index 000000000000..fa241c597e54 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/annotations/rect/tooltip.ts @@ -0,0 +1,100 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Rect } from '../../../../geoms/types'; +import { Rotation } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { Point } from '../../../../utils/point'; +import { isHorizontalRotation } from '../../state/utils/common'; +import { AnnotationType } from '../../utils/specs'; +import { AnnotationTooltipState, Bounds } from '../types'; +import { isWithinRectBounds } from './dimensions'; +import { AnnotationRectProps } from './types'; + +/** @internal */ +export function getRectAnnotationTooltipState( + cursorPosition: Point, + annotationRects: AnnotationRectProps[], + rotation: Rotation, + chartDimensions: Dimensions, +): AnnotationTooltipState | null { + const totalAnnotationRect = annotationRects.length; + + for (let i = 0; i < totalAnnotationRect; i++) { + const rectProps = annotationRects[i]; + const { panel, datum } = rectProps; + + const rect = transformRotateRect(rectProps.rect, rotation, panel); + + const startX = rect.x + chartDimensions.left + panel.left; + const endX = startX + rect.width; + const startY = rect.y + chartDimensions.top + panel.top; + const endY = startY + rect.height; + const bounds: Bounds = { startX, endX, startY, endY }; + const isWithinBounds = isWithinRectBounds(cursorPosition, bounds); + if (isWithinBounds) { + return { + isVisible: true, + annotationType: AnnotationType.Rectangle, + anchor: { + x: cursorPosition.x, + y: cursorPosition.y, + width: 0, + height: 0, + }, + datum, + }; + } + } + + return null; +} + +function transformRotateRect(rect: Rect, rotation: Rotation, dim: Dimensions): Rect { + const isHorizontalRotated = isHorizontalRotation(rotation); + const width = isHorizontalRotated ? dim.width : dim.height; + const height = isHorizontalRotated ? dim.height : dim.width; + + switch (rotation) { + case 90: + return { + x: height - rect.height - rect.y, + y: rect.x, + width: rect.height, + height: rect.width, + }; + case -90: + return { + x: rect.y, + y: width - rect.x - rect.width, + width: rect.height, + height: rect.width, + }; + case 180: + return { + x: width - rect.x - rect.width, + y: height - rect.y - rect.height, + width: rect.width, + height: rect.height, + }; + case 0: + default: + return rect; + } +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/annotations/rect/types.ts b/packages/osd-charts/src/chart_types/xy_chart/annotations/rect/types.ts new file mode 100644 index 000000000000..a604fa8a38ef --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/annotations/rect/types.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Dimensions } from '../../../../utils/dimensions'; +import { RectAnnotationDatum } from '../../utils/specs'; + +/** + * @internal + */ +export interface AnnotationRectProps { + datum: RectAnnotationDatum; + rect: { + x: number; + y: number; + width: number; + height: number; + }; + panel: Dimensions; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/annotations/tooltip.ts b/packages/osd-charts/src/chart_types/xy_chart/annotations/tooltip.ts new file mode 100644 index 000000000000..aea89dfc8da0 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/annotations/tooltip.ts @@ -0,0 +1,87 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TooltipPortalSettings } from '../../../components/portal'; +import { Rotation } from '../../../utils/common'; +import { Dimensions } from '../../../utils/dimensions'; +import { AnnotationId } from '../../../utils/ids'; +import { Point } from '../../../utils/point'; +import { AnnotationSpec, AxisSpec, isRectAnnotation } from '../utils/specs'; +import { getRectAnnotationTooltipState } from './rect/tooltip'; +import { AnnotationRectProps } from './rect/types'; +import { AnnotationDimensions, AnnotationTooltipState } from './types'; + +/** @internal */ +export function computeRectAnnotationTooltipState( + cursorPosition: Point, + annotationDimensions: Map, + annotationSpecs: AnnotationSpec[], + chartRotation: Rotation, + axesSpecs: AxisSpec[], + chartDimensions: Dimensions, +): AnnotationTooltipState | null { + // allow picking up the last spec added as the top most or use it's zIndex value + const sortedAnnotationSpecs = annotationSpecs + .filter(isRectAnnotation) + .reverse() + .sort(({ zIndex: a = Number.MIN_SAFE_INTEGER }, { zIndex: b = Number.MIN_SAFE_INTEGER }) => b - a); + + for (let i = 0; i < sortedAnnotationSpecs.length; i++) { + const spec = sortedAnnotationSpecs[i]; + const annotationDimension = annotationDimensions.get(spec.id); + if (spec.hideTooltips || !annotationDimension) { + continue; + } + const { customTooltip, customTooltipDetails } = spec; + + const tooltipSettings = getTooltipSettings(spec); + + const rectAnnotationTooltipState = getRectAnnotationTooltipState( + cursorPosition, + annotationDimension as AnnotationRectProps[], + chartRotation, + chartDimensions, + ); + + if (rectAnnotationTooltipState) { + return { + ...rectAnnotationTooltipState, + tooltipSettings, + customTooltip, + customTooltipDetails: customTooltipDetails ?? spec.renderTooltip, + }; + } + } + + return null; +} + +function getTooltipSettings({ + placement, + fallbackPlacements, + boundary, + offset, +}: AnnotationSpec): TooltipPortalSettings<'chart'> { + return { + placement, + fallbackPlacements, + boundary, + offset, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/annotations/types.ts b/packages/osd-charts/src/chart_types/xy_chart/annotations/types.ts new file mode 100644 index 000000000000..1263f0ac2ba2 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/annotations/types.ts @@ -0,0 +1,98 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ComponentType, ReactNode } from 'react'; + +import { TooltipPortalSettings } from '../../../components/portal'; +import { Position, Color } from '../../../utils/common'; +import { AnnotationType, LineAnnotationDatum, RectAnnotationDatum } from '../utils/specs'; +import { AnnotationLineProps } from './line/types'; +import { AnnotationRectProps } from './rect/types'; + +/** @public */ +export type AnnotationTooltipFormatter = (details?: string) => JSX.Element | null; + +/** @public */ +export type CustomAnnotationTooltip = ComponentType<{ + header?: string; + details?: string; + datum: LineAnnotationDatum | RectAnnotationDatum; +}> | null; + +/** + * The header and description strings for an Annotation + * @internal + */ +export interface AnnotationDetails { + headerText?: string; + detailsText?: string; +} + +/** + * Component to render based on annotation datum + * + * @public + */ +export type ComponentWithAnnotationDatum = ComponentType; + +/** + * The marker for an Annotation. Usually a JSX element + * @internal + */ +export interface AnnotationMarker { + icon?: ReactNode | ComponentWithAnnotationDatum; + position: { + top: number; + left: number; + }; + dimension?: { + width: number; + height: number; + }; + body?: ReactNode | ComponentWithAnnotationDatum; + alignment: Position; + color: Color; +} + +/** @internal */ +export interface AnnotationTooltipState { + isVisible: true; + annotationType: AnnotationType; + datum: LineAnnotationDatum | RectAnnotationDatum; + anchor: { + x: number; + y: number; + width: number; + height: number; + }; + customTooltipDetails?: AnnotationTooltipFormatter; + customTooltip?: CustomAnnotationTooltip; + tooltipSettings?: TooltipPortalSettings<'chart'>; +} + +/** @internal */ +export type AnnotationDimensions = Array; + +/** @internal */ +export type Bounds = { + startX: number; + endX: number; + startY: number; + endY: number; +}; diff --git a/packages/osd-charts/src/chart_types/xy_chart/annotations/utils.test.ts b/packages/osd-charts/src/chart_types/xy_chart/annotations/utils.test.ts new file mode 100644 index 000000000000..6d82500e8bcf --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/annotations/utils.test.ts @@ -0,0 +1,96 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MockGlobalSpec } from '../../../mocks/specs'; +import { Position, Rotation } from '../../../utils/common'; +import { Dimensions } from '../../../utils/dimensions'; +import { AnnotationDomainType } from '../utils/specs'; +import { getAnnotationAxis, getTransformedCursor, invertTransformedCursor } from './utils'; + +describe('Annotation utils', () => { + const groupId = 'foo-group'; + + const verticalAxisSpec = MockGlobalSpec.axis({ + id: 'vertical_axis', + groupId, + position: Position.Left, + }); + const horizontalAxisSpec = MockGlobalSpec.axis({ + id: 'vertical_axis', + groupId, + position: Position.Bottom, + }); + + test('should get associated axis for an annotation', () => { + const noAxis = getAnnotationAxis([], groupId, AnnotationDomainType.XDomain, 0); + expect(noAxis).toBeUndefined(); + + const localAxesSpecs = [horizontalAxisSpec, verticalAxisSpec]; + + const xAnnotationAxisPosition = getAnnotationAxis(localAxesSpecs, groupId, AnnotationDomainType.XDomain, 0); + expect(xAnnotationAxisPosition).toEqual(Position.Bottom); + + const yAnnotationAxisPosition = getAnnotationAxis(localAxesSpecs, groupId, AnnotationDomainType.YDomain, 0); + expect(yAnnotationAxisPosition).toEqual(Position.Left); + }); + + test('should get rotated cursor position', () => { + const cursorPosition = { x: 1, y: 2 }; + const chartDimensions: Dimensions = { + width: 10, + height: 20, + top: 5, + left: 15, + }; + expect(getTransformedCursor(cursorPosition, chartDimensions, 0)).toEqual(cursorPosition); + expect(getTransformedCursor(cursorPosition, chartDimensions, 90)).toEqual({ x: 2, y: 9 }); + expect(getTransformedCursor(cursorPosition, chartDimensions, -90)).toEqual({ x: 18, y: 1 }); + expect(getTransformedCursor(cursorPosition, chartDimensions, 180)).toEqual({ x: 9, y: 18 }); + }); + + describe('#invertTranformedCursor', () => { + const cursorPosition = { x: 1, y: 2 }; + const chartDimensions: Dimensions = { + width: 10, + height: 20, + top: 5, + left: 15, + }; + it.each([0, 90, -90, 180])('Should invert rotated cursor - rotation %d', (rotation) => { + expect( + invertTransformedCursor( + getTransformedCursor(cursorPosition, chartDimensions, rotation), + chartDimensions, + rotation, + ), + ).toEqual(cursorPosition); + }); + + it.each([0, 90, -90, 180])('Should invert rotated projected cursor - rotation %d', (rotation) => { + expect( + invertTransformedCursor( + getTransformedCursor(cursorPosition, chartDimensions, rotation, true), + chartDimensions, + rotation, + true, + ), + ).toEqual(cursorPosition); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/annotations/utils.ts b/packages/osd-charts/src/chart_types/xy_chart/annotations/utils.ts new file mode 100644 index 000000000000..1751f48b5c7e --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/annotations/utils.ts @@ -0,0 +1,173 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Scale } from '../../../scales'; +import { Rotation, Position } from '../../../utils/common'; +import { Dimensions } from '../../../utils/dimensions'; +import { AnnotationId, GroupId } from '../../../utils/ids'; +import { Point } from '../../../utils/point'; +import { SmallMultipleScales } from '../state/selectors/compute_small_multiple_scales'; +import { isHorizontalRotation } from '../state/utils/common'; +import { getAxesSpecForSpecId } from '../state/utils/spec'; +import { AnnotationDomainType, AnnotationSpec, AxisSpec, isLineAnnotation } from '../utils/specs'; +import { computeLineAnnotationDimensions } from './line/dimensions'; +import { computeRectAnnotationDimensions } from './rect/dimensions'; +import { AnnotationDimensions } from './types'; + +/** @internal */ +export function getAnnotationAxis( + axesSpecs: AxisSpec[], + groupId: GroupId, + domainType: AnnotationDomainType, + chartRotation: Rotation, +): Position | undefined { + const { xAxis, yAxis } = getAxesSpecForSpecId(axesSpecs, groupId); + const isHorizontalRotated = isHorizontalRotation(chartRotation); + const isXDomainAnnotation = isXDomain(domainType); + const annotationAxis = isXDomainAnnotation ? xAxis : yAxis; + const rotatedAnnotation = isHorizontalRotated ? annotationAxis : isXDomainAnnotation ? yAxis : xAxis; + return rotatedAnnotation ? rotatedAnnotation.position : undefined; +} + +/** @internal */ +export function isXDomain(domainType: AnnotationDomainType): boolean { + return domainType === AnnotationDomainType.XDomain; +} + +/** @internal */ +export function getTransformedCursor( + cursorPosition: Point, + chartDimensions: Dimensions, + chartRotation: Rotation | null, + /** + * getTransformedCursor to account for projected cursor position relative to chart dimensions + */ + projectArea = false, +): Point { + const { height, width, left, top } = chartDimensions; + let { x, y } = cursorPosition; + + if (projectArea) { + x = cursorPosition.x - left; + y = cursorPosition.y - top; + } + + if (chartRotation === null) { + return { x, y }; + } + + switch (chartRotation) { + case 90: + return { x: y, y: width - x }; + case -90: + return { x: height - y, y: x }; + case 180: + return { x: width - x, y: height - y }; + case 0: + default: + return { x, y }; + } +} + +/** @internal */ +export function invertTransformedCursor( + cursorPosition: Point, + chartDimensions: Dimensions, + chartRotation: Rotation | null, + /** + * Used to account for projected cursor position relative to chart dimensions + */ + projectArea = false, +): Point { + const { height, width, left, top } = chartDimensions; + let { x, y } = cursorPosition; + + switch (chartRotation) { + case 0: + case null: + break; + case 90: + x = width - cursorPosition.y; + y = cursorPosition.x; + break; + case -90: + y = height - cursorPosition.x; + x = cursorPosition.y; + break; + case 180: + default: + y = height - cursorPosition.y; + x = width - cursorPosition.x; + } + + if (projectArea) { + x += left; + y += top; + } + + return { x, y }; +} + +/** @internal */ +export function computeAnnotationDimensions( + annotations: AnnotationSpec[], + chartDimensions: Dimensions, + chartRotation: Rotation, + yScales: Map, + xScale: Scale, + axesSpecs: AxisSpec[], + isHistogramModeEnabled: boolean, + smallMultipleScales: SmallMultipleScales, +): Map { + return annotations.reduce>((annotationDimensions, annotationSpec) => { + const { id } = annotationSpec; + + if (isLineAnnotation(annotationSpec)) { + const { groupId, domainType } = annotationSpec; + const annotationAxisPosition = getAnnotationAxis(axesSpecs, groupId, domainType, chartRotation); + + const dimensions = computeLineAnnotationDimensions( + annotationSpec, + chartRotation, + yScales, + xScale, + smallMultipleScales, + isHistogramModeEnabled, + annotationAxisPosition, + ); + if (dimensions) { + annotationDimensions.set(id, dimensions); + } + return annotationDimensions; + } + + const dimensions = computeRectAnnotationDimensions( + annotationSpec, + yScales, + xScale, + smallMultipleScales, + isHistogramModeEnabled, + ); + + if (dimensions) { + annotationDimensions.set(id, dimensions); + } + return annotationDimensions; + }, new Map()); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/axes/axes_sizes.ts b/packages/osd-charts/src/chart_types/xy_chart/axes/axes_sizes.ts new file mode 100644 index 000000000000..00d60f5ef832 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/axes/axes_sizes.ts @@ -0,0 +1,116 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SmallMultiplesSpec } from '../../../specs'; +import { Position } from '../../../utils/common'; +import { getSimplePadding } from '../../../utils/dimensions'; +import { AxisId } from '../../../utils/ids'; +import { AxisStyle, Theme } from '../../../utils/themes/theme'; +import { getSpecsById } from '../state/utils/spec'; +import { isVerticalAxis } from '../utils/axis_type_utils'; +import { AxisTicksDimensions, getTitleDimension, shouldShowTicks } from '../utils/axis_utils'; +import { AxisSpec } from '../utils/specs'; + +/** + * Compute the axes required size around the chart + * @param chartTheme the theme style of the chart + * @param axisDimensions the axis dimensions + * @param axesStyles a map with all the custom axis styles + * @param axisSpecs the axis specs + * @internal + */ +export function computeAxesSizes( + { axes: sharedAxesStyles, chartMargins }: Theme, + axisDimensions: Map, + axesStyles: Map, + axisSpecs: AxisSpec[], + smSpec?: SmallMultiplesSpec, +): { left: number; right: number; top: number; bottom: number; margin: { left: number } } { + const axisMainSize = { + left: 0, + right: 0, + top: 0, + bottom: 0, + }; + const axisLabelOverflow = { + left: 0, + right: 0, + top: 0, + bottom: 0, + }; + + axisDimensions.forEach(({ maxLabelBboxWidth = 0, maxLabelBboxHeight = 0, isHidden }, id) => { + const axisSpec = getSpecsById(axisSpecs, id); + if (!axisSpec || isHidden) { + return; + } + const { tickLine, axisTitle, axisPanelTitle, tickLabel } = axesStyles.get(id) ?? sharedAxesStyles; + const showTicks = shouldShowTicks(tickLine, axisSpec.hide); + const { position, title } = axisSpec; + const labelPadding = getSimplePadding(tickLabel.padding); + const labelPaddingSum = tickLabel.visible ? labelPadding.inner + labelPadding.outer : 0; + + const tickDimension = showTicks ? tickLine.size + tickLine.padding : 0; + const titleDimension = title ? getTitleDimension(axisTitle) : 0; + const hasPanelTitle = Boolean(isVerticalAxis(position) ? smSpec?.splitVertically : smSpec?.splitHorizontally); + const panelTitleDimension = hasPanelTitle ? getTitleDimension(axisPanelTitle) : 0; + const axisDimension = labelPaddingSum + tickDimension + titleDimension + panelTitleDimension; + const maxAxisHeight = tickLabel.visible ? maxLabelBboxHeight + axisDimension : axisDimension; + const maxAxisWidth = tickLabel.visible ? maxLabelBboxWidth + axisDimension : axisDimension; + + switch (position) { + case Position.Top: + axisMainSize.top += maxAxisHeight + chartMargins.top; + // find the max half label size to accommodate the left/right labels + // TODO use first and last labels + axisLabelOverflow.left = Math.max(axisLabelOverflow.left, maxLabelBboxWidth / 2); + axisLabelOverflow.right = Math.max(axisLabelOverflow.right, maxLabelBboxWidth / 2); + break; + case Position.Bottom: + axisMainSize.bottom += maxAxisHeight + chartMargins.bottom; + // find the max half label size to accommodate the left/right labels + // TODO use first and last labels + axisLabelOverflow.left = Math.max(axisLabelOverflow.left, maxLabelBboxWidth / 2); + axisLabelOverflow.right = Math.max(axisLabelOverflow.right, maxLabelBboxWidth / 2); + break; + case Position.Right: + axisMainSize.right += maxAxisWidth + chartMargins.right; + // TODO use first and last labels + axisLabelOverflow.top = Math.max(axisLabelOverflow.top, maxLabelBboxHeight / 2); + axisLabelOverflow.bottom = Math.max(axisLabelOverflow.bottom, maxLabelBboxHeight / 2); + break; + case Position.Left: + default: + axisMainSize.left += maxAxisWidth + chartMargins.left; + // TODO use first and last labels + axisLabelOverflow.top = Math.max(axisLabelOverflow.top, maxLabelBboxHeight / 2); + axisLabelOverflow.bottom = Math.max(axisLabelOverflow.bottom, maxLabelBboxHeight / 2); + } + }); + const left = Math.max(axisLabelOverflow.left + chartMargins.left, axisMainSize.left); + return { + margin: { + left: left - axisMainSize.left, + }, + left, + right: Math.max(axisLabelOverflow.right + chartMargins.right, axisMainSize.right), + top: Math.max(axisLabelOverflow.top + chartMargins.top, axisMainSize.top), + bottom: Math.max(axisLabelOverflow.bottom + chartMargins.bottom, axisMainSize.bottom), + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/crosshair/crosshair_line.test.ts b/packages/osd-charts/src/chart_types/xy_chart/crosshair/crosshair_line.test.ts new file mode 100644 index 000000000000..06f618da219b --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/crosshair/crosshair_line.test.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getCursorLinePosition } from './crosshair_utils'; + +describe('Crosshair line position', () => { + it('shuld not compute line position for outside pointer coordinates', () => { + const linePos = getCursorLinePosition(0, { width: 100, height: 100, left: 0, top: 0 }, { x: -1, y: -1 }); + expect(linePos).toBeUndefined(); + }); + it('shuld compute line position for inside pointer coordinates', () => { + const linePos = getCursorLinePosition(0, { width: 100, height: 100, left: 0, top: 0 }, { x: 50, y: 50 }); + expect(linePos).toBeDefined(); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/crosshair/crosshair_utils.linear_snap.test.ts b/packages/osd-charts/src/chart_types/xy_chart/crosshair/crosshair_utils.linear_snap.test.ts new file mode 100644 index 000000000000..93b896569333 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/crosshair/crosshair_utils.linear_snap.test.ts @@ -0,0 +1,1590 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartType } from '../..'; +import { MockGlobalSpec } from '../../../mocks/specs/specs'; +import { MockXDomain } from '../../../mocks/xy/domains'; +import { ScaleType } from '../../../scales/constants'; +import { SpecType } from '../../../specs/constants'; +import { Dimensions } from '../../../utils/dimensions'; +import { getScaleConfigsFromSpecs } from '../state/selectors/get_api_scale_configs'; +import { computeSeriesDomains } from '../state/utils/utils'; +import { computeXScale } from '../utils/scales'; +import { BasicSeriesSpec, SeriesType } from '../utils/specs'; +import { getCursorBandPosition, getSnapPosition } from './crosshair_utils'; + +describe('Crosshair utils linear scale', () => { + const barSeries1SpecId = 'barSeries1'; + const barSeries2SpecId = 'barSeries2'; + const lineSeries1SpecId = 'lineSeries1'; + const lineSeries2SpecId = 'lineSeries2'; + + const barSeries1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: barSeries1SpecId, + groupId: 'group1', + seriesType: SeriesType.Bar, + data: [ + [0, 0], + [1, 0], + [2, 0], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + }; + const barSeries2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: barSeries2SpecId, + groupId: 'group1', + seriesType: SeriesType.Bar, + data: [ + [0, 2], + [1, 2], + [2, 2], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + }; + const lineSeries1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: lineSeries1SpecId, + groupId: 'group1', + seriesType: SeriesType.Line, + data: [ + [0, 0], + [1, 0], + [2, 0], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + }; + const lineSeries2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: lineSeries2SpecId, + groupId: 'group1', + seriesType: SeriesType.Line, + data: [ + [0, 2], + [1, 2], + [2, 2], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + }; + + const barSeries = [barSeries1]; + const barSeriesDomains = computeSeriesDomains( + barSeries, + getScaleConfigsFromSpecs([], barSeries, MockGlobalSpec.settings()), + ); + + const multiBarSeries = [barSeries1, barSeries2]; + const multiBarSeriesDomains = computeSeriesDomains( + multiBarSeries, + getScaleConfigsFromSpecs([], multiBarSeries, MockGlobalSpec.settings()), + ); + + const lineSeries = [lineSeries1]; + const lineSeriesDomains = computeSeriesDomains( + lineSeries, + getScaleConfigsFromSpecs([], lineSeries, MockGlobalSpec.settings()), + ); + + const multiLineSeries = [lineSeries1, lineSeries2]; + const multiLineSeriesDomains = computeSeriesDomains( + multiLineSeries, + getScaleConfigsFromSpecs([], multiLineSeries, MockGlobalSpec.settings()), + ); + + const mixedLinesBars = [lineSeries1, lineSeries2, barSeries1, barSeries2]; + const mixedLinesBarsSeriesDomains = computeSeriesDomains( + mixedLinesBars, + getScaleConfigsFromSpecs([], mixedLinesBars, MockGlobalSpec.settings()), + ); + + const barSeriesScale = computeXScale({ + xDomain: barSeriesDomains.xDomain, + totalBarsInCluster: barSeries.length, + range: [0, 120], + }); + const multiBarSeriesScale = computeXScale({ + xDomain: multiBarSeriesDomains.xDomain, + totalBarsInCluster: multiBarSeries.length, + range: [0, 120], + }); + const lineSeriesScale = computeXScale({ + xDomain: lineSeriesDomains.xDomain, + totalBarsInCluster: lineSeries.length, + range: [0, 120], + }); + const multiLineSeriesScale = computeXScale({ + xDomain: multiLineSeriesDomains.xDomain, + totalBarsInCluster: multiLineSeries.length, + range: [0, 120], + }); + const mixedLinesBarsSeriesScale = computeXScale({ + xDomain: mixedLinesBarsSeriesDomains.xDomain, + totalBarsInCluster: mixedLinesBars.length, + range: [0, 120], + }); + + /** + * if we have lines on a linear scale, the snap position and band should + * be always the same independently of the number of series + */ + test('can snap position on linear scale (line/area)', () => { + let snappedPosition = getSnapPosition(0, lineSeriesScale); + expect(snappedPosition?.band).toEqual(1); + expect(snappedPosition?.position).toEqual(0); + + snappedPosition = getSnapPosition(1, lineSeriesScale); + expect(snappedPosition?.band).toEqual(1); + expect(snappedPosition?.position).toEqual(60); + + snappedPosition = getSnapPosition(2, lineSeriesScale); + expect(snappedPosition?.band).toEqual(1); + expect(snappedPosition?.position).toEqual(120); + + // TODO uncomment this when we will limit the scale function to domain values. + // snappedPosition = getSnapPosition(3, singleScale); + // expect(snappedPosition?.band).toEqual(1); + // expect(snappedPosition?.position).toBeUndefined(); + + snappedPosition = getSnapPosition(0, multiLineSeriesScale, 2); + expect(snappedPosition?.band).toEqual(1); + expect(snappedPosition?.position).toEqual(0); + + snappedPosition = getSnapPosition(1, multiLineSeriesScale, 2); + expect(snappedPosition?.band).toEqual(1); + expect(snappedPosition?.position).toEqual(60); + + snappedPosition = getSnapPosition(2, multiLineSeriesScale, 2); + expect(snappedPosition?.band).toEqual(1); + expect(snappedPosition?.position).toEqual(120); + }); + + /** + * if we have bars on a linear scale, the snap position and band should + * be always the same independently of the number of series + */ + test('can snap position on linear scale (bar)', () => { + let snappedPosition = getSnapPosition(0, barSeriesScale); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(0); + + snappedPosition = getSnapPosition(1, barSeriesScale); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(40); + + snappedPosition = getSnapPosition(2, barSeriesScale); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(80); + + // TODO uncomment this when we will limit the scale function to domain values. + // snappedPosition = getSnapPosition(3, singleScale); + // expect(snappedPosition?.band).toEqual(40); + // expect(snappedPosition?.position).toBeUndefined(); + + // test a scale with a value of totalBarsInCluster > 1 + snappedPosition = getSnapPosition(0, multiBarSeriesScale, 2); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(0); + + snappedPosition = getSnapPosition(1, multiBarSeriesScale, 2); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(40); + + snappedPosition = getSnapPosition(2, multiBarSeriesScale, 2); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(80); + }); + + /** + * if we have bars and lines on a linear scale, the snap position and band should + * be always the same independently of the number of series + */ + test('can snap position on linear scale (mixed bars and lines)', () => { + let snappedPosition = getSnapPosition(0, mixedLinesBarsSeriesScale, 4); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(0); + + snappedPosition = getSnapPosition(1, mixedLinesBarsSeriesScale, 4); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(40); + + snappedPosition = getSnapPosition(2, mixedLinesBarsSeriesScale, 4); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(80); + }); + test('safeguard cursor band position', () => { + const chartDimensions: Dimensions = { top: 0, left: 0, width: 120, height: 100 }; + const chartRotation = 0; + const snapPosition = false; + + let bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 200, y: 0 }, + lineSeriesScale.invertWithStep(200, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 1, + ); + expect(bandPosition).toBeUndefined(); + + bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 200 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 1, + ); + expect(bandPosition).toBeUndefined(); + + bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: -1, y: 0 }, + lineSeriesScale.invertWithStep(-1, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 1, + ); + expect(bandPosition).toBeUndefined(); + + bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: -1 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 1, + ); + expect(bandPosition).toBeUndefined(); + }); + + describe('BandPosition line chart', () => { + const chartDimensions: Dimensions = { top: 0, left: 0, width: 120, height: 100 }; + + describe('0 degree rotation, snap disabled', () => { + const chartRotation = 0; + const snapPosition = false; + + test('0,0 position', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 0 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 0, + y: 0, + height: 100, + }); + }); + + test("changes on y mouse position doesn't change the band position", () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 45 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 0, + y: 0, + height: 100, + }); + }); + + test('increase of x axis increase the left param 1', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 40, y: 0 }, + lineSeriesScale.invertWithStep(40, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 40, + width: 0, + y: 0, + height: 100, + }); + }); + + test('increase of x axis increase the left param 2', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 90, y: 0 }, + lineSeriesScale.invertWithStep(90, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 90, + width: 0, + y: 0, + height: 100, + }); + }); + + test('limit the band position based on chart dimension', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 200, y: 0 }, + lineSeriesScale.invertWithStep(200, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toBeUndefined(); + }); + }); + describe('0 degree rotation, snap enabled', () => { + const chartRotation = 0; + const snapPosition = true; + + test('0,0 position', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 0 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 0, + y: 0, + height: 100, + }); + }); + + test("changes on y mouse position doesn't change the band position", () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 45 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 0, + y: 0, + height: 100, + }); + }); + + test('increase of x axis increase the left param 1', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 20, y: 0 }, + lineSeriesScale.invertWithStep(20, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 0, + y: 0, + height: 100, + }); + }); + + test('increase of x axis increase the left param 2', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 40, y: 0 }, + lineSeriesScale.invertWithStep(40, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 60, + width: 0, + y: 0, + height: 100, + }); + }); + + test('increase of x axis increase the left param 3', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 95, y: 0 }, + lineSeriesScale.invertWithStep(95, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 120, + width: 0, + y: 0, + height: 100, + }); + }); + + test('limit the band position based on chart dimension', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 200, y: 0 }, + lineSeriesScale.invertWithStep(200, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toBeUndefined(); + }); + }); + + describe('180 degree rotation, snap disabled', () => { + const chartRotation = 180; + const snapPosition = false; + + test('0,0 position', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 0 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + + expect(bandPosition).toEqual({ + x: 120, + width: 0, + y: 0, + height: 100, + }); + }); + + test("changes on y mouse position doesn't change the band position", () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 45 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 120, + width: 0, + y: 0, + height: 100, + }); + }); + + test('increase of x axis increase the left param 1', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 40, y: 0 }, + lineSeriesScale.invertWithStep(40, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 80, + width: 0, + y: 0, + height: 100, + }); + }); + + test('increase of x axis increase the left param 2', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 90, y: 0 }, + lineSeriesScale.invertWithStep(90, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 30, + width: 0, + y: 0, + height: 100, + }); + }); + + test('limit the band position based on chart dimension', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 200, y: 0 }, + lineSeriesScale.invertWithStep(200, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toBeUndefined(); + }); + }); + + describe('180 degree rotation, snap enabled', () => { + const chartRotation = 180; + const snapPosition = true; + + test('0,0 position', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 0 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 120, + width: 0, + y: 0, + height: 100, + }); + }); + + test("changes on y mouse position doesn't change the band position", () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 45 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 120, + width: 0, + y: 0, + height: 100, + }); + }); + + test('increase of x axis increase the left param 1', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 20, y: 0 }, + lineSeriesScale.invertWithStep(20, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 120, + width: 0, + y: 0, + height: 100, + }); + }); + + test('increase of x axis increase the left param 2', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 40, y: 0 }, + lineSeriesScale.invertWithStep(40, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 60, + width: 0, + y: 0, + height: 100, + }); + }); + + test('increase of x axis increase the left param 3', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 95, y: 0 }, + lineSeriesScale.invertWithStep(95, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 0, + y: 0, + height: 100, + }); + }); + + test('limit the band position based on chart dimension', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 200, y: 0 }, + lineSeriesScale.invertWithStep(200, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toBeUndefined(); + }); + }); + + describe('90 degree rotation, snap disabled', () => { + const chartRotation = 90; + const snapPosition = false; + + test('0,0 position', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 0 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 120, + y: 0, + height: 0, + }); + }); + + test("changes on x mouse position doesn't change the band position", () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 45, y: 0 }, + lineSeriesScale.invertWithStep(45, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 120, + y: 45, + height: 0, + }); + }); + + test('increase of y mouse position increase the top param 1', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 40 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 120, + y: 0, + height: 0, + }); + }); + + test('increase of y mouse position increase the top param 2', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 90 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 120, + y: 0, + height: 0, + }); + }); + + test('limit the band position based on chart dimension', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 200 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toBeUndefined(); + }); + }); + + describe('90 degree rotation, snap enabled', () => { + const chartRotation = 90; + const snapPosition = true; + + test('0,0 position', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 0 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 120, + y: 0, + height: 0, + }); + }); + + test("changes on x mouse position doesn't change the band position", () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 45, y: 0 }, + lineSeriesScale.invertWithStep(45, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 120, + y: 60, + height: 0, + }); + }); + + test('increase of y mouse position increase the top param 1', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 20 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 120, + y: 0, + height: 0, + }); + }); + + test('increase of y mouse position increase the top param 2', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 40 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 120, + y: 0, + height: 0, + }); + }); + + test('increase of y mouse position increase the top param 3', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 95 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 120, + y: 0, + height: 0, + }); + }); + + test('limit the band position based on chart dimension', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 200 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toBeUndefined(); + }); + }); + + describe('-90 degree rotation, snap disabled', () => { + const chartRotation = -90; + const snapPosition = false; + + test('0,0 position', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 0 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 120, + y: 100, + height: 0, + }); + }); + + test("changes on x mouse position doesn't change the band position", () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 45, y: 0 }, + lineSeriesScale.invertWithStep(45, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 120, + y: 55, + height: 0, + }); + }); + + test('increase of y mouse position increase the top param 1', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 40 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 120, + y: 100, + height: 0, + }); + }); + + test('increase of y mouse position increase the top param 2', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 90 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 120, + y: 100, + height: 0, + }); + }); + + test('limit the band position based on chart dimension', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 200 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toBeUndefined(); + }); + }); + + describe('-90 degree rotation, snap enabled', () => { + const chartRotation = -90; + const snapPosition = true; + + test('0,0 position', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 0 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 120, + y: 100, + height: 0, + }); + }); + + test("changes on x mouse position doesn't change the band position", () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 45, y: 0 }, + lineSeriesScale.invertWithStep(45, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 120, + y: 40, + height: 0, + }); + }); + + test('increase of y mouse position increase the top param 1', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 20 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 120, + y: 100, + height: 0, + }); + }); + + test('increase of y mouse position increase the top param 2', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 40 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 120, + y: 100, + height: 0, + }); + }); + + test('increase of y mouse position increase the top param 3', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 95 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toEqual({ + x: 0, + width: 120, + y: 100, + height: 0, + }); + }); + + test('limit the band position based on chart dimension', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 200 }, + lineSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + lineSeriesScale, + 0, + ); + expect(bandPosition).toBeUndefined(); + }); + }); + }); + + describe('BandPosition bar chart', () => { + const chartDimensions: Dimensions = { top: 0, left: 0, width: 120, height: 100 }; + describe('0 degree rotation, snap enabled', () => { + const chartRotation = 0; + const snapPosition = true; + + test('0,0 position', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 0 }, + barSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toEqual({ + x: 0, + y: 0, + height: 100, + width: 40, + }); + }); + + test("changes on y mouse position doesn't change the band position", () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 45 }, + barSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toEqual({ + x: 0, + y: 0, + height: 100, + width: 40, + }); + }); + + test('increase of x axis increase the left param 1', () => { + let bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 40, y: 0 }, + barSeriesScale.invertWithStep(40, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toEqual({ + x: 0, + y: 0, + height: 100, + width: 40, + }); + bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 41, y: 0 }, + barSeriesScale.invertWithStep(41, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toEqual({ + x: 40, + y: 0, + height: 100, + width: 40, + }); + }); + + test('increase of x axis increase the left param 2', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 90, y: 0 }, + barSeriesScale.invertWithStep(90, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toEqual({ + x: 80, + y: 0, + height: 100, + width: 40, + }); + }); + + test('limit the band position based on chart dimension', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 200, y: 0 }, + barSeriesScale.invertWithStep(200, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toBeUndefined(); + }); + }); + + describe('180 degree rotation, snap enabled', () => { + const chartRotation = 180; + const snapPosition = true; + + test('0,0 position', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 0 }, + barSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toEqual({ + x: 80, + y: 0, + height: 100, + width: 40, + }); + }); + + test("changes on y mouse position doesn't change the band position", () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 45 }, + barSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toEqual({ + x: 80, + y: 0, + height: 100, + width: 40, + }); + }); + + test('increase of x axis increase the left param 1', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 41, y: 0 }, + barSeriesScale.invertWithStep(41, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toEqual({ + x: 40, + y: 0, + height: 100, + width: 40, + }); + }); + + test('increase of x axis increase the left param 2', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 90, y: 0 }, + barSeriesScale.invertWithStep(90, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toEqual({ + x: 0, + y: 0, + height: 100, + width: 40, + }); + }); + + test('limit the band position based on chart dimension', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 200, y: 0 }, + barSeriesScale.invertWithStep(200, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toBeUndefined(); + }); + }); + + describe('90 degree rotation, snap enabled', () => { + const chartRotation = 90; + const snapPosition = true; + + test('0,0 position', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 0 }, + barSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toEqual({ + x: 0, + y: 0, + height: 40, + width: 120, + }); + }); + + test("changes on x mouse position doesn't change the band position", () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 45, y: 0 }, + barSeriesScale.invertWithStep(45, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toEqual({ + x: 0, + y: 40, + height: 40, + width: 120, + }); + }); + + test('increase of y mouse position increase the top param 1', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 40 }, + barSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toEqual({ + x: 0, + y: 0, + height: 40, + width: 120, + }); + }); + + test('increase of y mouse position increase the top param 2', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 90 }, + barSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toEqual({ + x: 0, + y: 0, + height: 40, + width: 120, + }); + }); + + test('limit the band position based on chart dimension', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 200 }, + barSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toBeUndefined(); + }); + }); + + describe('-90 degree rotation, snap disabled', () => { + const chartRotation = -90; + const snapPosition = true; + + test('0,0 position', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 0 }, + barSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toEqual({ + x: 0, + y: 60, + height: 40, + width: 120, + }); + }); + + test("changes on x mouse position doesn't change the band position", () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 45, y: 0 }, + barSeriesScale.invertWithStep(45, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toEqual({ + x: 0, + y: 20, + height: 40, + width: 120, + }); + }); + + test('increase of y mouse position increase the top param 1', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 40 }, + barSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toEqual({ + x: 0, + y: 60, + height: 40, + width: 120, + }); + }); + + test('increase of y mouse position increase the top param 2', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 90 }, + barSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toEqual({ + x: 0, + y: 60, + height: 40, + width: 120, + }); + }); + + test('limit the band position based on chart dimension', () => { + const bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 0, y: 200 }, + barSeriesScale.invertWithStep(0, [0, 1, 2])!, + snapPosition, + barSeriesScale, + 1, + ); + expect(bandPosition).toBeUndefined(); + }); + }); + }); + + describe('BandPosition bar chart wwith limited edges', () => { + const chartDimensions: Dimensions = { top: 0, left: 0, width: 120, height: 120 }; + test('cursor at begin of domain', () => { + const barSeriesScaleLimited = computeXScale({ + xDomain: MockXDomain.fromScaleType(ScaleType.Linear, { + domain: [0.5, 3.5], + isBandScale: true, + minInterval: 1, + }), + totalBarsInCluster: 1, + range: [0, 120], + }); + const bandPosition = getCursorBandPosition( + 0, + chartDimensions, + { x: 0, y: 0 }, + { + value: 0, + withinBandwidth: true, + }, + true, + barSeriesScaleLimited, + 1, + ); + expect(bandPosition).toEqual({ + x: 0, + y: 0, + height: 120, + width: 15, + }); + }); + test('cursor at end of domain', () => { + const barSeriesScaleLimited = computeXScale({ + xDomain: MockXDomain.fromScaleType(ScaleType.Linear, { + domain: [-0.5, 2.5], + isBandScale: true, + minInterval: 1, + }), + totalBarsInCluster: barSeries.length, + range: [0, 120], + }); + const bandPosition = getCursorBandPosition( + 0, + chartDimensions, + { x: 119, y: 0 }, + { + value: 3, + withinBandwidth: true, + }, + true, + barSeriesScaleLimited, + 1, + ); + expect(bandPosition).toEqual({ + x: 105, + y: 0, + height: 120, + width: 15, + }); + }); + test('cursor at top begin of domain', () => { + const barSeriesScaleLimited = computeXScale({ + xDomain: MockXDomain.fromScaleType(ScaleType.Linear, { + domain: [0.5, 3.5], + isBandScale: true, + minInterval: 1, + }), + totalBarsInCluster: 1, + range: [0, 120], + }); + const bandPosition = getCursorBandPosition( + 90, + chartDimensions, + { x: 0, y: 0 }, + { + value: 0, + withinBandwidth: true, + }, + true, + barSeriesScaleLimited, + 1, + ); + expect(bandPosition).toEqual({ + x: 0, + y: 0, + height: 15, + width: 120, + }); + }); + test('cursor at top end of domain', () => { + const barSeriesScaleLimited = computeXScale({ + xDomain: MockXDomain.fromScaleType(ScaleType.Linear, { + domain: [-0.5, 2.5], + isBandScale: true, + minInterval: 1, + }), + totalBarsInCluster: barSeries.length, + range: [0, 120], + }); + const bandPosition = getCursorBandPosition( + 90, + chartDimensions, + { x: 0, y: 119 }, + { + value: 3, + withinBandwidth: true, + }, + true, + barSeriesScaleLimited, + 1, + ); + expect(bandPosition).toEqual({ + x: 0, + y: 105, + height: 15, + width: 120, + }); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/crosshair/crosshair_utils.ordinal_snap.test.ts b/packages/osd-charts/src/chart_types/xy_chart/crosshair/crosshair_utils.ordinal_snap.test.ts new file mode 100644 index 000000000000..7cd861b1d982 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/crosshair/crosshair_utils.ordinal_snap.test.ts @@ -0,0 +1,231 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartType } from '../..'; +import { MockGlobalSpec } from '../../../mocks/specs/specs'; +import { ScaleType } from '../../../scales/constants'; +import { SpecType } from '../../../specs/constants'; +import { getScaleConfigsFromSpecs } from '../state/selectors/get_api_scale_configs'; +import { computeSeriesDomains } from '../state/utils/utils'; +import { computeXScale } from '../utils/scales'; +import { BasicSeriesSpec, SeriesType } from '../utils/specs'; +import { getSnapPosition } from './crosshair_utils'; + +describe('Crosshair utils ordinal scales', () => { + const barSeries1SpecId = 'barSeries1'; + const barSeries2SpecId = 'barSeries2'; + const lineSeries1SpecId = 'lineSeries1'; + const lineSeries2SpecId = 'lineSeries2'; + + const barSeries1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: barSeries1SpecId, + groupId: 'group1', + seriesType: SeriesType.Bar, + data: [ + ['a', 0], + ['b', 0], + ['c', 0], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }; + const barSeries2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: barSeries2SpecId, + groupId: 'group1', + seriesType: SeriesType.Bar, + data: [ + ['a', 2], + ['b', 2], + ['c', 2], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }; + const lineSeries1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: lineSeries1SpecId, + groupId: 'group1', + seriesType: SeriesType.Line, + data: [ + ['a', 0], + ['b', 0], + ['c', 0], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }; + const lineSeries2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: lineSeries2SpecId, + groupId: 'group1', + seriesType: SeriesType.Line, + data: [ + ['a', 2], + ['b', 2], + ['c', 2], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }; + + const barSeries = [barSeries1]; + + const barSeriesDomains = computeSeriesDomains( + barSeries, + getScaleConfigsFromSpecs([], barSeries, MockGlobalSpec.settings()), + ); + + const multiBarSeries = [barSeries1, barSeries2]; + const multiBarSeriesDomains = computeSeriesDomains( + multiBarSeries, + getScaleConfigsFromSpecs([], multiBarSeries, MockGlobalSpec.settings()), + ); + + const lineSeries = [lineSeries1]; + const lineSeriesDomains = computeSeriesDomains( + lineSeries, + getScaleConfigsFromSpecs([], lineSeries, MockGlobalSpec.settings()), + ); + + const multiLineSeries = [lineSeries1, lineSeries2]; + const multiLineSeriesDomains = computeSeriesDomains( + multiLineSeries, + getScaleConfigsFromSpecs([], multiLineSeries, MockGlobalSpec.settings()), + ); + + const mixedLinesBars = [lineSeries1, lineSeries2, barSeries1, barSeries2]; + const mixedLinesBarsSeriesDomains = computeSeriesDomains( + mixedLinesBars, + getScaleConfigsFromSpecs([], mixedLinesBars, MockGlobalSpec.settings()), + ); + + const barSeriesScale = computeXScale({ + xDomain: barSeriesDomains.xDomain, + totalBarsInCluster: barSeries.length, + range: [0, 120], + }); + const multiBarSeriesScale = computeXScale({ + xDomain: multiBarSeriesDomains.xDomain, + totalBarsInCluster: multiBarSeries.length, + range: [0, 120], + }); + const lineSeriesScale = computeXScale({ + xDomain: lineSeriesDomains.xDomain, + totalBarsInCluster: lineSeries.length, + range: [0, 120], + }); + const multiLineSeriesScale = computeXScale({ + xDomain: multiLineSeriesDomains.xDomain, + totalBarsInCluster: multiLineSeries.length, + range: [0, 120], + }); + const mixedLinesBarsSeriesScale = computeXScale({ + xDomain: mixedLinesBarsSeriesDomains.xDomain, + totalBarsInCluster: mixedLinesBars.length, + range: [0, 120], + }); + + test('can snap position on scale ordinal bar', () => { + let snappedPosition = getSnapPosition('a', barSeriesScale); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(0); + + snappedPosition = getSnapPosition('b', barSeriesScale); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(40); + + snappedPosition = getSnapPosition('c', barSeriesScale); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(80); + + snappedPosition = getSnapPosition('x', barSeriesScale); + expect(snappedPosition).toBeUndefined(); + + snappedPosition = getSnapPosition('a', multiBarSeriesScale, 2); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(0); + + snappedPosition = getSnapPosition('b', multiBarSeriesScale, 2); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(40); + + snappedPosition = getSnapPosition('c', multiBarSeriesScale, 2); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(80); + }); + test('can snap position on scale ordinal lines', () => { + let snappedPosition = getSnapPosition('a', lineSeriesScale); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(0); + + snappedPosition = getSnapPosition('b', lineSeriesScale); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(40); + + snappedPosition = getSnapPosition('c', lineSeriesScale); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(80); + + snappedPosition = getSnapPosition('x', lineSeriesScale); + expect(snappedPosition).toBeUndefined(); + + snappedPosition = getSnapPosition('a', multiLineSeriesScale, 2); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(0); + + snappedPosition = getSnapPosition('b', multiLineSeriesScale, 2); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(40); + + snappedPosition = getSnapPosition('c', multiLineSeriesScale, 2); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(80); + }); + + test('can snap position on scale ordinal mixed lines/bars', () => { + let snappedPosition = getSnapPosition('a', mixedLinesBarsSeriesScale, 4); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(0); + + snappedPosition = getSnapPosition('b', mixedLinesBarsSeriesScale, 4); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(40); + + snappedPosition = getSnapPosition('c', mixedLinesBarsSeriesScale, 4); + expect(snappedPosition?.band).toEqual(40); + expect(snappedPosition?.position).toEqual(80); + + snappedPosition = getSnapPosition('x', mixedLinesBarsSeriesScale, 4); + expect(snappedPosition).toBeUndefined(); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/crosshair/crosshair_utils.ts b/packages/osd-charts/src/chart_types/xy_chart/crosshair/crosshair_utils.ts new file mode 100644 index 000000000000..645428630d38 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/crosshair/crosshair_utils.ts @@ -0,0 +1,214 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AnchorPosition } from '../../../components/portal/types'; +import { Line, Rect } from '../../../geoms/types'; +import { Scale } from '../../../scales'; +import { isContinuousScale } from '../../../scales/types'; +import { TooltipStickTo } from '../../../specs/constants'; +import { Rotation } from '../../../utils/common'; +import { Dimensions } from '../../../utils/dimensions'; +import { Point } from '../../../utils/point'; +import { isHorizontalRotation, isVerticalRotation } from '../state/utils/common'; + +/** @internal */ +export const DEFAULT_SNAP_POSITION_BAND = 1; + +/** @internal */ +export function getSnapPosition( + value: string | number, + scale: Scale, + totalBarsInCluster = 1, +): { band: number; position: number } | undefined { + const position = scale.scale(value); + if (position === null) { + return; + } + + if (scale.bandwidth > 0) { + const band = scale.bandwidth / (1 - scale.barsPadding); + + const halfPadding = (band - scale.bandwidth) / 2; + return { + position: position - halfPadding * totalBarsInCluster, + band: band * totalBarsInCluster, + }; + } + return { + position, + band: DEFAULT_SNAP_POSITION_BAND, + }; +} + +/** @internal */ +export function getCursorLinePosition( + chartRotation: Rotation, + chartDimensions: Dimensions, + projectedPointerPosition: { x: number; y: number }, +): Line | undefined { + const { x, y } = projectedPointerPosition; + if (x < 0 || y < 0) { + return void 0; + } + const { left, top, width, height } = chartDimensions; + const isHorizontalRotated = isHorizontalRotation(chartRotation); + if (isHorizontalRotated) { + const crosshairTop = y + top; + return { + x1: left, + x2: left + width, + y1: crosshairTop, + y2: crosshairTop, + }; + } + const crosshairLeft = x + left; + + return { + x1: crosshairLeft, + x2: crosshairLeft, + y1: top, + y2: top + height, + }; +} + +/** @internal */ +export function getCursorBandPosition( + chartRotation: Rotation, + panel: Dimensions, + cursorPosition: Point, + invertedValue: { + value: any; + withinBandwidth: boolean; + }, + snapEnabled: boolean, + xScale: Scale, + totalBarsInCluster?: number, +): Rect | undefined { + const { top, left, width, height } = panel; + const { x, y } = cursorPosition; + const isHorizontalRotated = isHorizontalRotation(chartRotation); + const chartWidth = isHorizontalRotated ? width : height; + const chartHeight = isHorizontalRotated ? height : width; + + const isLineOrAreaOnly = !totalBarsInCluster; + + if (x > chartWidth || y > chartHeight || x < 0 || y < 0 || !invertedValue.withinBandwidth) { + return undefined; + } + + const snappedPosition = getSnapPosition(invertedValue.value, xScale, isLineOrAreaOnly ? 1 : totalBarsInCluster); + if (!snappedPosition) { + return undefined; + } + + const { position, band } = snappedPosition; + const bandOffset = xScale.bandwidth > 0 ? band : 0; + + if (isHorizontalRotated) { + const adjustedLeft = snapEnabled ? position : cursorPosition.x; + let leftPosition = chartRotation === 0 ? left + adjustedLeft : left + width - adjustedLeft - bandOffset; + let adjustedWidth = band; + if (band > 1 && leftPosition + band > left + width) { + adjustedWidth = left + width - leftPosition; + } else if (band > 1 && leftPosition < left) { + adjustedWidth = band - (left - leftPosition); + leftPosition = left; + } + if (isLineOrAreaOnly && isContinuousScale(xScale)) { + return { + x: leftPosition, + width: 0, + y: top, + height, + }; + } + return { + x: leftPosition, + y: top, + width: adjustedWidth, + height, + }; + } + const adjustedTop = snapEnabled ? position : cursorPosition.x; + let topPosition = chartRotation === 90 ? top + adjustedTop : height + top - adjustedTop - bandOffset; + let adjustedHeight = band; + if (band > 1 && topPosition + band > top + height) { + adjustedHeight = band - (topPosition + band - (top + height)); + } else if (band > 1 && topPosition < top) { + adjustedHeight = band - (top - topPosition); + topPosition = top; + } + if (isLineOrAreaOnly && isContinuousScale(xScale)) { + return { + x: left, + width, + y: topPosition, + height: 0, + }; + } + return { + y: topPosition, + x: left, + width, + height: adjustedHeight, + }; +} + +/** @internal */ +export function getTooltipAnchorPosition( + chartRotation: Rotation, + cursorBandPosition: Rect, + cursorPosition: { x: number; y: number }, + panel: Dimensions, + stickTo: TooltipStickTo = TooltipStickTo.MousePosition, +): AnchorPosition { + const { x, y, width, height } = cursorBandPosition; + const isRotated = isVerticalRotation(chartRotation); + // horizontal movement of cursor + if (!isRotated) { + const stickY = + stickTo === TooltipStickTo.MousePosition + ? cursorPosition.y + panel.top + : stickTo === TooltipStickTo.Middle + ? y + height / 2 + : stickTo === TooltipStickTo.Bottom + ? y + height + : y; // TooltipStickTo.Top is also ok with that value + return { + x, + width, + y: stickY, + height: 0, + }; + } + const stickX = + stickTo === TooltipStickTo.MousePosition + ? cursorPosition.x + panel.left + : stickTo === TooltipStickTo.Right + ? x + width + : stickTo === TooltipStickTo.Center + ? x + width / 2 + : x; // TooltipStickTo.Left + return { + x: stickX, + width: 0, + y, + height, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/domains/nice.ts b/packages/osd-charts/src/chart_types/xy_chart/domains/nice.ts new file mode 100644 index 000000000000..665fb4d4178e --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/domains/nice.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export function areAllNiceDomain(nice: Array) { + return nice.length > 0 && nice.every((d) => d); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/domains/types.ts b/packages/osd-charts/src/chart_types/xy_chart/domains/types.ts new file mode 100644 index 000000000000..05e8580560eb --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/domains/types.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ScaleContinuousType } from '../../../scales'; +import { LogScaleOptions } from '../../../scales/scale_continuous'; +import { OrdinalDomain, ContinuousDomain } from '../../../utils/domain'; +import { GroupId } from '../../../utils/ids'; +import { XScaleType } from '../utils/specs'; + +/** @internal */ +export type XDomain = Pick & { + type: XScaleType; + nice: boolean; + /* if the scale needs to be a band scale: used when displaying bars */ + isBandScale: boolean; + /* the minimum interval of the scale if not-ordinal band-scale */ + minInterval: number; + /** if x domain is time, we should also specify the timezone */ + timeZone?: string; + domain: OrdinalDomain | ContinuousDomain; + desiredTickCount: number; +}; + +/** @internal */ +export type YDomain = LogScaleOptions & { + type: ScaleContinuousType; + nice: boolean; + isBandScale: false; + groupId: GroupId; + domain: ContinuousDomain; + desiredTickCount: number; + domainPixelPadding?: number; + constrainDomainPadding?: boolean; +}; diff --git a/packages/osd-charts/src/chart_types/xy_chart/domains/x_domain.test.ts b/packages/osd-charts/src/chart_types/xy_chart/domains/x_domain.test.ts new file mode 100644 index 000000000000..af4b008105ee --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/domains/x_domain.test.ts @@ -0,0 +1,876 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartType } from '../..'; +import { MockGlobalSpec, MockSeriesSpec, MockSeriesSpecs } from '../../../mocks/specs'; +import { ScaleType } from '../../../scales/constants'; +import { SpecType, Direction, BinAgg } from '../../../specs/constants'; +import { Logger } from '../../../utils/logger'; +import { getXNiceFromSpec, getXScaleTypeFromSpec } from '../scales/get_api_scales'; +import { getScaleConfigsFromSpecs } from '../state/selectors/get_api_scale_configs'; +import { getDataSeriesFromSpecs } from '../utils/series'; +import { BasicSeriesSpec, SeriesType } from '../utils/specs'; +import { convertXScaleTypes, findMinInterval, mergeXDomain } from './x_domain'; + +jest.mock('../../../utils/logger', () => ({ + Logger: { + warn: jest.fn(), + }, +})); + +describe('X Domain', () => { + test('Should return a default scale when missing specs or specs types', () => { + const seriesSpecs: BasicSeriesSpec[] = []; + const mainXScale = convertXScaleTypes(seriesSpecs); + expect(mainXScale).not.toBeNull(); + }); + + test('Should return correct scale type with single bar', () => { + const seriesSpecs: Pick[] = [ + { + seriesType: SeriesType.Bar, + xScaleType: ScaleType.Linear, + }, + ]; + const mainXScale = convertXScaleTypes(seriesSpecs); + expect(mainXScale).toEqual({ + type: getXScaleTypeFromSpec(ScaleType.Linear), + nice: getXNiceFromSpec(), + isBandScale: true, + }); + }); + + test('Should return correct scale type with single bar with Ordinal', () => { + const seriesSpecs: Pick[] = [ + { + seriesType: SeriesType.Bar, + xScaleType: ScaleType.Ordinal, + }, + ]; + const mainXScale = convertXScaleTypes(seriesSpecs); + expect(mainXScale).toEqual({ + type: getXScaleTypeFromSpec(ScaleType.Ordinal), + nice: getXNiceFromSpec(), + isBandScale: true, + }); + }); + + test('Should return correct scale type with single area', () => { + const seriesSpecs: Pick[] = [ + { + seriesType: SeriesType.Area, + xScaleType: ScaleType.Linear, + }, + ]; + const mainXScale = convertXScaleTypes(seriesSpecs); + expect(mainXScale).toEqual({ + type: getXScaleTypeFromSpec(ScaleType.Linear), + nice: getXNiceFromSpec(), + isBandScale: false, + }); + }); + test('Should return correct scale type with single line (time)', () => { + const seriesSpecs: Pick[] = [ + { + seriesType: SeriesType.Line, + xScaleType: ScaleType.Time, + timeZone: 'utc-3', + }, + ]; + const mainXScale = convertXScaleTypes(seriesSpecs); + expect(mainXScale).toEqual({ + type: getXScaleTypeFromSpec(ScaleType.Time), + nice: getXNiceFromSpec(), + isBandScale: false, + timeZone: 'utc-3', + }); + }); + test('Should return correct scale type with multi line with same scale types (time) same tz', () => { + const seriesSpecs: Pick[] = [ + { + seriesType: SeriesType.Line, + xScaleType: ScaleType.Time, + timeZone: 'UTC-3', + }, + { + seriesType: SeriesType.Line, + xScaleType: ScaleType.Time, + timeZone: 'utc-3', + }, + ]; + const mainXScale = convertXScaleTypes(seriesSpecs); + expect(mainXScale).toEqual({ + type: getXScaleTypeFromSpec(ScaleType.Time), + nice: getXNiceFromSpec(), + isBandScale: false, + timeZone: 'utc-3', + }); + }); + test('Should return correct scale type with multi line with same scale types (time) coerce to UTC', () => { + const seriesSpecs: Pick[] = [ + { + seriesType: SeriesType.Line, + xScaleType: ScaleType.Time, + timeZone: 'utc-3', + }, + { + seriesType: SeriesType.Line, + xScaleType: ScaleType.Time, + timeZone: 'utc+3', + }, + ]; + const mainXScale = convertXScaleTypes(seriesSpecs); + expect(mainXScale).toEqual({ + type: getXScaleTypeFromSpec(ScaleType.Time), + nice: getXNiceFromSpec(), + isBandScale: false, + timeZone: 'utc', + }); + }); + + test('Should return correct scale type with multi line with different scale types (linear, ordinal)', () => { + const seriesSpecs: Pick[] = [ + { + seriesType: SeriesType.Line, + xScaleType: ScaleType.Linear, + }, + { + seriesType: SeriesType.Line, + xScaleType: ScaleType.Ordinal, + }, + ]; + const mainXScale = convertXScaleTypes(seriesSpecs); + expect(mainXScale).toEqual({ + type: getXScaleTypeFromSpec(ScaleType.Ordinal), + nice: getXNiceFromSpec(), + isBandScale: false, + }); + }); + test('Should return correct scale type with multi bar, area with different scale types (linear, ordinal)', () => { + const seriesSpecs: Pick[] = [ + { + seriesType: SeriesType.Bar, + xScaleType: ScaleType.Linear, + }, + { + seriesType: SeriesType.Area, + xScaleType: ScaleType.Ordinal, + }, + ]; + const mainXScale = convertXScaleTypes(seriesSpecs); + expect(mainXScale).toEqual({ + type: getXScaleTypeFromSpec(ScaleType.Ordinal), + nice: getXNiceFromSpec(), + isBandScale: true, + }); + }); + test('Should return correct scale type with multi bar, area with same scale types (linear, linear)', () => { + const seriesSpecs: Pick[] = [ + { + seriesType: SeriesType.Bar, + xScaleType: ScaleType.Linear, + }, + { + seriesType: SeriesType.Area, + xScaleType: ScaleType.Time, + timeZone: 'utc+3', + }, + ]; + const mainXScale = convertXScaleTypes(seriesSpecs); + expect(mainXScale).toEqual({ + type: getXScaleTypeFromSpec(ScaleType.Linear), + nice: getXNiceFromSpec(), + isBandScale: true, + }); + }); + + test('Should merge line series correctly', () => { + const ds1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds1', + groupId: 'g1', + seriesType: SeriesType.Line, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 2, y: 0 }, + { x: 5, y: 0 }, + ], + }; + const ds2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds2', + groupId: 'g1', + seriesType: SeriesType.Line, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 7, y: 0 }, + ], + }; + const specDataSeries: BasicSeriesSpec[] = [ds1, ds2]; + const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); + const mergedDomain = mergeXDomain(scalesConfig.x, xValues); + expect(mergedDomain.domain).toEqual([0, 7]); + }); + test('Should merge bar series correctly', () => { + const ds1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds1', + groupId: 'g1', + seriesType: SeriesType.Bar, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 2, y: 0 }, + { x: 5, y: 0 }, + ], + }; + const ds2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds2', + groupId: 'g1', + seriesType: SeriesType.Bar, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 7, y: 0 }, + ], + }; + const specDataSeries = [ds1, ds2]; + const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); + const mergedDomain = mergeXDomain(scalesConfig.x, xValues); + expect(mergedDomain.domain).toEqual([0, 7]); + }); + test('Should merge multi bar series correctly', () => { + const ds1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds1', + groupId: 'g1', + seriesType: SeriesType.Bar, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 2, y: 0 }, + { x: 5, y: 0 }, + ], + }; + const ds2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds2', + groupId: 'g2', + seriesType: SeriesType.Bar, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 7, y: 0 }, + ], + }; + const specDataSeries = [ds1, ds2]; + const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); + const mergedDomain = mergeXDomain(scalesConfig.x, xValues); + expect(mergedDomain.domain).toEqual([0, 7]); + }); + test('Should merge multi bar series correctly - 2', () => { + const ds1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds1', + groupId: 'g1', + seriesType: SeriesType.Bar, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 2, y: 0 }, + { x: 5, y: 0 }, + ], + }; + const ds2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds2', + groupId: 'g2', + seriesType: SeriesType.Bar, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 7, y: 0 }, + ], + }; + const specDataSeries = [ds1, ds2]; + + const { xValues } = getDataSeriesFromSpecs(specDataSeries); + const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); + + const mergedDomain = mergeXDomain(scalesConfig.x, xValues); + expect(mergedDomain.domain).toEqual([0, 7]); + }); + test('Should merge multi bar linear/bar ordinal series correctly', () => { + const ds1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds1', + groupId: 'g1', + seriesType: SeriesType.Bar, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 2, y: 0 }, + { x: 5, y: 0 }, + ], + }; + const ds2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds2', + groupId: 'g2', + seriesType: SeriesType.Bar, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 7, y: 0 }, + ], + }; + const specDataSeries = [ds1, ds2]; + const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); + const mergedDomain = mergeXDomain(scalesConfig.x, xValues); + expect(mergedDomain.domain).toEqual([0, 1, 2, 5, 7]); + }); + + test('Should fallback to ordinal scale if not array of numbers', () => { + const ds1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds1', + groupId: 'g1', + seriesType: SeriesType.Bar, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 'a', y: 0 }, + { x: 2, y: 0 }, + { x: 5, y: 0 }, + ], + }; + const ds2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds2', + groupId: 'g2', + seriesType: SeriesType.Bar, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 7, y: 0 }, + ], + }; + const specDataSeries = [ds1, ds2]; + const customDomain = { + min: 0, + }; + + const { xValues } = getDataSeriesFromSpecs(specDataSeries); + const scalesConfig = getScaleConfigsFromSpecs( + [], + specDataSeries, + MockGlobalSpec.settings({ xDomain: customDomain }), + ); + + const getResult = () => mergeXDomain(scalesConfig.x, xValues, ScaleType.Ordinal); + + expect(getResult).not.toThrow(); + + const mergedDomain = getResult(); + expect(mergedDomain.domain).toEqual([0, 'a', 2, 5, 7]); + expect(mergedDomain.type).toEqual(ScaleType.Ordinal); + }); + + test('Should merge multi bar/line ordinal series correctly', () => { + const ds1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds1', + groupId: 'g1', + seriesType: SeriesType.Bar, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 2, y: 0 }, + { x: 5, y: 0 }, + ], + }; + const ds2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds2', + groupId: 'g2', + seriesType: SeriesType.Line, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 7, y: 0 }, + ], + }; + const specDataSeries = [ds1, ds2]; + + const { xValues } = getDataSeriesFromSpecs(specDataSeries); + const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); + + const mergedDomain = mergeXDomain(scalesConfig.x, xValues); + expect(mergedDomain.domain).toEqual([0, 1, 2, 5, 7]); + }); + test('Should merge multi bar/line time series correctly', () => { + const ds1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds1', + groupId: 'g1', + seriesType: SeriesType.Bar, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 2, y: 0 }, + { x: 5, y: 0 }, + ], + }; + const ds2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds2', + groupId: 'g2', + seriesType: SeriesType.Line, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 7, y: 0 }, + ], + }; + const specDataSeries = [ds1, ds2]; + + const { xValues } = getDataSeriesFromSpecs(specDataSeries); + const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); + + const mergedDomain = mergeXDomain(scalesConfig.x, xValues); + expect(mergedDomain.domain).toEqual([0, 1, 2, 5, 7]); + }); + test('Should merge multi lines series correctly', () => { + const ds1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds1', + groupId: 'g1', + seriesType: SeriesType.Line, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 2, y: 0 }, + { x: 5, y: 0 }, + ], + }; + const ds2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds2', + groupId: 'g2', + seriesType: SeriesType.Line, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 7, y: 0 }, + ], + }; + const specDataSeries = [ds1, ds2]; + + const { xValues } = getDataSeriesFromSpecs(specDataSeries); + const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); + + const mergedDomain = mergeXDomain(scalesConfig.x, xValues); + expect(mergedDomain.domain).toEqual([0, 1, 2, 5, 7]); + }); + + test('Should merge X multi high volume of data', () => { + const maxValues = 10000; + const ds1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds1', + groupId: 'g1', + seriesType: SeriesType.Area, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + data: new Array(maxValues).fill(0).map((d, i) => ({ x: i, y: i })), + }; + const ds2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'ds2', + groupId: 'g2', + seriesType: SeriesType.Line, + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + data: new Array(maxValues).fill(0).map((d, i) => ({ x: i, y: i })), + }; + const specDataSeries = [ds1, ds2]; + + const { xValues } = getDataSeriesFromSpecs(specDataSeries); + const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); + + const mergedDomain = mergeXDomain(scalesConfig.x, xValues); + expect(mergedDomain.domain.length).toEqual(maxValues); + }); + test('should compute minInterval an ordered list of numbers', () => { + const minInterval = findMinInterval([0, 1, 2, 3, 4, 5]); + expect(minInterval).toBe(1); + }); + test('should compute minInterval an unordered list of numbers', () => { + const minInterval = findMinInterval([2, 10, 3, 1, 5]); + expect(minInterval).toBe(1); + }); + test('should compute minInterval an list greater than 9', () => { + const minInterval = findMinInterval([0, 2, 4, 6, 8, 10, 20, 30, 40, 50, 80]); + expect(minInterval).toBe(2); + }); + test('should compute minInterval an list with negative numbers', () => { + const minInterval = findMinInterval([-1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -11, -12]); + expect(minInterval).toBe(1); + }); + test('should compute minInterval an list with negative and positive numbers', () => { + const minInterval = findMinInterval([-2, -4, -6, -8, -10, -12, 0, 2, 4, 6, 8, 10, 12]); + expect(minInterval).toBe(2); + }); + test('should compute minInterval a single element array', () => { + const minInterval = findMinInterval([100]); + expect(minInterval).toBe(1); + }); + test('should compute minInterval a empty element array', () => { + const minInterval = findMinInterval([]); + expect(minInterval).toBe(0); + }); + test('should account for custom domain when merging a linear domain: complete bounded domain', () => { + const xValues = new Set([1, 2, 3, 4, 5]); + const xDomain = { min: 0, max: 3 }; + const specs = [MockSeriesSpec.line({ xScaleType: ScaleType.Linear })]; + + const basicMergedDomain = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain })).x, + xValues, + ); + expect(basicMergedDomain.domain).toEqual([0, 3]); + + const arrayXDomain = [1, 2]; + let { domain } = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: arrayXDomain })).x, + xValues, + ); + expect(domain).toEqual([1, 5]); + const warnMessage = 'xDomain for continuous scale should be a DomainRange object, not an array'; + expect(Logger.warn).toBeCalledWith(warnMessage); + + (Logger.warn as jest.Mock).mockClear(); + + const invalidXDomain = { min: 10, max: 0 }; + domain = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: invalidXDomain })).x, + xValues, + ).domain; + expect(domain).toEqual([1, 5]); + expect(Logger.warn).toBeCalledWith('custom xDomain is invalid, min is greater than max. Custom domain is ignored.'); + }); + + test('should account for custom domain when merging a linear domain: lower bounded domain', () => { + const xValues = new Set([1, 2, 3, 4, 5]); + const xDomain = { min: 0 }; + const specs = [MockSeriesSpec.line({ xScaleType: ScaleType.Linear })]; + + const mergedDomain = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain })).x, + xValues, + ); + expect(mergedDomain.domain).toEqual([0, 5]); + + const invalidXDomain = { min: 10 }; + const { domain } = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: invalidXDomain })).x, + xValues, + ); + expect(domain).toEqual([1, 5]); + expect(Logger.warn).toBeCalledWith( + 'custom xDomain is invalid, custom min is greater than computed max. Custom domain is ignored.', + ); + }); + + test('should account for custom domain when merging a linear domain: upper bounded domain', () => { + const xValues = new Set([1, 2, 3, 4, 5]); + const xDomain = { max: 3 }; + const specs = [MockSeriesSpec.line({ xScaleType: ScaleType.Linear })]; + + const mergedDomain = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain })).x, + xValues, + ); + expect(mergedDomain.domain).toEqual([1, 3]); + + const invalidXDomain = { max: -1 }; + const { domain } = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: invalidXDomain })).x, + xValues, + ); + expect(domain).toEqual([1, 5]); + expect(Logger.warn).toBeCalledWith( + 'custom xDomain is invalid, computed min is greater than custom max. Custom domain is ignored.', + ); + }); + + test('should account for custom domain when merging an ordinal domain', () => { + const xValues = new Set(['a', 'b', 'c', 'd']); + const xDomain = ['a', 'b', 'c']; + const specs = [MockSeriesSpec.bar({ xScaleType: ScaleType.Ordinal })]; + const basicMergedDomain = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain })).x, + xValues, + ); + expect(basicMergedDomain.domain).toEqual(['a', 'b', 'c']); + + const objectXDomain = { max: 10, min: 0 }; + const { domain } = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: objectXDomain })).x, + xValues, + ); + expect(domain).toEqual(['a', 'b', 'c', 'd']); + const warnMessage = + 'xDomain for ordinal scale should be an array of values, not a DomainRange object. xDomain is ignored.'; + expect(Logger.warn).toBeCalledWith(warnMessage); + }); + + describe('should account for custom minInterval', () => { + const xValues = new Set([1, 2, 3, 4, 5]); + const specs = [MockSeriesSpec.bar({ xScaleType: ScaleType.Linear })]; + + test('with valid minInterval', () => { + const xDomain = { minInterval: 0.5 }; + const mergedDomain = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain })).x, + xValues, + ); + expect(mergedDomain.minInterval).toEqual(0.5); + }); + + test('with valid minInterval greater than computed minInterval for single datum set', () => { + const xDomain = { minInterval: 10 }; + const mergedDomain = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain })).x, + new Set([5]), + ); + expect(mergedDomain.minInterval).toEqual(10); + }); + + test('with invalid minInterval greater than computed minInterval for multi data set', () => { + const invalidXDomain = { minInterval: 10 }; + const { minInterval } = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: invalidXDomain })).x, + xValues, + ); + expect(minInterval).toEqual(1); + const expectedWarning = + 'custom xDomain is invalid, custom minInterval is greater than computed minInterval. Using computed minInterval.'; + expect(Logger.warn).toBeCalledWith(expectedWarning); + }); + + test('with invalid minInterval less than 0', () => { + const invalidXDomain = { minInterval: -1 }; + const { minInterval } = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: invalidXDomain })).x, + xValues, + ); + expect(minInterval).toEqual(1); + const expectedWarning = + 'custom xDomain is invalid, custom minInterval is less than 0. Using computed minInterval.'; + expect(Logger.warn).toBeCalledWith(expectedWarning); + }); + }); + + describe('orderOrdinalBinsBySum', () => { + const ordinalSpecs = MockSeriesSpecs.fromPartialSpecs([ + { + id: 'ordinal1', + seriesType: SeriesType.Bar, + xScaleType: ScaleType.Ordinal, + data: [ + { x: 'a', y: 2 }, + { x: 'b', y: 4 }, + { x: 'c', y: 8 }, + { x: 'd', y: 6 }, + ], + }, + { + id: 'ordinal2', + seriesType: SeriesType.Bar, + xScaleType: ScaleType.Ordinal, + data: [ + { x: 'a', y: 4 }, + { x: 'b', y: 8 }, + { x: 'c', y: 16 }, + { x: 'd', y: 12 }, + ], + }, + ]); + + const linearSpecs = MockSeriesSpecs.fromPartialSpecs([ + { + id: 'linear1', + seriesType: SeriesType.Bar, + xScaleType: ScaleType.Linear, + data: [ + { x: 1, y: 2 }, + { x: 2, y: 4 }, + { x: 3, y: 8 }, + { x: 4, y: 6 }, + ], + }, + { + id: 'linear2', + seriesType: SeriesType.Bar, + xScaleType: ScaleType.Linear, + data: [ + { x: 1, y: 4 }, + { x: 2, y: 8 }, + { x: 3, y: 16 }, + { x: 4, y: 12 }, + ], + }, + ]); + + it('should sort ordinal xValues by descending sum by default', () => { + const { xValues } = getDataSeriesFromSpecs(ordinalSpecs, [], {}); + expect(xValues).toEqual(new Set(['c', 'd', 'b', 'a'])); + }); + + it('should sort ordinal xValues by descending sum', () => { + const { xValues } = getDataSeriesFromSpecs(ordinalSpecs, [], { + binAgg: BinAgg.None, + direction: Direction.Descending, + }); + expect(xValues).toEqual(new Set(['c', 'd', 'b', 'a'])); + }); + + it('should sort ordinal xValues by ascending sum', () => { + const { xValues } = getDataSeriesFromSpecs(ordinalSpecs, [], { + binAgg: BinAgg.None, + direction: Direction.Ascending, + }); + expect(xValues).toEqual(new Set(['a', 'b', 'd', 'c'])); + }); + + it('should NOT sort ordinal xValues sum', () => { + const { xValues } = getDataSeriesFromSpecs(ordinalSpecs, [], undefined); + expect(xValues).toEqual(new Set(['a', 'b', 'c', 'd'])); + }); + + it('should NOT sort ordinal xValues sum when undefined', () => { + const { xValues } = getDataSeriesFromSpecs(ordinalSpecs, [], { + binAgg: BinAgg.None, + direction: Direction.Descending, + }); + expect(xValues).toEqual(new Set(['a', 'b', 'c', 'd'])); + }); + + it('should NOT sort linear xValue by descending sum', () => { + const { xValues } = getDataSeriesFromSpecs(linearSpecs, [], { + direction: Direction.Descending, + }); + expect(xValues).toEqual(new Set([1, 2, 3, 4])); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/domains/x_domain.ts b/packages/osd-charts/src/chart_types/xy_chart/domains/x_domain.ts new file mode 100644 index 000000000000..97521a733b0f --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/domains/x_domain.ts @@ -0,0 +1,222 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Optional } from 'utility-types'; + +import { ScaleType } from '../../../scales/constants'; +import { compareByValueAsc, identity } from '../../../utils/common'; +import { computeContinuousDataDomain, computeOrdinalDataDomain } from '../../../utils/domain'; +import { Logger } from '../../../utils/logger'; +import { getXNiceFromSpec, getXScaleTypeFromSpec } from '../scales/get_api_scales'; +import { ScaleConfigs } from '../state/selectors/get_api_scale_configs'; +import { isCompleteBound, isLowerBound, isUpperBound } from '../utils/axis_type_utils'; +import { BasicSeriesSpec, SeriesType, XScaleType } from '../utils/specs'; +import { areAllNiceDomain } from './nice'; +import { XDomain } from './types'; + +/** + * Merge X domain value between a set of chart specification. + * @internal + */ +export function mergeXDomain( + { type, nice, isBandScale, timeZone, desiredTickCount, customDomain }: ScaleConfigs['x'], + xValues: Set, + fallbackScale?: XScaleType, +): XDomain { + const values = [...xValues.values()]; + let seriesXComputedDomains; + let minInterval = 0; + + if (type === ScaleType.Ordinal || fallbackScale === ScaleType.Ordinal) { + if (type !== ScaleType.Ordinal) { + Logger.warn(`Each X value in a ${type} x scale needs be be a number. Using ordinal x scale as fallback.`); + } + + seriesXComputedDomains = computeOrdinalDataDomain(values, identity, false, true); + if (customDomain) { + if (Array.isArray(customDomain)) { + seriesXComputedDomains = customDomain; + } else { + if (fallbackScale === ScaleType.Ordinal) { + Logger.warn(`xDomain ignored for fallback ordinal scale. Options to resolve: + +1) Correct data to match ${type} scale type (see previous warning) +2) Change xScaleType to ordinal and set xDomain to Domain array`); + } else { + Logger.warn( + 'xDomain for ordinal scale should be an array of values, not a DomainRange object. xDomain is ignored.', + ); + } + } + } + } else { + seriesXComputedDomains = computeContinuousDataDomain(values, identity, type, { + fit: true, + }); + let customMinInterval: undefined | number; + + if (customDomain) { + if (Array.isArray(customDomain)) { + Logger.warn('xDomain for continuous scale should be a DomainRange object, not an array'); + } else { + customMinInterval = customDomain.minInterval; + const [computedDomainMin, computedDomainMax] = seriesXComputedDomains; + + if (isCompleteBound(customDomain)) { + if (customDomain.min > customDomain.max) { + Logger.warn('custom xDomain is invalid, min is greater than max. Custom domain is ignored.'); + } else { + seriesXComputedDomains = [customDomain.min, customDomain.max]; + } + } else if (isLowerBound(customDomain)) { + if (customDomain.min > computedDomainMax) { + Logger.warn( + 'custom xDomain is invalid, custom min is greater than computed max. Custom domain is ignored.', + ); + } else { + seriesXComputedDomains = [customDomain.min, computedDomainMax]; + } + } else if (isUpperBound(customDomain)) { + if (computedDomainMin > customDomain.max) { + Logger.warn( + 'custom xDomain is invalid, computed min is greater than custom max. Custom domain is ignored.', + ); + } else { + seriesXComputedDomains = [computedDomainMin, customDomain.max]; + } + } + } + } + const computedMinInterval = findMinInterval(values as number[]); + minInterval = getMinInterval(computedMinInterval, xValues.size, customMinInterval); + } + + return { + type: fallbackScale ?? type, + nice, + isBandScale, + domain: seriesXComputedDomains, + minInterval, + timeZone, + logBase: customDomain && 'logBase' in customDomain ? customDomain.logBase : undefined, + desiredTickCount, + }; +} + +function getMinInterval(computedMinInterval: number, size: number, customMinInterval?: number): number { + if (customMinInterval == null) { + return computedMinInterval; + } + // Allow greater custom min if xValues has 1 member. + if (size > 1 && customMinInterval > computedMinInterval) { + Logger.warn( + 'custom xDomain is invalid, custom minInterval is greater than computed minInterval. Using computed minInterval.', + ); + return computedMinInterval; + } + if (customMinInterval < 0) { + Logger.warn('custom xDomain is invalid, custom minInterval is less than 0. Using computed minInterval.'); + return computedMinInterval; + } + + return customMinInterval; +} + +/** + * Find the minimum interval between xValues. + * Default to 0 if an empty array, 1 if one item array + * @internal + */ +export function findMinInterval(xValues: number[]): number { + const valuesLength = xValues.length; + if (valuesLength <= 0) { + return 0; + } + if (valuesLength === 1) { + return 1; + } + const sortedValues = xValues.slice().sort(compareByValueAsc); + let i; + let minInterval = Math.abs(sortedValues[1] - sortedValues[0]); + for (i = 1; i < valuesLength - 1; i++) { + const current = sortedValues[i]; + const next = sortedValues[i + 1]; + const interval = Math.abs(next - current); + minInterval = Math.min(minInterval, interval); + } + return minInterval; +} + +/** + * Convert the scale types of a set of specification to a generic one. + * If there are at least one `ordinal` scale type, the resulting scale is coerced to ordinal. + * If there are only `continuous` scale types, the resulting scale is coerced to linear. + * If there are only `time` scales, we coerce the timeZone to `utc` only if we have multiple + * different timezones. + * @returns the coerced scale type, the timezone and a parameter that describe if its a bandScale or not + * @internal + */ +export function convertXScaleTypes( + specs: Optional, 'seriesType'>[], +): { + type: XScaleType; + nice: boolean; + isBandScale: boolean; + timeZone?: string; +} { + const seriesTypes = new Set(); + const scaleTypes = new Set(); + const timeZones = new Set(); + const niceDomainConfigs: Array = []; + specs.forEach((spec) => { + niceDomainConfigs.push(getXNiceFromSpec(spec.xNice)); + seriesTypes.add(spec.seriesType); + scaleTypes.add(getXScaleTypeFromSpec(spec.xScaleType)); + if (spec.timeZone) { + timeZones.add(spec.timeZone.toLowerCase()); + } + }); + if (specs.length === 0 || seriesTypes.size === 0 || scaleTypes.size === 0) { + return { + type: ScaleType.Linear, + nice: true, + isBandScale: false, + }; + } + const nice = areAllNiceDomain(niceDomainConfigs); + const isBandScale = seriesTypes.has(SeriesType.Bar); + if (scaleTypes.size === 1) { + const scaleType = scaleTypes.values().next().value; + const timeZone = timeZones.size > 1 ? 'utc' : timeZones.values().next().value; + return { type: scaleType, nice, isBandScale, timeZone }; + } + + if (scaleTypes.size > 1 && scaleTypes.has(ScaleType.Ordinal)) { + return { + type: ScaleType.Ordinal, + nice, + isBandScale, + }; + } + return { + type: ScaleType.Linear, + nice, + isBandScale, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/domains/y_domain.test.ts b/packages/osd-charts/src/chart_types/xy_chart/domains/y_domain.test.ts new file mode 100644 index 000000000000..fdb3239f257c --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/domains/y_domain.test.ts @@ -0,0 +1,551 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartType } from '../..'; +import { MockSeriesSpec, MockGlobalSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store'; +import { MockYDomain } from '../../../mocks/xy/domains'; +import { ScaleType } from '../../../scales/constants'; +import { SpecType } from '../../../specs/constants'; +import { Position } from '../../../utils/common'; +import { BARCHART_1Y0G } from '../../../utils/data_samples/test_dataset'; +import { Logger } from '../../../utils/logger'; +import { computeSeriesDomainsSelector } from '../state/selectors/compute_series_domains'; +import { BasicSeriesSpec, SeriesType, DEFAULT_GLOBAL_ID, StackMode } from '../utils/specs'; +import { coerceYScaleTypes, groupSeriesByYGroup } from './y_domain'; + +jest.mock('../../../utils/logger', () => ({ + Logger: { + warn: jest.fn(), + }, +})); + +const DEMO_AREA_SPEC_1 = { + id: 'a', + groupId: 'a', + yAccessors: ['y1'], + stackAccessors: ['x'], + splitSeriesAccessors: ['g'], + yScaleType: ScaleType.Linear, + data: [ + { x: 1, y1: 2, g: 'a' }, + { x: 2, y1: 2, g: 'a' }, + { x: 3, y1: 2, g: 'a' }, + { x: 4, y1: 5, g: 'a' }, + + { x: 1, y1: 2, g: 'b' }, + { x: 4, y1: 7, g: 'b' }, + ], +}; +const DEMO_AREA_SPEC_2 = { + id: 'b', + yAccessors: ['y1'], + yScaleType: ScaleType.Log, + data: [ + { x: 1, y1: 10 }, + { x: 2, y1: 10 }, + { x: 3, y1: 2 }, + { x: 4, y1: 5 }, + ], +}; + +describe('Y Domain', () => { + test('Should merge Y domain for non zero baseline charts', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockGlobalSpec.axis({ + id: 'y', + position: Position.Left, + domain: { fit: true }, + }), + MockSeriesSpec.line({ + ...DEMO_AREA_SPEC_1, + groupId: DEFAULT_GLOBAL_ID, + }), + ], + store, + ); + const { yDomains } = computeSeriesDomainsSelector(store.getState()); + + expect(yDomains).toEqual([ + MockYDomain.fromScaleType(ScaleType.Linear, { + groupId: DEFAULT_GLOBAL_ID, + domain: [2, 12], + isBandScale: false, + }), + ]); + }); + test('Should merge Y domain for zero baseline charts', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockGlobalSpec.axis({ + id: 'y', + position: Position.Left, + domain: { fit: true }, + }), + MockSeriesSpec.area({ + ...DEMO_AREA_SPEC_1, + groupId: DEFAULT_GLOBAL_ID, + }), + ], + store, + ); + const { yDomains } = computeSeriesDomainsSelector(store.getState()); + + expect(yDomains).toEqual([ + MockYDomain.fromScaleType(ScaleType.Linear, { + groupId: DEFAULT_GLOBAL_ID, + domain: [0, 12], + isBandScale: false, + }), + ]); + }); + test('Should merge Y domain different group', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockGlobalSpec.axis({ + id: 'y a', + groupId: 'a', + position: Position.Left, + domain: { fit: true }, + }), + MockGlobalSpec.axis({ + id: 'y b', + groupId: 'b', + position: Position.Left, + domain: { fit: true }, + }), + MockSeriesSpec.line(DEMO_AREA_SPEC_1), + MockSeriesSpec.line({ + ...DEMO_AREA_SPEC_2, + groupId: 'b', + }), + ], + store, + ); + const { yDomains } = computeSeriesDomainsSelector(store.getState()); + + expect(yDomains).toEqual([ + MockYDomain.fromScaleType(ScaleType.Linear, { + groupId: 'a', + domain: [2, 12], + isBandScale: false, + }), + MockYDomain.fromScaleType(ScaleType.Log, { + groupId: 'b', + domain: [2, 10], + isBandScale: false, + }), + ]); + }); + test('Should merge Y domain same group all stacked', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockGlobalSpec.axis({ + id: 'y a', + groupId: 'a', + position: Position.Left, + domain: { fit: true }, + }), + MockSeriesSpec.area(DEMO_AREA_SPEC_1), + MockSeriesSpec.area({ + ...DEMO_AREA_SPEC_2, + groupId: 'a', + stackAccessors: ['x'], + }), + ], + store, + ); + const { yDomains } = computeSeriesDomainsSelector(store.getState()); + + expect(yDomains).toEqual([ + MockYDomain.fromScaleType(ScaleType.Linear, { + groupId: 'a', + domain: [0, 17], + isBandScale: false, + }), + ]); + }); + test('Should merge Y domain same group partially stacked', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockGlobalSpec.axis({ + id: 'y a', + groupId: 'a', + position: Position.Left, + domain: { fit: true }, + }), + MockSeriesSpec.area(DEMO_AREA_SPEC_1), + MockSeriesSpec.area({ + ...DEMO_AREA_SPEC_2, + groupId: 'a', + }), + ], + store, + ); + const { yDomains } = computeSeriesDomainsSelector(store.getState()); + expect(yDomains).toEqual([ + MockYDomain.fromScaleType(ScaleType.Linear, { + groupId: 'a', + domain: [0, 12], + isBandScale: false, + }), + ]); + }); + + test('Should split specs by groupId, two groups, non stacked', () => { + const spec1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'spec1', + groupId: 'group1', + seriesType: SeriesType.Line, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + data: BARCHART_1Y0G, + }; + const spec2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'spec2', + groupId: 'group2', + seriesType: SeriesType.Line, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + data: BARCHART_1Y0G, + }; + const splitSpecs = groupSeriesByYGroup([spec1, spec2]); + const groupKeys = [...splitSpecs.keys()]; + const groupValues = [...splitSpecs.values()]; + expect(groupKeys).toEqual(['group1', 'group2']); + expect(groupValues.length).toBe(2); + expect(groupValues[0].nonStacked).toEqual([spec1]); + expect(groupValues[1].nonStacked).toEqual([spec2]); + expect(groupValues[0].stacked).toEqual([]); + expect(groupValues[1].stacked).toEqual([]); + }); + test('Should split specs by groupId, two groups, stacked', () => { + const spec1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'spec1', + groupId: 'group1', + seriesType: SeriesType.Line, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + stackAccessors: ['x'], + data: BARCHART_1Y0G, + }; + const spec2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'spec2', + groupId: 'group2', + seriesType: SeriesType.Line, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + stackAccessors: ['x'], + data: BARCHART_1Y0G, + }; + const splitSpecs = groupSeriesByYGroup([spec1, spec2]); + const groupKeys = [...splitSpecs.keys()]; + const groupValues = [...splitSpecs.values()]; + expect(groupKeys).toEqual(['group1', 'group2']); + expect(groupValues.length).toBe(2); + expect(groupValues[0].stacked).toEqual([spec1]); + expect(groupValues[1].stacked).toEqual([spec2]); + expect(groupValues[0].nonStacked).toEqual([]); + expect(groupValues[1].nonStacked).toEqual([]); + }); + test('Should split specs by groupId, 1 group, stacked', () => { + const spec1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'spec1', + groupId: 'group', + seriesType: SeriesType.Line, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + stackAccessors: ['x'], + data: BARCHART_1Y0G, + }; + const spec2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'spec2', + groupId: 'group', + seriesType: SeriesType.Line, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + stackAccessors: ['x'], + data: BARCHART_1Y0G, + }; + const splitSpecs = groupSeriesByYGroup([spec1, spec2]); + const groupKeys = [...splitSpecs.keys()]; + const groupValues = [...splitSpecs.values()]; + expect(groupKeys).toEqual(['group']); + expect(groupValues.length).toBe(1); + expect(groupValues[0].stacked).toEqual([spec1, spec2]); + expect(groupValues[0].nonStacked).toEqual([]); + }); + test('Should 3 split specs by groupId, 2 group, semi/stacked', () => { + const spec1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'spec1', + groupId: 'group1', + seriesType: SeriesType.Line, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + stackAccessors: ['x'], + data: BARCHART_1Y0G, + }; + const spec2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'spec2', + groupId: 'group1', + seriesType: SeriesType.Line, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + stackAccessors: ['x'], + data: BARCHART_1Y0G, + }; + const spec3: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'spec3', + groupId: 'group2', + seriesType: SeriesType.Line, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + stackAccessors: ['x'], + data: BARCHART_1Y0G, + }; + const splitSpecs = groupSeriesByYGroup([spec1, spec2, spec3]); + const groupKeys = [...splitSpecs.keys()]; + const groupValues = [...splitSpecs.values()]; + expect(groupKeys).toEqual(['group1', 'group2']); + expect(groupValues.length).toBe(2); + expect(groupValues[0].stacked).toEqual([spec1, spec2]); + expect(groupValues[0].nonStacked).toEqual([]); + expect(groupValues[1].stacked).toEqual([spec3]); + expect(groupValues[0].nonStacked).toEqual([]); + }); + + test('Should return a default Scale Linear for YScaleType when there are no specs', () => { + expect(coerceYScaleTypes([])).toEqual({ + nice: false, + type: ScaleType.Linear, + }); + }); + + test('Should merge Y domain accounting for custom domain limits: complete bounded domain', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockGlobalSpec.axis({ + id: 'y a', + groupId: 'a', + position: Position.Left, + domain: { min: 0, max: 20, fit: true }, + }), + MockSeriesSpec.area(DEMO_AREA_SPEC_1), + ], + store, + ); + const { yDomains } = computeSeriesDomainsSelector(store.getState()); + + expect(yDomains).toEqual([ + MockYDomain.fromScaleType(ScaleType.Linear, { + groupId: 'a', + domain: [0, 20], + isBandScale: false, + }), + ]); + }); + test('Should merge Y domain accounting for custom domain limits: partial lower bounded domain', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockGlobalSpec.axis({ + id: 'y a', + groupId: 'a', + position: Position.Left, + domain: { min: 0, fit: true }, + }), + MockSeriesSpec.area(DEMO_AREA_SPEC_1), + ], + store, + ); + const { yDomains } = computeSeriesDomainsSelector(store.getState()); + + expect(yDomains).toEqual([ + MockYDomain.fromScaleType(ScaleType.Linear, { + groupId: 'a', + domain: [0, 12], + isBandScale: false, + }), + ]); + }); + test('Should not merge Y domain with invalid custom domain limits: partial lower bounded domain', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockGlobalSpec.axis({ + id: 'y a', + groupId: 'a', + position: Position.Left, + domain: { min: 20, fit: true }, + }), + MockSeriesSpec.area(DEMO_AREA_SPEC_1), + ], + store, + ); + + const { + yDomains: [{ domain }], + } = computeSeriesDomainsSelector(store.getState()); + expect(domain).toEqual([20, 20]); + + const warnMessage = 'custom yDomain for a is invalid, custom min is greater than computed max.'; + expect(Logger.warn).toBeCalledWith(warnMessage); + }); + test('Should merge Y domain accounting for custom domain limits: partial upper bounded domain', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockGlobalSpec.axis({ + id: 'y a', + groupId: 'a', + position: Position.Left, + domain: { max: 20, fit: true }, + }), + MockSeriesSpec.line(DEMO_AREA_SPEC_1), + ], + store, + ); + + const { yDomains } = computeSeriesDomainsSelector(store.getState()); + expect(yDomains).toEqual([ + MockYDomain.fromScaleType(ScaleType.Linear, { + groupId: 'a', + domain: [2, 20], + isBandScale: false, + }), + ]); + }); + test('Should not merge Y domain with invalid custom domain limits: partial upper bounded domain', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockGlobalSpec.axis({ + id: 'y a', + groupId: 'a', + position: Position.Left, + domain: { max: -1, fit: true }, + }), + MockSeriesSpec.area(DEMO_AREA_SPEC_1), + ], + store, + ); + + const { + yDomains: [{ domain }], + } = computeSeriesDomainsSelector(store.getState()); + expect(domain).toEqual([-1, -1]); + + const warnMessage = 'custom yDomain for a is invalid, custom max is less than computed max.'; + expect(Logger.warn).toBeCalledWith(warnMessage); + }); + test('Should merge Y domain with stacked as percentage', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockSeriesSpec.area({ + ...DEMO_AREA_SPEC_1, + stackMode: StackMode.Percentage, + }), + MockSeriesSpec.area({ + ...DEMO_AREA_SPEC_2, + groupId: 'a', + }), + ], + store, + ); + + const { yDomains } = computeSeriesDomainsSelector(store.getState()); + expect(yDomains).toEqual([ + MockYDomain.fromScaleType(ScaleType.Linear, { + groupId: 'a', + domain: [0, 1], + isBandScale: false, + }), + ]); + }); + test('Should merge Y domain with as percentage regadless of custom domains', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockGlobalSpec.axis({ + id: 'y a', + groupId: 'a', + position: Position.Left, + domain: { min: 2, max: 20, fit: true }, + }), + MockSeriesSpec.area({ + ...DEMO_AREA_SPEC_1, + stackMode: StackMode.Percentage, + }), + ], + store, + ); + const { yDomains } = computeSeriesDomainsSelector(store.getState()); + expect(yDomains).toEqual([ + MockYDomain.fromScaleType(ScaleType.Linear, { + groupId: 'a', + domain: [0, 1], + isBandScale: false, + }), + ]); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/domains/y_domain.ts b/packages/osd-charts/src/chart_types/xy_chart/domains/y_domain.ts new file mode 100644 index 000000000000..24d1d564c82b --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/domains/y_domain.ts @@ -0,0 +1,243 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ScaleContinuousType } from '../../../scales'; +import { ScaleType } from '../../../scales/constants'; +import { identity } from '../../../utils/common'; +import { computeContinuousDataDomain, ContinuousDomain } from '../../../utils/domain'; +import { GroupId } from '../../../utils/ids'; +import { Logger } from '../../../utils/logger'; +import { ScaleConfigs } from '../state/selectors/get_api_scale_configs'; +import { getSpecDomainGroupId } from '../state/utils/spec'; +import { isCompleteBound, isLowerBound, isUpperBound } from '../utils/axis_type_utils'; +import { groupBy } from '../utils/group_data_series'; +import { DataSeries } from '../utils/series'; +import { BasicSeriesSpec, YDomainRange, SeriesType, StackMode, DomainPaddingUnit } from '../utils/specs'; +import { areAllNiceDomain } from './nice'; +import { YDomain } from './types'; + +/** @internal */ +export type YBasicSeriesSpec = Pick< + BasicSeriesSpec, + 'id' | 'seriesType' | 'yScaleType' | 'groupId' | 'stackAccessors' | 'useDefaultGroupDomain' +> & { stackMode?: StackMode; enableHistogramMode?: boolean }; + +/** @internal */ +export function mergeYDomain(dataSeries: DataSeries[], yScaleAPIConfig: ScaleConfigs['y']): YDomain[] { + const dataSeriesByGroupId = groupBy(dataSeries, ({ spec }) => getSpecDomainGroupId(spec), true); + + return dataSeriesByGroupId.reduce((acc, groupedDataSeries) => { + const stacked = groupedDataSeries.filter(({ isStacked, isFiltered }) => isStacked && !isFiltered); + const nonStacked = groupedDataSeries.filter(({ isStacked, isFiltered }) => !isStacked && !isFiltered); + const hasNonZeroBaselineTypes = groupedDataSeries.some( + ({ seriesType, isFiltered }) => seriesType === SeriesType.Bar || (seriesType === SeriesType.Area && !isFiltered), + ); + const domain = mergeYDomainForGroup(stacked, nonStacked, hasNonZeroBaselineTypes, yScaleAPIConfig); + if (!domain) { + return acc; + } + return [...acc, domain]; + }, []); +} + +function mergeYDomainForGroup( + stacked: DataSeries[], + nonStacked: DataSeries[], + hasZeroBaselineSpecs: boolean, + yScaleConfig: ScaleConfigs['y'], +): YDomain | null { + const dataSeries = [...stacked, ...nonStacked]; + if (dataSeries.length === 0) { + return null; + } + + const [{ stackMode, spec }] = dataSeries; + const groupId = getSpecDomainGroupId(spec); + const { customDomain, type, nice, desiredTickCount } = yScaleConfig[groupId]; + const newCustomDomain = customDomain ? { ...customDomain } : {}; + + let domain: ContinuousDomain; + if (stackMode === StackMode.Percentage) { + domain = computeContinuousDataDomain([0, 1], identity, type, customDomain); + } else { + // compute stacked domain + const stackedDomain = computeYDomain(stacked, hasZeroBaselineSpecs, type, newCustomDomain); + + // compute non stacked domain + const nonStackedDomain = computeYDomain(nonStacked, hasZeroBaselineSpecs, type, newCustomDomain); + + // merge stacked and non stacked domain together + domain = computeContinuousDataDomain([...stackedDomain, ...nonStackedDomain], identity, type, newCustomDomain); + + const [computedDomainMin, computedDomainMax] = domain; + + if (newCustomDomain && isCompleteBound(newCustomDomain)) { + // Don't need to check min > max because this has been validated on axis domain merge + domain = [newCustomDomain.min, newCustomDomain.max]; + } else if (newCustomDomain && isLowerBound(newCustomDomain)) { + if (newCustomDomain.min > computedDomainMax) { + Logger.warn(`custom yDomain for ${groupId} is invalid, custom min is greater than computed max.`); + domain = [newCustomDomain.min, newCustomDomain.min]; + } else { + domain = [newCustomDomain.min, computedDomainMax]; + } + } else if (newCustomDomain && isUpperBound(newCustomDomain)) { + if (computedDomainMin > newCustomDomain.max) { + Logger.warn(`custom yDomain for ${groupId} is invalid, custom max is less than computed max.`); + domain = [newCustomDomain.max, newCustomDomain.max]; + } else { + domain = [computedDomainMin, newCustomDomain.max]; + } + } + } + return { + type, + nice, + isBandScale: false, + groupId, + domain, + logBase: customDomain?.logBase, + logMinLimit: customDomain?.logMinLimit, + desiredTickCount, + domainPixelPadding: newCustomDomain.paddingUnit === DomainPaddingUnit.Pixel ? newCustomDomain.padding : 0, + constrainDomainPadding: newCustomDomain.constrainPadding, + }; +} + +function computeYDomain( + dataSeries: DataSeries[], + hasZeroBaselineSpecs: boolean, + scaleType: ScaleType, + customDomain?: YDomainRange, +) { + const yValues = new Set(); + dataSeries.forEach(({ data }) => { + for (let i = 0; i < data.length; i++) { + const datum = data[i]; + yValues.add(datum.y1); + if (hasZeroBaselineSpecs && datum.y0 != null) { + yValues.add(datum.y0); + } + } + }); + if (yValues.size === 0) { + return []; + } + const domainOptions = { + ...customDomain, + // padding already applied, set to 0 here to avoid duplicating + padding: 0, + }; + return computeContinuousDataDomain([...yValues.values()], identity, scaleType, domainOptions); +} + +/** @internal */ +export function groupSeriesByYGroup(specs: YBasicSeriesSpec[]) { + const specsByGroupIds = new Map< + GroupId, + { stackMode: StackMode | undefined; stacked: YBasicSeriesSpec[]; nonStacked: YBasicSeriesSpec[] } + >(); + + const histogramEnabled = isHistogramEnabled(specs); + // split each specs by groupId and by stacked or not + specs.forEach((spec) => { + const group = specsByGroupIds.get(spec.groupId) || { + stackMode: undefined, + stacked: [], + nonStacked: [], + }; + + if (isStackedSpec(spec, histogramEnabled)) { + group.stacked.push(spec); + } else { + group.nonStacked.push(spec); + } + if (group.stackMode === undefined && spec.stackMode !== undefined) { + group.stackMode = spec.stackMode; + } + if (spec.stackMode !== undefined && group.stackMode !== undefined && group.stackMode !== spec.stackMode) { + Logger.warn(`Is not possible to mix different stackModes, please align all stackMode on the same GroupId + to the same mode. The default behaviour will be to use the first encountered stackMode on the series`); + } + specsByGroupIds.set(spec.groupId, group); + }); + return specsByGroupIds; +} + +/** + * Histogram mode is forced on every specs if at least one specs has that prop flagged + * @remarks + * After mobx->redux https://github.com/elastic/elastic-charts/pull/281 we keep the specs untouched on mount + * in MobX version, the stackAccessors was programmatically added to every histogram specs + * in ReduX version, we left untouched the specs, so we have to manually check that + * @param specs + * @internal + */ +export function isHistogramEnabled(specs: YBasicSeriesSpec[]) { + return specs.some(({ seriesType, enableHistogramMode }) => seriesType === SeriesType.Bar && enableHistogramMode); +} + +/** + * Return true if the passed spec needs to be rendered as stack + * @param spec + * @param histogramEnabled + * @internal + */ +export function isStackedSpec(spec: YBasicSeriesSpec, histogramEnabled: boolean) { + const isBarAndHistogram = spec.seriesType === SeriesType.Bar && histogramEnabled; + const hasStackAccessors = spec.stackAccessors && spec.stackAccessors.length > 0; + return isBarAndHistogram || hasStackAccessors; +} + +/** + * Coerce the scale types of a set of specification to a generic one. + * If there is at least one bar series type, than the response will specity + * that the coerced scale is a `scaleBand` (each point needs to have a surrounding empty + * space to draw the bar width). + * If there are multiple continuous scale types, is coerced to linear. + * If there are at least one Ordinal scale type, is coerced to ordinal. + * If none of the above, than coerce to the specified scale. + * @returns {ScaleContinuousType} + * @internal + */ +export function coerceYScaleTypes( + scales: Array<{ type: ScaleContinuousType; nice: boolean }>, +): { type: ScaleContinuousType; nice: boolean } { + const scaleCollection = scales.reduce<{ + types: Set; + nice: Array; + }>( + (acc, scale) => { + acc.types.add(scale.type); + acc.nice.push(scale.nice); + return acc; + }, + { + types: new Set(), + nice: [], + }, + ); + const nice = areAllNiceDomain(scaleCollection.nice); + return scaleCollection.types.size === 1 + ? { type: scaleCollection.types.values().next().value, nice } + : { + type: ScaleType.Linear, + nice, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/legend/legend.test.ts b/packages/osd-charts/src/chart_types/xy_chart/legend/legend.test.ts new file mode 100644 index 000000000000..8ba78ea4c442 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/legend/legend.test.ts @@ -0,0 +1,445 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Store } from 'redux'; + +import { ChartType } from '../..'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs/specs'; +import { MockStore } from '../../../mocks/store/store'; +import { ScaleType } from '../../../scales/constants'; +import { SpecType } from '../../../specs/constants'; +import { onToggleDeselectSeriesAction } from '../../../state/actions/legend'; +import { GlobalChartState } from '../../../state/chart_state'; +import { Position, RecursivePartial } from '../../../utils/common'; +import { AxisStyle } from '../../../utils/themes/theme'; +import { computeLegendSelector } from '../state/selectors/compute_legend'; +import { computeSeriesDomainsSelector } from '../state/selectors/compute_series_domains'; +import { getSeriesName } from '../utils/series'; +import { AxisSpec, BasicSeriesSpec, SeriesType } from '../utils/specs'; +import { getLegendExtra } from './legend'; + +const nullDisplayValue = { + formatted: null, + raw: null, + legendSizingLabel: null, +}; + +const spec1: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'spec1', + name: 'Spec 1 title', + groupId: 'group', + seriesType: SeriesType.Line, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + data: [], + hideInLegend: false, +}; +const spec2: BasicSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'spec2', + groupId: 'group', + seriesType: SeriesType.Line, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + data: [], + hideInLegend: false, +}; + +const style: RecursivePartial = { + tickLine: { + size: 10, + padding: 10, + }, +}; +const axesSpecs: AxisSpec[] = []; +const axisSpec: AxisSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Axis, + id: 'axis1', + groupId: 'group1', + hide: false, + showOverlappingTicks: false, + showOverlappingLabels: false, + position: Position.Left, + style, + tickFormat: (value: any) => `${value}`, +}; +axesSpecs.push(axisSpec); + +describe('Legends', () => { + let store: Store; + + beforeEach(() => { + store = MockStore.default(); + }); + it('compute legend for a single series', () => { + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + name: 'Spec 1 title', + yAccessors: ['y1'], + data: [{ x: 0, y1: 1 }], + }), + MockGlobalSpec.settings({ showLegend: true, theme: { colors: { vizColors: ['red'] } } }), + ], + store, + ); + const legend = computeLegendSelector(store.getState()); + const expected = { + color: 'red', + label: 'Spec 1 title', + childId: 'y1', + isItemHidden: false, + isSeriesHidden: false, + isToggleable: true, + defaultExtra: nullDisplayValue, + path: [{ index: 0, value: 'groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{}' }], + }; + expect(legend[0]).toMatchObject(expected); + }); + it('compute legend for a single spec but with multiple series', () => { + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + yAccessors: ['y1', 'y2'], + splitSeriesAccessors: ['g'], + data: [ + { + x: 0, + y1: 1, + g: 'a', + y2: 3, + }, + { + x: 0, + y1: 1, + g: 'b', + y2: 3, + }, + ], + }), + MockGlobalSpec.settings({ + showLegend: true, + theme: { colors: { vizColors: ['red', 'blue', 'violet', 'green'] } }, + }), + ], + store, + ); + const legend = computeLegendSelector(store.getState()); + + const expected = [ + { + color: 'red', + label: 'a - y1', + childId: 'y1', + isItemHidden: false, + isSeriesHidden: false, + isToggleable: true, + defaultExtra: nullDisplayValue, + path: [{ index: 0, value: 'groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-a}' }], + }, + { + color: 'blue', + label: 'a - y2', + childId: 'y1', + isItemHidden: false, + isSeriesHidden: false, + isToggleable: true, + defaultExtra: nullDisplayValue, + path: [{ index: 0, value: 'groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g-a}' }], + }, + { + color: 'violet', + label: 'b - y1', + childId: 'y1', + isItemHidden: false, + isSeriesHidden: false, + isToggleable: true, + defaultExtra: nullDisplayValue, + path: [{ index: 0, value: 'groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-b}' }], + }, + { + color: 'green', + label: 'b - y2', + childId: 'y1', + isItemHidden: false, + isSeriesHidden: false, + isToggleable: true, + defaultExtra: nullDisplayValue, + path: [{ index: 0, value: 'groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g-b}' }], + }, + ]; + expect(legend).toHaveLength(4); + expect(legend).toMatchObject(expected); + }); + it('compute legend for multiple specs', () => { + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + id: 'spec1', + data: [ + { + x: 0, + y: 1, + }, + ], + }), + MockSeriesSpec.bar({ + id: 'spec2', + data: [ + { + x: 0, + y: 1, + }, + ], + }), + MockGlobalSpec.settings({ + showLegend: true, + theme: { colors: { vizColors: ['red', 'blue'] } }, + }), + ], + store, + ); + const legend = computeLegendSelector(store.getState()); + const expected = [ + { + color: 'red', + label: 'spec1', + childId: 'y1', + }, + { + color: 'blue', + label: 'spec2', + childId: 'y1', + isItemHidden: false, + isSeriesHidden: false, + isToggleable: true, + defaultExtra: nullDisplayValue, + path: [{ index: 0, value: 'groupId{__global__}spec{spec2}yAccessor{y}splitAccessors{}' }], + }, + ]; + expect(legend).toHaveLength(2); + expect(legend).toMatchObject(expected); + }); + + it('default all series legend items to visible when deselectedDataSeries is null', () => { + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + id: 'spec1', + data: [ + { + x: 0, + y: 1, + }, + ], + }), + MockSeriesSpec.bar({ + id: 'spec2', + data: [ + { + x: 0, + y: 1, + }, + ], + }), + MockGlobalSpec.settings({ + showLegend: true, + theme: { colors: { vizColors: ['red', 'blue'] } }, + }), + ], + store, + ); + const legend = computeLegendSelector(store.getState()); + + const visibility = legend.map((item) => !item.isSeriesHidden); + + expect(visibility).toEqual([true, true]); + }); + it('selectively sets series to visible when there are deselectedDataSeries items', () => { + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + id: 'spec1', + data: [ + { + x: 0, + y: 1, + }, + ], + }), + MockSeriesSpec.bar({ + id: 'spec2', + data: [ + { + x: 0, + y: 1, + }, + ], + }), + MockGlobalSpec.settings({ + showLegend: true, + theme: { colors: { vizColors: ['red', 'blue'] } }, + }), + ], + store, + ); + const { + formattedDataSeries: [{ key, specId }], + } = computeSeriesDomainsSelector(store.getState()); + + store.dispatch(onToggleDeselectSeriesAction([{ key, specId }])); + const legend = computeLegendSelector(store.getState()); + const visibility = legend.map((item) => !item.isSeriesHidden); + expect(visibility).toEqual([false, true]); + }); + it('returns the right series name for a color series', () => { + const seriesIdentifier1 = { + specId: '', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: ['y1'], + key: '', + }; + const seriesIdentifier2 = { + specId: '', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: ['a', 'b', 'y1'], + key: '', + }; + + // null removed, seriesIdentifier has to be at least an empty array + let name = getSeriesName(seriesIdentifier1, true, false); + expect(name).toBe(''); + name = getSeriesName(seriesIdentifier1, true, false, spec1); + expect(name).toBe('Spec 1 title'); + name = getSeriesName(seriesIdentifier1, true, false, spec2); + expect(name).toBe('spec2'); + name = getSeriesName(seriesIdentifier2, true, false, spec1); + expect(name).toBe('Spec 1 title'); + name = getSeriesName(seriesIdentifier2, true, false, spec2); + expect(name).toBe('spec2'); + + name = getSeriesName(seriesIdentifier1, false, false, spec1); + expect(name).toBe('Spec 1 title'); + name = getSeriesName(seriesIdentifier1, false, false, spec2); + expect(name).toBe('spec2'); + name = getSeriesName(seriesIdentifier2, false, false, spec1); + expect(name).toBe('a - b'); + name = getSeriesName(seriesIdentifier2, false, false, spec2); + expect(name).toBe('a - b'); + + name = getSeriesName(seriesIdentifier1, true, false, spec1); + expect(name).toBe('Spec 1 title'); + name = getSeriesName(seriesIdentifier1, true, false, spec2); + expect(name).toBe('spec2'); + name = getSeriesName(seriesIdentifier1, true, false); + expect(name).toBe(''); + name = getSeriesName(seriesIdentifier1, true, false, spec1); + expect(name).toBe('Spec 1 title'); + name = getSeriesName(seriesIdentifier1, true, false, spec2); + expect(name).toBe('spec2'); + }); + it('use the split value as name if has a single series and splitSeries is used', () => { + const seriesIdentifier1 = { + specId: '', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: ['y1'], + key: '', + }; + const seriesIdentifier2 = { + specId: '', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: ['a', 'b', 'y1'], + key: '', + }; + const seriesIdentifier3 = { + specId: '', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: ['a', 'y1'], + key: '', + }; + + const specWithSplit: BasicSeriesSpec = { + ...spec1, + splitSeriesAccessors: ['g'], + }; + let name = getSeriesName(seriesIdentifier1, true, false, specWithSplit); + expect(name).toBe('Spec 1 title'); + + name = getSeriesName(seriesIdentifier3, true, false, specWithSplit); + expect(name).toBe('a'); + + // happens when we have multiple values in splitSeriesAccessor + // or we have also multiple yAccessors + name = getSeriesName(seriesIdentifier2, true, false, specWithSplit); + expect(name).toBe('a - b'); + + // happens when the value of a splitSeriesAccessor is null + name = getSeriesName(seriesIdentifier1, true, false, specWithSplit); + expect(name).toBe('Spec 1 title'); + + name = getSeriesName(seriesIdentifier1, false, false, specWithSplit); + expect(name).toBe('Spec 1 title'); + }); + it('should return correct legendSizingLabel with linear scale and showExtraLegend set to true', () => { + const formatter = (d: string | number) => `${Number(d).toFixed(2)} dogs`; + const lastValues = { y0: null, y1: 14 }; + const showExtraLegend = true; + const xScaleIsLinear = ScaleType.Linear; + + expect(getLegendExtra(showExtraLegend, xScaleIsLinear, formatter, 'y1', lastValues)).toMatchObject({ + raw: 14, + formatted: '14.00 dogs', + legendSizingLabel: '14.00 dogs', + }); + }); + it('should return formatted to null with ordinal scale and showExtraLegend set to true', () => { + const formatter = (d: string | number) => `${Number(d).toFixed(2)} dogs`; + const lastValues = { y0: null, y1: 14 }; + + expect(getLegendExtra(true, ScaleType.Ordinal, formatter, 'y1', lastValues)).toMatchObject({ + raw: 14, + formatted: null, + legendSizingLabel: '14.00 dogs', + }); + }); + it('should return legendSizingLabel null with showLegendExtra set to false', () => { + const formatter = (d: string | number) => `${Number(d).toFixed(2)} dogs`; + const lastValues = { y0: null, y1: 14 }; + const showLegendExtra = false; + + expect(getLegendExtra(showLegendExtra, ScaleType.Ordinal, formatter, 'y1', lastValues)).toMatchObject({ + raw: null, + formatted: null, + legendSizingLabel: null, + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/legend/legend.ts b/packages/osd-charts/src/chart_types/xy_chart/legend/legend.ts new file mode 100644 index 000000000000..a09da6d93c18 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/legend/legend.ts @@ -0,0 +1,190 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegendItem } from '../../../common/legend'; +import { SeriesKey, SeriesIdentifier } from '../../../common/series_id'; +import { ScaleType } from '../../../scales/constants'; +import { SortSeriesByConfig, TickFormatterOptions } from '../../../specs'; +import { Color } from '../../../utils/common'; +import { BandedAccessorType } from '../../../utils/geometry'; +import { getLegendCompareFn, SeriesCompareFn } from '../../../utils/series_sort'; +import { getXScaleTypeFromSpec } from '../scales/get_api_scales'; +import { getAxesSpecForSpecId, getSpecsById } from '../state/utils/spec'; +import { LastValues } from '../state/utils/types'; +import { Y0_ACCESSOR_POSTFIX, Y1_ACCESSOR_POSTFIX } from '../tooltip/tooltip'; +import { defaultTickFormatter } from '../utils/axis_utils'; +import { defaultXYLegendSeriesSort } from '../utils/default_series_sort_fn'; +import { groupBy } from '../utils/group_data_series'; +import { + getSeriesIndex, + getSeriesName, + DataSeries, + getSeriesKey, + isDataSeriesBanded, + getSeriesIdentifierFromDataSeries, +} from '../utils/series'; +import { AxisSpec, BasicSeriesSpec, Postfixes, isAreaSeriesSpec, isBarSeriesSpec } from '../utils/specs'; + +/** @internal */ +export interface FormattedLastValues { + y0: number | string | null; + y1: number | string | null; +} + +function getPostfix(spec: BasicSeriesSpec): Postfixes { + if (isAreaSeriesSpec(spec) || isBarSeriesSpec(spec)) { + const { y0AccessorFormat = Y0_ACCESSOR_POSTFIX, y1AccessorFormat = Y1_ACCESSOR_POSTFIX } = spec; + return { + y0AccessorFormat, + y1AccessorFormat, + }; + } + + return {}; +} + +function getBandedLegendItemLabel(name: string, yAccessor: BandedAccessorType, postfixes: Postfixes) { + return yAccessor === BandedAccessorType.Y1 + ? `${name}${postfixes.y1AccessorFormat}` + : `${name}${postfixes.y0AccessorFormat}`; +} + +/** @internal */ +export function getLegendExtra( + showLegendExtra: boolean, + xScaleType: ScaleType, + formatter: (value: any, options?: TickFormatterOptions | undefined) => string, + key: keyof LastValues, + lastValue?: LastValues, +): LegendItem['defaultExtra'] { + if (showLegendExtra) { + const rawValue = (lastValue && lastValue[key]) ?? null; + const formattedValue = rawValue !== null ? formatter(rawValue) : null; + + return { + raw: rawValue !== null ? rawValue : null, + formatted: xScaleType === ScaleType.Ordinal ? null : formattedValue, + legendSizingLabel: formattedValue, + }; + } + return { + raw: null, + formatted: null, + legendSizingLabel: null, + }; +} + +/** @internal */ +export function computeLegend( + dataSeries: DataSeries[], + lastValues: Map, + seriesColors: Map, + specs: BasicSeriesSpec[], + defaultColor: string, + axesSpecs: AxisSpec[], + showLegendExtra: boolean, + serialIdentifierDataSeriesMap: Record, + deselectedDataSeries: SeriesIdentifier[] = [], + sortSeriesBy?: SeriesCompareFn | SortSeriesByConfig, +): LegendItem[] { + const legendItems: LegendItem[] = []; + + dataSeries.forEach((series) => { + const { specId, yAccessor } = series; + const banded = isDataSeriesBanded(series); + const key = getSeriesKey(series, series.groupId); + const spec = getSpecsById(specs, specId); + const dataSeriesKey = getSeriesKey( + { + specId: series.specId, + yAccessor: series.yAccessor, + splitAccessors: series.splitAccessors, + }, + series.groupId, + ); + + const color = seriesColors.get(dataSeriesKey) || defaultColor; + + const hasSingleSeries = dataSeries.length === 1; + const name = getSeriesName(series, hasSingleSeries, false, spec); + const isSeriesHidden = deselectedDataSeries ? getSeriesIndex(deselectedDataSeries, series) >= 0 : false; + if (name === '' || !spec) { + return; + } + + const postFixes = getPostfix(spec); + const labelY1 = banded ? getBandedLegendItemLabel(name, BandedAccessorType.Y1, postFixes) : name; + + // Use this to get axis spec w/ tick formatter + const { yAxis } = getAxesSpecForSpecId(axesSpecs, spec.groupId); + const formatter = spec.tickFormat ?? yAxis?.tickFormat ?? defaultTickFormatter; + const { hideInLegend } = spec; + + const lastValue = lastValues.get(key); + const seriesIdentifier = getSeriesIdentifierFromDataSeries(series); + const xScaleType = getXScaleTypeFromSpec(spec.xScaleType); + legendItems.push({ + color, + label: labelY1, + seriesIdentifiers: [seriesIdentifier], + childId: BandedAccessorType.Y1, + isSeriesHidden, + isItemHidden: hideInLegend, + isToggleable: true, + defaultExtra: getLegendExtra(showLegendExtra, xScaleType, formatter, 'y1', lastValue), + path: [{ index: 0, value: seriesIdentifier.key }], + keys: [specId, spec.groupId, yAccessor, ...series.splitAccessors.values()], + }); + if (banded) { + const labelY0 = getBandedLegendItemLabel(name, BandedAccessorType.Y0, postFixes); + legendItems.push({ + color, + label: labelY0, + seriesIdentifiers: [seriesIdentifier], + childId: BandedAccessorType.Y0, + isSeriesHidden, + isItemHidden: hideInLegend, + isToggleable: true, + defaultExtra: getLegendExtra(showLegendExtra, xScaleType, formatter, 'y0', lastValue), + path: [{ index: 0, value: seriesIdentifier.key }], + keys: [specId, spec.groupId, yAccessor, ...series.splitAccessors.values()], + }); + } + }); + + const legendSortFn = getLegendCompareFn(sortSeriesBy, (a, b) => { + const aDs = serialIdentifierDataSeriesMap[a.key]; + const bDs = serialIdentifierDataSeriesMap[b.key]; + return defaultXYLegendSeriesSort(aDs, bDs); + }); + + return groupBy( + legendItems.sort((a, b) => legendSortFn(a.seriesIdentifiers[0], b.seriesIdentifiers[0])), + ({ keys, childId }) => { + return [...keys, childId].join('__'); // childId is used for band charts + }, + true, + ).map((d) => { + return { + ...d[0], + seriesIdentifiers: d.map(({ seriesIdentifiers: [s] }) => s), + path: d.map(({ path: [p] }) => p), + }; + }); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/_index.scss b/packages/osd-charts/src/chart_types/xy_chart/renderer/_index.scss new file mode 100644 index 000000000000..57d9fadc198a --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/_index.scss @@ -0,0 +1 @@ +@import 'dom/index'; diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/annotations/index.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/annotations/index.ts new file mode 100644 index 000000000000..3eb2fe6dead3 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/annotations/index.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Rotation } from '../../../../../utils/common'; +import { Dimensions } from '../../../../../utils/dimensions'; +import { AnnotationId } from '../../../../../utils/ids'; +import { + mergeWithDefaultAnnotationLine, + mergeWithDefaultAnnotationRect, +} from '../../../../../utils/themes/merge_utils'; +import { AnnotationLineProps } from '../../../annotations/line/types'; +import { AnnotationRectProps } from '../../../annotations/rect/types'; +import { AnnotationDimensions } from '../../../annotations/types'; +import { getSpecsById } from '../../../state/utils/spec'; +import { AnnotationSpec, isLineAnnotation, isRectAnnotation } from '../../../utils/specs'; +import { renderLineAnnotations } from './lines'; +import { renderRectAnnotations } from './rect'; + +interface AnnotationProps { + annotationDimensions: Map; + annotationSpecs: AnnotationSpec[]; + rotation: Rotation; + renderingArea: Dimensions; +} + +/** @internal */ +export function renderAnnotations( + ctx: CanvasRenderingContext2D, + { annotationDimensions, annotationSpecs, rotation, renderingArea }: AnnotationProps, + renderOnBackground: boolean = true, +) { + annotationDimensions.forEach((annotation, id) => { + const spec = getSpecsById(annotationSpecs, id); + if (!spec) { + return null; + } + const isBackground = !spec.zIndex || (spec.zIndex && spec.zIndex <= 0); + if ((isBackground && renderOnBackground) || (!isBackground && !renderOnBackground)) { + if (isLineAnnotation(spec)) { + const lineStyle = mergeWithDefaultAnnotationLine(spec.style); + renderLineAnnotations(ctx, annotation as AnnotationLineProps[], lineStyle, rotation, renderingArea); + } else if (isRectAnnotation(spec)) { + const rectStyle = mergeWithDefaultAnnotationRect(spec.style); + renderRectAnnotations(ctx, annotation as AnnotationRectProps[], rectStyle, rotation, renderingArea); + } + } + }); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts new file mode 100644 index 000000000000..7e0ae37ae86a --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { stringToRGB } from '../../../../../common/color_library_wrappers'; +import { Stroke } from '../../../../../geoms/types'; +import { Rotation } from '../../../../../utils/common'; +import { Dimensions } from '../../../../../utils/dimensions'; +import { LineAnnotationStyle } from '../../../../../utils/themes/theme'; +import { AnnotationLineProps } from '../../../annotations/line/types'; +import { renderLine } from '../primitives/line'; +import { withPanelTransform } from '../utils/panel_transform'; + +/** @internal */ +export function renderLineAnnotations( + ctx: CanvasRenderingContext2D, + annotations: AnnotationLineProps[], + lineStyle: LineAnnotationStyle, + rotation: Rotation, + renderingArea: Dimensions, +) { + const strokeColor = stringToRGB(lineStyle.line.stroke); + strokeColor.opacity *= lineStyle.line.opacity; + const stroke: Stroke = { + color: strokeColor, + width: lineStyle.line.strokeWidth, + dash: lineStyle.line.dash, + }; + + annotations.forEach(({ linePathPoints, panel }) => { + withPanelTransform(ctx, panel, rotation, renderingArea, (ctx) => { + renderLine(ctx, linePathPoints, stroke); + }); + }); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts new file mode 100644 index 000000000000..36c9f0a0fc33 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { stringToRGB } from '../../../../../common/color_library_wrappers'; +import { Fill, Stroke } from '../../../../../geoms/types'; +import { Rotation } from '../../../../../utils/common'; +import { Dimensions } from '../../../../../utils/dimensions'; +import { RectAnnotationStyle } from '../../../../../utils/themes/theme'; +import { AnnotationRectProps } from '../../../annotations/rect/types'; +import { renderRect } from '../primitives/rect'; +import { withPanelTransform } from '../utils/panel_transform'; + +/** @internal */ +export function renderRectAnnotations( + ctx: CanvasRenderingContext2D, + annotations: AnnotationRectProps[], + rectStyle: RectAnnotationStyle, + rotation: Rotation, + renderingArea: Dimensions, +) { + const fillColor = stringToRGB(rectStyle.fill); + fillColor.opacity *= rectStyle.opacity; + const fill: Fill = { + color: fillColor, + }; + const strokeColor = stringToRGB(rectStyle.stroke); + strokeColor.opacity *= rectStyle.opacity; + const stroke: Stroke = { + color: strokeColor, + width: rectStyle.strokeWidth, + }; + + const rectsLength = annotations.length; + + for (let i = 0; i < rectsLength; i++) { + const { rect, panel } = annotations[i]; + withPanelTransform(ctx, panel, rotation, renderingArea, (ctx) => { + renderRect(ctx, rect, fill, stroke); + }); + } +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/areas.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/areas.ts new file mode 100644 index 000000000000..ede739518903 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/areas.ts @@ -0,0 +1,124 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegendItem } from '../../../../common/legend'; +import { Rect } from '../../../../geoms/types'; +import { withContext } from '../../../../renderers/canvas'; +import { Rotation } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { AreaGeometry, PerPanel } from '../../../../utils/geometry'; +import { SharedGeometryStateStyle } from '../../../../utils/themes/theme'; +import { getGeometryStateStyle } from '../../rendering/utils'; +import { renderPoints } from './points'; +import { renderLinePaths, renderAreaPath } from './primitives/path'; +import { buildAreaStyles } from './styles/area'; +import { buildLineStyles } from './styles/line'; +import { withPanelTransform } from './utils/panel_transform'; + +interface AreaGeometriesProps { + areas: Array>; + sharedStyle: SharedGeometryStateStyle; + rotation: Rotation; + renderingArea: Dimensions; + highlightedLegendItem?: LegendItem; + clippings: Rect; +} + +/** @internal */ +export function renderAreas(ctx: CanvasRenderingContext2D, imgCanvas: HTMLCanvasElement, props: AreaGeometriesProps) { + const { sharedStyle, highlightedLegendItem, areas, rotation, clippings, renderingArea } = props; + + withContext(ctx, (ctx) => { + areas.forEach(({ panel, value: area }) => { + const { seriesAreaLineStyle, seriesAreaStyle } = area; + if (seriesAreaStyle.visible) { + withPanelTransform( + ctx, + panel, + rotation, + renderingArea, + (ctx) => { + renderArea(ctx, imgCanvas, area, sharedStyle, clippings, highlightedLegendItem); + }, + { area: clippings, shouldClip: true }, + ); + } + if (seriesAreaLineStyle.visible) { + withPanelTransform( + ctx, + panel, + rotation, + renderingArea, + (ctx) => { + renderAreaLines(ctx, area, sharedStyle, clippings, highlightedLegendItem); + }, + { area: clippings, shouldClip: true }, + ); + } + }); + + areas.forEach(({ panel, value: area }) => { + const { seriesPointStyle, seriesIdentifier, points } = area; + const visiblePoints = seriesPointStyle.visible ? points : points.filter(({ orphan }) => orphan); + if (visiblePoints.length === 0) { + return; + } + const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem); + withPanelTransform( + ctx, + panel, + rotation, + renderingArea, + (ctx) => { + renderPoints(ctx, visiblePoints, geometryStateStyle); + }, + { area: clippings, shouldClip: points[0]?.value.mark !== null }, + ); + }); + }); +} + +function renderArea( + ctx: CanvasRenderingContext2D, + imgCanvas: HTMLCanvasElement, + glyph: AreaGeometry, + sharedStyle: SharedGeometryStateStyle, + clippings: Rect, + highlightedLegendItem?: LegendItem, +) { + const { area, color, transform, seriesIdentifier, seriesAreaStyle, clippedRanges, hideClippedRanges } = glyph; + const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem); + const styles = buildAreaStyles(ctx, imgCanvas, color, seriesAreaStyle, geometryStateStyle); + + renderAreaPath(ctx, transform, area, styles, clippedRanges, clippings, hideClippedRanges); +} + +function renderAreaLines( + ctx: CanvasRenderingContext2D, + glyph: AreaGeometry, + sharedStyle: SharedGeometryStateStyle, + clippings: Rect, + highlightedLegendItem?: LegendItem, +) { + const { lines, color, seriesIdentifier, transform, seriesAreaLineStyle, clippedRanges, hideClippedRanges } = glyph; + const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem); + const styles = buildLineStyles(color, seriesAreaLineStyle, geometryStateStyle); + + renderLinePaths(ctx, transform, lines, styles, clippedRanges, clippings, hideClippedRanges); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/global_title.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/global_title.ts new file mode 100644 index 000000000000..1415a4b92851 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/global_title.ts @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AxisProps } from '.'; +import { Position } from '../../../../../utils/common'; +import { getSimplePadding } from '../../../../../utils/dimensions'; +import { Point } from '../../../../../utils/point'; +import { isHorizontalAxis } from '../../../utils/axis_type_utils'; +import { getTitleDimension, shouldShowTicks } from '../../../utils/axis_utils'; +import { renderText } from '../primitives/text'; +import { renderDebugRect } from '../utils/debug'; +import { getFontStyle } from './panel_title'; + +type TitleProps = Pick & { + anchorPoint: Point; +}; + +/** @internal */ +export function renderTitle(ctx: CanvasRenderingContext2D, props: TitleProps) { + const { + axisSpec: { position, title }, + axisStyle: { axisTitle }, + } = props; + if (!title || !axisTitle.visible) { + return null; + } + if (isHorizontalAxis(position)) { + return renderHorizontalTitle(ctx, props); + } + return renderVerticalTitle(ctx, props); +} + +function renderVerticalTitle(ctx: CanvasRenderingContext2D, props: TitleProps) { + const { + size: { height }, + axisSpec: { position, hide: hideAxis, title }, + dimension: { maxLabelBboxWidth }, + axisStyle: { axisTitle, axisPanelTitle, tickLine, tickLabel }, + anchorPoint, + debug, + panelTitle, + } = props; + + if (!title) { + return null; + } + + const font = getFontStyle(axisTitle); + const titlePadding = getSimplePadding(axisTitle.visible && title ? axisTitle.padding : 0); + const panelTitleDimension = panelTitle ? getTitleDimension(axisPanelTitle) : 0; + const tickDimension = shouldShowTicks(tickLine, hideAxis) ? tickLine.size + tickLine.padding : 0; + const labelPadding = getSimplePadding(tickLabel.padding); + const labelWidth = tickLabel.visible ? labelPadding.outer + maxLabelBboxWidth + labelPadding.inner : 0; + + const top = height + anchorPoint.y; + const left = + (position === Position.Left + ? titlePadding.outer + : tickDimension + labelWidth + titlePadding.inner + panelTitleDimension) + anchorPoint.x; + + if (debug) { + renderDebugRect(ctx, { x: left, y: top, width: height, height: font.fontSize }, undefined, undefined, -90); + } + + renderText( + ctx, + { + x: left + font.fontSize / 2, + y: top - height / 2, + }, + title, + font, + -90, + ); +} + +function renderHorizontalTitle(ctx: CanvasRenderingContext2D, props: TitleProps) { + const { + size: { width }, + axisSpec: { position, hide: hideAxis, title }, + dimension: { maxLabelBboxHeight }, + axisStyle: { axisTitle, axisPanelTitle, tickLine, tickLabel }, + anchorPoint, + debug, + panelTitle, + } = props; + + if (!title) { + return; + } + + const font = getFontStyle(axisTitle); + const titlePadding = getSimplePadding(axisTitle.visible && title ? axisTitle.padding : 0); + const panelTitleDimension = panelTitle ? getTitleDimension(axisPanelTitle) : 0; + const tickDimension = shouldShowTicks(tickLine, hideAxis) ? tickLine.size + tickLine.padding : 0; + const labelPadding = getSimplePadding(tickLabel.padding); + const labelHeight = tickLabel.visible ? maxLabelBboxHeight + labelPadding.outer + labelPadding.inner : 0; + + const top = + (position === Position.Top + ? titlePadding.outer + : labelHeight + tickDimension + titlePadding.inner + panelTitleDimension) + anchorPoint.y; + const left = anchorPoint.x; + + if (debug) { + renderDebugRect(ctx, { x: left, y: top, width, height: font.fontSize }); + } + + renderText( + ctx, + { + x: left + width / 2, + y: top + font.fontSize / 2, + }, + title, + font, + ); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/index.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/index.ts new file mode 100644 index 000000000000..237dcb9f4dcf --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/index.ts @@ -0,0 +1,176 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { withContext } from '../../../../../renderers/canvas'; +import { Position } from '../../../../../utils/common'; +import { Dimensions, Size } from '../../../../../utils/dimensions'; +import { AxisId } from '../../../../../utils/ids'; +import { Point } from '../../../../../utils/point'; +import { AxisStyle } from '../../../../../utils/themes/theme'; +import { PerPanelAxisGeoms } from '../../../state/selectors/compute_per_panel_axes_geoms'; +import { getSpecsById } from '../../../state/utils/spec'; +import { isVerticalAxis } from '../../../utils/axis_type_utils'; +import { AxisTick, AxisTicksDimensions, shouldShowTicks } from '../../../utils/axis_utils'; +import { AxisSpec } from '../../../utils/specs'; +import { renderDebugRect } from '../utils/debug'; +import { renderTitle } from './global_title'; +import { renderLine } from './line'; +import { renderPanelTitle } from './panel_title'; +import { renderTick } from './tick'; +import { renderTickLabel } from './tick_label'; + +/** @internal */ +export interface AxisProps { + panelTitle?: string; + secondary?: boolean; + panelAnchor: Point; + axisStyle: AxisStyle; + axisSpec: AxisSpec; + size: Size; + anchorPoint: Point; + dimension: AxisTicksDimensions; + ticks: AxisTick[]; + debug: boolean; + renderingArea: Dimensions; +} + +/** @internal */ +export interface AxesProps { + axesSpecs: AxisSpec[]; + perPanelAxisGeoms: PerPanelAxisGeoms[]; + axesStyles: Map; + sharedAxesStyle: AxisStyle; + debug: boolean; + renderingArea: Dimensions; +} + +/** @internal */ +export function renderAxes(ctx: CanvasRenderingContext2D, props: AxesProps) { + const { axesSpecs, perPanelAxisGeoms, axesStyles, sharedAxesStyle, debug, renderingArea } = props; + const seenAxesTitleIds = new Set(); + + perPanelAxisGeoms.forEach(({ axesGeoms, panelAnchor }) => { + withContext(ctx, (ctx) => { + axesGeoms.forEach((geometry) => { + const { + axis: { panelTitle, id, position, secondary }, + anchorPoint, + size, + dimension, + visibleTicks: ticks, + parentSize, + } = geometry; + const axisSpec = getSpecsById(axesSpecs, id); + + if (!axisSpec || !dimension || !position || axisSpec.hide) { + return; + } + + const axisStyle = axesStyles.get(axisSpec.id) ?? sharedAxesStyle; + + if (!seenAxesTitleIds.has(id)) { + seenAxesTitleIds.add(id); + + renderTitle(ctx, { + ...props, + panelTitle, + size: parentSize, + anchorPoint, + dimension, + axisStyle, + axisSpec, + }); + } + + renderAxis(ctx, { + panelTitle, + secondary, + panelAnchor, + axisSpec, + anchorPoint, + size, + dimension, + ticks, + axisStyle, + debug, + renderingArea, + }); + }); + }); + }); +} + +function renderAxis(ctx: CanvasRenderingContext2D, props: AxisProps) { + withContext(ctx, (ctx) => { + const { ticks, size, anchorPoint, debug, axisStyle, axisSpec, panelAnchor, secondary } = props; + const showTicks = shouldShowTicks(axisStyle.tickLine, axisSpec.hide); + const { position } = axisSpec; + const isVertical = isVerticalAxis(position); + const y = isVertical + ? anchorPoint.y + panelAnchor.y + : anchorPoint.y + (position === Position.Top ? 1 : -1) * panelAnchor.y; + const x = isVertical + ? anchorPoint.x + (position === Position.Right ? -1 : 1) * panelAnchor.x + : anchorPoint.x + panelAnchor.x; + const translate = { + y, + x, + }; + + ctx.translate(translate.x, translate.y); + + if (debug && !secondary) { + renderDebugRect(ctx, { + x: 0, + y: 0, + ...size, + }); + } + + withContext(ctx, (ctx) => { + renderLine(ctx, props); + }); + + // TODO: compute axis dimensions per panels + // For now just rendering axis line + if (secondary) return; + + if (showTicks) { + withContext(ctx, (ctx) => { + ticks.forEach((tick) => { + renderTick(ctx, tick, props); + }); + }); + } + + if (axisStyle.tickLabel.visible) { + withContext(ctx, (ctx) => { + ticks + .filter((tick) => tick.label !== null) + .forEach((tick) => { + renderTickLabel(ctx, tick, showTicks, props); + }); + }); + } + + withContext(ctx, (ctx) => { + renderPanelTitle(ctx, props); + }); + }); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/line.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/line.ts new file mode 100644 index 000000000000..6429d879f1c9 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/line.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AxisProps } from '.'; +import { Position } from '../../../../../utils/common'; +import { isVerticalAxis } from '../../../utils/axis_type_utils'; + +/** @internal */ +export function renderLine( + ctx: CanvasRenderingContext2D, + { axisSpec: { position }, size, axisStyle: { axisLine } }: AxisProps, +) { + if (!axisLine.visible) { + return; + } + + const lineProps: number[] = []; + if (isVerticalAxis(position)) { + lineProps[0] = position === Position.Left ? size.width : 0; + lineProps[2] = position === Position.Left ? size.width : 0; + lineProps[1] = 0; + lineProps[3] = size.height; + } else { + lineProps[0] = 0; + lineProps[2] = size.width; + lineProps[1] = position === Position.Top ? size.height : 0; + lineProps[3] = position === Position.Top ? size.height : 0; + } + ctx.beginPath(); + ctx.moveTo(lineProps[0], lineProps[1]); + ctx.lineTo(lineProps[2], lineProps[3]); + ctx.strokeStyle = axisLine.stroke; + ctx.lineWidth = axisLine.strokeWidth; + ctx.stroke(); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/panel_title.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/panel_title.ts new file mode 100644 index 000000000000..13a1922f9305 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/panel_title.ts @@ -0,0 +1,146 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AxisProps } from '.'; +import { FontStyle } from '../../../../../common/text_utils'; +import { Position } from '../../../../../utils/common'; +import { getSimplePadding } from '../../../../../utils/dimensions'; +import { AxisStyle } from '../../../../../utils/themes/theme'; +import { isHorizontalAxis } from '../../../utils/axis_type_utils'; +import { getTitleDimension, shouldShowTicks } from '../../../utils/axis_utils'; +import { renderText, TextFont } from '../primitives/text'; +import { renderDebugRect } from '../utils/debug'; + +type PanelTitleProps = Pick; + +/** @internal */ +export function renderPanelTitle(ctx: CanvasRenderingContext2D, props: PanelTitleProps) { + const { + axisSpec: { position }, + axisStyle: { axisPanelTitle }, + panelTitle, + } = props; + if (!panelTitle || !axisPanelTitle.visible) { + return null; + } + return isHorizontalAxis(position) ? renderHorizontalTitle(ctx, props) : renderVerticalTitle(ctx, props); +} + +function renderVerticalTitle(ctx: CanvasRenderingContext2D, props: PanelTitleProps) { + const { + size: { height }, + axisSpec: { position, hide: hideAxis, title }, + dimension: { maxLabelBboxWidth }, + axisStyle: { axisTitle, axisPanelTitle, tickLine, tickLabel }, + debug, + panelTitle, + } = props; + if (!panelTitle) { + return null; + } + const font = getFontStyle(axisPanelTitle); + const tickDimension = shouldShowTicks(tickLine, hideAxis) ? tickLine.size + tickLine.padding : 0; + const panelTitlePadding = getSimplePadding(axisPanelTitle.visible && panelTitle ? axisPanelTitle.padding : 0); + const titleDimension = title ? getTitleDimension(axisTitle) : 0; + const labelPadding = getSimplePadding(tickLabel.padding); + const labelWidth = tickLabel.visible ? labelPadding.outer + maxLabelBboxWidth + labelPadding.inner : 0; + const top = height; + const left = + position === Position.Left + ? titleDimension + panelTitlePadding.outer + : tickDimension + labelWidth + panelTitlePadding.inner; + + if (debug) { + renderDebugRect(ctx, { x: left, y: top, width: height, height: font.fontSize }, undefined, undefined, -90); + } + + renderText( + ctx, + { + x: left + font.fontSize / 2, + y: top - height / 2, + }, + panelTitle, + font, + -90, + ); +} + +function renderHorizontalTitle(ctx: CanvasRenderingContext2D, props: PanelTitleProps) { + const { + size: { width }, + axisSpec: { position, hide: hideAxis, title }, + dimension: { maxLabelBboxHeight }, + axisStyle: { axisTitle, axisPanelTitle, tickLine, tickLabel }, + debug, + panelTitle, + } = props; + + if (!panelTitle) { + return; + } + + const font = getFontStyle(axisPanelTitle); + const tickDimension = shouldShowTicks(tickLine, hideAxis) ? tickLine.size + tickLine.padding : 0; + const panelTitlePadding = getSimplePadding(axisPanelTitle.visible && panelTitle ? axisPanelTitle.padding : 0); + const titleDimension = title ? getTitleDimension(axisTitle) : 0; + const labelPadding = getSimplePadding(tickLabel.padding); + const labelHeight = tickLabel.visible ? maxLabelBboxHeight + labelPadding.outer + labelPadding.inner : 0; + + const top = + position === Position.Top + ? titleDimension + panelTitlePadding.outer + : labelHeight + tickDimension + panelTitlePadding.inner; + const left = 0; + + if (debug) { + renderDebugRect(ctx, { x: left, y: top, width, height: font.fontSize }); + } + + renderText( + ctx, + { + x: left + width / 2, + y: top + font.fontSize / 2, + }, + panelTitle, + font, + ); +} + +/** @internal */ +export function getFontStyle({ + fontFamily, + fontStyle, + fill, + fontSize, +}: AxisStyle['axisTitle'] | AxisStyle['axisPanelTitle']): TextFont { + return { + fontFamily, + fontVariant: 'normal', + fontStyle: fontStyle ? (fontStyle as FontStyle) : 'normal', + fontWeight: 'normal', + textColor: fill, + textOpacity: 1, + fill, + align: 'center', + baseline: 'middle', + fontSize, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts new file mode 100644 index 000000000000..812c7851c1fc --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AxisProps } from '.'; +import { stringToRGB } from '../../../../../common/color_library_wrappers'; +import { Position } from '../../../../../utils/common'; +import { TickStyle } from '../../../../../utils/themes/theme'; +import { isVerticalAxis } from '../../../utils/axis_type_utils'; +import { AxisTick } from '../../../utils/axis_utils'; +import { renderLine } from '../primitives/line'; + +/** @internal */ +export function renderTick(ctx: CanvasRenderingContext2D, tick: AxisTick, props: AxisProps) { + const { + axisSpec: { position }, + size, + axisStyle: { tickLine }, + } = props; + if (isVerticalAxis(position)) { + renderVerticalTick(ctx, position, size.width, tickLine.size, tick.position, tickLine); + } else { + renderHorizontalTick(ctx, position, size.height, tickLine.size, tick.position, tickLine); + } +} + +function renderVerticalTick( + ctx: CanvasRenderingContext2D, + position: Position, + axisWidth: number, + tickSize: number, + tickPosition: number, + tickStyle: TickStyle, +) { + const isLeftAxis = position === Position.Left; + const x1 = isLeftAxis ? axisWidth : 0; + const x2 = isLeftAxis ? axisWidth - tickSize : tickSize; + renderLine( + ctx, + { x1, y1: tickPosition, x2, y2: tickPosition }, + { + color: stringToRGB(tickStyle.stroke), + width: tickStyle.strokeWidth, + }, + ); +} + +function renderHorizontalTick( + ctx: CanvasRenderingContext2D, + position: Position, + axisHeight: number, + tickSize: number, + tickPosition: number, + tickStyle: TickStyle, +) { + const isTopAxis = position === Position.Top; + const y1 = isTopAxis ? axisHeight - tickSize : 0; + const y2 = isTopAxis ? axisHeight : tickSize; + + renderLine( + ctx, + { x1: tickPosition, y1, x2: tickPosition, y2 }, + { + color: stringToRGB(tickStyle.stroke), + width: tickStyle.strokeWidth, + }, + ); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts new file mode 100644 index 000000000000..67e8f922c70a --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts @@ -0,0 +1,120 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AxisProps } from '.'; +import { Font, FontStyle } from '../../../../../common/text_utils'; +import { withContext } from '../../../../../renderers/canvas'; +import { AxisTick, getTickLabelProps } from '../../../utils/axis_utils'; +import { renderText } from '../primitives/text'; +import { renderDebugRectCenterRotated } from '../utils/debug'; + +/** @internal */ +export function renderTickLabel(ctx: CanvasRenderingContext2D, tick: AxisTick, showTicks: boolean, props: AxisProps) { + const { + axisSpec: { position, labelFormat }, + dimension: axisTicksDimensions, + size, + debug, + axisStyle, + } = props; + const labelStyle = axisStyle.tickLabel; + const { rotation: tickLabelRotation, alignment, offset } = labelStyle; + + const { maxLabelBboxWidth, maxLabelBboxHeight, maxLabelTextWidth, maxLabelTextHeight } = axisTicksDimensions; + const { x, y, offsetX, offsetY, textOffsetX, textOffsetY, horizontalAlign, verticalAlign } = getTickLabelProps( + axisStyle, + tick.position, + position, + tickLabelRotation, + size, + axisTicksDimensions, + showTicks, + offset, + alignment, + ); + + if (debug) { + // full text container + renderDebugRectCenterRotated( + ctx, + { + x: x + offsetX, + y: y + offsetY, + }, + { + x: x + offsetX, + y: y + offsetY, + height: maxLabelTextHeight, + width: maxLabelTextWidth, + }, + undefined, + undefined, + tickLabelRotation, + ); + // rotated text container + if (![0, -90, 90, 180].includes(tickLabelRotation)) { + renderDebugRectCenterRotated( + ctx, + { + x: x + offsetX, + y: y + offsetY, + }, + { + x: x + offsetX, + y: y + offsetY, + height: maxLabelBboxHeight, + width: maxLabelBboxWidth, + }, + undefined, + undefined, + 0, + ); + } + } + const font: Font = { + fontFamily: labelStyle.fontFamily, + fontStyle: labelStyle.fontStyle ? (labelStyle.fontStyle as FontStyle) : 'normal', + fontVariant: 'normal', + fontWeight: 'normal', + textColor: labelStyle.fill, + textOpacity: 1, + }; + withContext(ctx, (ctx) => { + renderText( + ctx, + { + x: x + offsetX, + y: y + offsetY, + }, + labelFormat ? labelFormat(tick.value) : tick.label, + { + ...font, + fontSize: labelStyle.fontSize, + fill: labelStyle.fill, + align: horizontalAlign as CanvasTextAlign, + baseline: verticalAlign as CanvasTextBaseline, + }, + tickLabelRotation, + { + x: textOffsetX, + y: textOffsetY, + }, + ); + }); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/bars.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/bars.ts new file mode 100644 index 000000000000..1d32fe7488f5 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/bars.ts @@ -0,0 +1,96 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegendItem } from '../../../../common/legend'; +import { Rect } from '../../../../geoms/types'; +import { withContext } from '../../../../renderers/canvas'; +import { Rotation } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { BarGeometry, PerPanel } from '../../../../utils/geometry'; +import { SharedGeometryStateStyle } from '../../../../utils/themes/theme'; +import { getGeometryStateStyle } from '../../rendering/utils'; +import { renderRect } from './primitives/rect'; +import { buildBarStyles } from './styles/bar'; +import { withPanelTransform } from './utils/panel_transform'; + +/** @internal */ +export function renderBars( + ctx: CanvasRenderingContext2D, + imgCanvas: HTMLCanvasElement, + barGeometries: Array>, + sharedStyle: SharedGeometryStateStyle, + clippings: Rect, + renderingArea: Dimensions, + highlightedLegendItem?: LegendItem, + rotation?: Rotation, +) { + withContext(ctx, (ctx) => { + const barRenderer = renderPerPanelBars( + ctx, + imgCanvas, + clippings, + sharedStyle, + renderingArea, + highlightedLegendItem, + rotation, + ); + barGeometries.forEach(barRenderer); + }); +} + +function renderPerPanelBars( + ctx: CanvasRenderingContext2D, + imgCanvas: HTMLCanvasElement, + clippings: Rect, + sharedStyle: SharedGeometryStateStyle, + renderingArea: Dimensions, + highlightedLegendItem?: LegendItem, + rotation: Rotation = 0, +) { + return ({ panel, value: bars }: PerPanel) => { + if (bars.length === 0) { + return; + } + withPanelTransform( + ctx, + panel, + rotation, + renderingArea, + (ctx) => { + bars.forEach((barGeometry) => { + const { x, y, width, height, color, seriesStyle, seriesIdentifier } = barGeometry; + const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem); + const { fill, stroke } = buildBarStyles( + ctx, + imgCanvas, + color, + seriesStyle.rect, + seriesStyle.rectBorder, + geometryStateStyle, + ); + const rect = { x, y, width, height }; + withContext(ctx, (ctx) => { + renderRect(ctx, rect, fill, stroke); + }); + }); + }, + { area: clippings, shouldClip: true }, + ); + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/bubbles.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/bubbles.ts new file mode 100644 index 000000000000..838a63bf923e --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/bubbles.ts @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegendItem } from '../../../../common/legend'; +import { SeriesKey } from '../../../../common/series_id'; +import { Rect } from '../../../../geoms/types'; +import { withContext } from '../../../../renderers/canvas'; +import { Rotation } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { BubbleGeometry, PerPanel, PointGeometry } from '../../../../utils/geometry'; +import { SharedGeometryStateStyle, GeometryStateStyle, PointStyle } from '../../../../utils/themes/theme'; +import { getGeometryStateStyle } from '../../rendering/utils'; +import { renderPointGroup } from './points'; + +interface BubbleGeometriesDataProps { + animated?: boolean; + bubbles: Array>; + sharedStyle: SharedGeometryStateStyle; + highlightedLegendItem?: LegendItem; + clippings: Rect; + rotation: Rotation; + renderingArea: Dimensions; +} + +/** @internal */ +export function renderBubbles(ctx: CanvasRenderingContext2D, props: BubbleGeometriesDataProps) { + withContext(ctx, (ctx) => { + const { bubbles, sharedStyle, highlightedLegendItem, clippings, rotation, renderingArea } = props; + const geometryStyles: Record = {}; + const pointStyles: Record = {}; + + const allPoints = bubbles.reduce( + (acc, { value: { seriesIdentifier, seriesPointStyle, points } }) => { + geometryStyles[seriesIdentifier.key] = getGeometryStateStyle( + seriesIdentifier, + sharedStyle, + highlightedLegendItem, + ); + pointStyles[seriesIdentifier.key] = seriesPointStyle; + + acc.push(...points); + return acc; + }, + [], + ); + if (allPoints.length === 0) { + return; + } + + renderPointGroup( + ctx, + allPoints, + pointStyles, + geometryStyles, + rotation, + renderingArea, + clippings, + // TODO: add padding over clipping + allPoints[0]?.value.mark !== null, + ); + }); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/grids.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/grids.ts new file mode 100644 index 000000000000..bb911fb53016 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/grids.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { withContext } from '../../../../renderers/canvas'; +import { Dimensions } from '../../../../utils/dimensions'; +import { AxisStyle } from '../../../../utils/themes/theme'; +import { LinesGrid } from '../../utils/grid_lines'; +import { AxisSpec } from '../../utils/specs'; +import { renderMultiLine } from './primitives/line'; + +interface GridProps { + sharedAxesStyle: AxisStyle; + perPanelGridLines: Array; + axesSpecs: AxisSpec[]; + renderingArea: Dimensions; + axesStyles: Map; +} + +/** @internal */ +export function renderGrids(ctx: CanvasRenderingContext2D, props: GridProps) { + const { + perPanelGridLines, + renderingArea: { left, top }, + } = props; + withContext(ctx, (ctx) => { + ctx.translate(left, top); + + perPanelGridLines.forEach(({ lineGroups, panelAnchor: { x, y } }) => { + withContext(ctx, (ctx) => { + ctx.translate(x, y); + lineGroups.forEach(({ lines, stroke }) => { + renderMultiLine(ctx, lines, stroke); + }); + }); + }); + }); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/lines.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/lines.ts new file mode 100644 index 000000000000..b72bd42d438f --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/lines.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegendItem } from '../../../../common/legend'; +import { Rect } from '../../../../geoms/types'; +import { withContext } from '../../../../renderers/canvas'; +import { Rotation } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { LineGeometry, PerPanel } from '../../../../utils/geometry'; +import { SharedGeometryStateStyle } from '../../../../utils/themes/theme'; +import { getGeometryStateStyle } from '../../rendering/utils'; +import { renderPoints } from './points'; +import { renderLinePaths } from './primitives/path'; +import { buildLineStyles } from './styles/line'; +import { withPanelTransform } from './utils/panel_transform'; + +interface LineGeometriesDataProps { + animated?: boolean; + lines: Array>; + renderingArea: Dimensions; + rotation: Rotation; + sharedStyle: SharedGeometryStateStyle; + highlightedLegendItem?: LegendItem; + clippings: Rect; +} + +/** @internal */ +export function renderLines(ctx: CanvasRenderingContext2D, props: LineGeometriesDataProps) { + withContext(ctx, (ctx) => { + const { lines, sharedStyle, highlightedLegendItem, clippings, renderingArea, rotation } = props; + + lines.forEach(({ panel, value: line }) => { + const { seriesLineStyle, seriesPointStyle, points } = line; + + if (seriesLineStyle.visible) { + withPanelTransform( + ctx, + panel, + rotation, + renderingArea, + (ctx) => { + renderLine(ctx, line, sharedStyle, clippings, highlightedLegendItem); + }, + { area: clippings, shouldClip: true }, + ); + } + + const visiblePoints = seriesPointStyle.visible ? points : points.filter(({ orphan }) => orphan); + if (visiblePoints.length === 0) { + return; + } + const geometryStyle = getGeometryStateStyle(line.seriesIdentifier, sharedStyle, highlightedLegendItem); + withPanelTransform( + ctx, + panel, + rotation, + renderingArea, + (ctx) => { + renderPoints(ctx, visiblePoints, geometryStyle); + }, + // TODO: add padding over clipping + { area: clippings, shouldClip: line.points[0]?.value.mark !== null }, + ); + }); + }); +} + +function renderLine( + ctx: CanvasRenderingContext2D, + line: LineGeometry, + sharedStyle: SharedGeometryStateStyle, + clippings: Rect, + highlightedLegendItem?: LegendItem, +) { + const { color, transform, seriesIdentifier, seriesLineStyle, clippedRanges, hideClippedRanges } = line; + const geometryStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem); + const stroke = buildLineStyles(color, seriesLineStyle, geometryStyle); + renderLinePaths(ctx, transform, [line.line], stroke, clippedRanges, clippings, hideClippedRanges); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/panels/panels.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/panels/panels.ts new file mode 100644 index 000000000000..94405918f24b --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/panels/panels.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { stringToRGB } from '../../../../../common/color_library_wrappers'; +import { withContext } from '../../../../../renderers/canvas'; +import { Point } from '../../../../../utils/point'; +import { PanelGeoms } from '../../../state/selectors/compute_panels'; +import { renderRect } from '../primitives/rect'; + +/** @internal */ +export function renderGridPanels(ctx: CanvasRenderingContext2D, chartAnchor: Point, panels: PanelGeoms) { + withContext(ctx, (ctx) => { + ctx.translate(chartAnchor.x, chartAnchor.y); + panels.forEach((panel) => { + withContext(ctx, (ctx) => { + ctx.translate(panel.panelAnchor.x, panel.panelAnchor.y); + withContext(ctx, (ctx) => { + renderRect( + ctx, + { x: 0, y: 0, ...panel }, + { color: stringToRGB('#00000000') }, + { color: stringToRGB('#000000'), width: 1 }, + ); + }); + }); + }); + }); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/points.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/points.ts new file mode 100644 index 000000000000..42887638f5d2 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/points.ts @@ -0,0 +1,116 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RgbObject } from '../../../../common/color_library_wrappers'; +import { SeriesKey } from '../../../../common/series_id'; +import { Circle, Stroke, Fill, Rect } from '../../../../geoms/types'; +import { Rotation } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { PointGeometry } from '../../../../utils/geometry'; +import { PointStyle, GeometryStateStyle, PointShape } from '../../../../utils/themes/theme'; +import { renderShape } from './primitives/shapes'; +import { withPanelTransform } from './utils/panel_transform'; + +/** + * Renders points from single series + * + * @internal + */ +export function renderPoints(ctx: CanvasRenderingContext2D, points: PointGeometry[], { opacity }: GeometryStateStyle) { + points + .map<[Circle, Fill, Stroke, PointShape]>(({ x, y, radius, transform, style }) => { + const fill: Fill = { + color: applyOpacity(style.fill.color, opacity), + }; + + const stroke: Stroke = { + ...style.stroke, + color: applyOpacity(style.stroke.color, opacity), + }; + + const coordinates: Circle = { + x: x + transform.x, + y: y + transform.y, + radius, + }; + + return [coordinates, fill, stroke, style.shape]; + }) + .sort(([{ radius: a }], [{ radius: b }]) => b - a) + .forEach(([coordinates, fill, stroke, shape]) => renderShape(ctx, shape, coordinates, fill, stroke)); +} + +/** + * Renders points in group from multiple series on a single layer + * + * @internal + */ +export function renderPointGroup( + ctx: CanvasRenderingContext2D, + points: PointGeometry[], + themeStyles: Record, + geometryStateStyles: Record, + rotation: Rotation, + renderingArea: Dimensions, + clippings: Rect, + shouldClip: boolean, +) { + points + .map<[Circle, Fill, Stroke, Dimensions, PointShape]>( + ({ x, y, radius, transform, style, seriesIdentifier: { key }, panel }) => { + const { opacity } = geometryStateStyles[key]; + const fill: Fill = { + color: applyOpacity(style.fill.color, opacity), + }; + + const stroke: Stroke = { + ...style.stroke, + color: applyOpacity(style.stroke.color, opacity), + }; + + const coordinates: Circle = { + x: x + transform.x, + y, + radius, + }; + + return [coordinates, fill, stroke, panel, style.shape]; + }, + ) + .sort(([{ radius: a }], [{ radius: b }]) => b - a) + .forEach(([coordinates, fill, stroke, panel, shape]) => { + withPanelTransform( + ctx, + panel, + rotation, + renderingArea, + (ctx) => { + renderShape(ctx, shape, coordinates, fill, stroke); + }, + { area: clippings, shouldClip }, + ); + }); +} + +function applyOpacity(color: RgbObject, opacity: number): RgbObject { + return { + ...color, + opacity: color.opacity * opacity, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/arc.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/arc.ts new file mode 100644 index 000000000000..7e46510637f0 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/arc.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Circle, Stroke, Fill, Arc } from '../../../../../geoms/types'; +import { withContext } from '../../../../../renderers/canvas'; +import { fillAndStroke } from './utils'; + +/** @internal */ +export function renderCircle(ctx: CanvasRenderingContext2D, circle: Circle, fill?: Fill, stroke?: Stroke) { + if (!fill && !stroke) { + return; + } + renderArc( + ctx, + { + ...circle, + startAngle: 0, + endAngle: Math.PI * 2, + }, + fill, + stroke, + ); +} + +/** @internal */ +export function renderArc(ctx: CanvasRenderingContext2D, arc: Arc, fill?: Fill, stroke?: Stroke) { + if (!fill && !stroke) { + return; + } + withContext(ctx, (ctx) => { + const { x, y, radius, startAngle, endAngle } = arc; + ctx.translate(x, y); + ctx.beginPath(); + ctx.arc(0, 0, radius, startAngle, endAngle, false); + fillAndStroke(ctx, fill, stroke); + }); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/line.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/line.ts new file mode 100644 index 000000000000..4bea23339889 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/line.ts @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RGBtoString } from '../../../../../common/color_library_wrappers'; +import { Stroke, Line } from '../../../../../geoms/types'; +import { withContext } from '../../../../../renderers/canvas'; + +/** + * Canvas2d stroke ignores an exact zero line width + * @internal + */ +export const MIN_STROKE_WIDTH = 0.001; + +/** @internal */ +export function renderLine(ctx: CanvasRenderingContext2D, line: Line, stroke: Stroke) { + renderMultiLine(ctx, [line], stroke); +} + +/** @internal */ +export function renderMultiLine(ctx: CanvasRenderingContext2D, lines: Line[] | string[], stroke: Stroke) { + if (stroke.width < MIN_STROKE_WIDTH) { + return; + } + withContext(ctx, (ctx) => { + const lineLength = lines.length; + if (lineLength === 0) { + return; + } + ctx.strokeStyle = RGBtoString(stroke.color); + ctx.lineJoin = 'round'; + ctx.lineWidth = stroke.width; + if (stroke.dash) { + ctx.setLineDash(stroke.dash); + } + + ctx.beginPath(); + + if (isStringArray(lines)) { + for (let i = 0; i < lineLength; i++) { + const path = lines[i]; + ctx.stroke(new Path2D(path)); + } + return; + } + for (let i = 0; i < lineLength; i++) { + const { x1, y1, x2, y2 } = lines[i]; + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + } + ctx.stroke(); + }); +} + +function isStringArray(lines: Line[] | string[]): lines is string[] { + return typeof lines[0] === 'string'; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts new file mode 100644 index 000000000000..0e82caad7c18 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts @@ -0,0 +1,116 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RGBtoString } from '../../../../../common/color_library_wrappers'; +import { Rect, Stroke, Fill } from '../../../../../geoms/types'; +import { withContext, withClipRanges } from '../../../../../renderers/canvas'; +import { getRadians } from '../../../../../utils/common'; +import { ClippedRanges } from '../../../../../utils/geometry'; +import { Point } from '../../../../../utils/point'; +import { renderMultiLine } from './line'; + +/** @internal */ +export function renderLinePaths( + context: CanvasRenderingContext2D, + transform: Point, + linePaths: Array, + stroke: Stroke, + clippedRanges: ClippedRanges, + clippings: Rect, + hideClippedRanges = false, +) { + if (clippedRanges.length > 0) { + withClipRanges(context, clippedRanges, clippings, false, (ctx) => { + ctx.translate(transform.x, transform.y); + renderMultiLine(ctx, linePaths, stroke); + }); + if (hideClippedRanges) { + return; + } + withClipRanges(context, clippedRanges, clippings, true, (ctx) => { + ctx.translate(transform.x, transform.y); + renderMultiLine(ctx, linePaths, { ...stroke, dash: [5, 5] }); + }); + return; + } + + withContext(context, (ctx) => { + ctx.translate(transform.x, transform.y); + renderMultiLine(ctx, linePaths, stroke); + }); +} + +/** @internal */ +export function renderAreaPath( + ctx: CanvasRenderingContext2D, + transform: Point, + area: string, + fill: Fill, + clippedRanges: ClippedRanges, + clippings: Rect, + hideClippedRanges = false, +) { + if (clippedRanges.length > 0) { + withClipRanges(ctx, clippedRanges, clippings, false, (ctx) => { + ctx.translate(transform.x, transform.y); + renderPathFill(ctx, area, fill); + }); + if (hideClippedRanges) { + return; + } + withClipRanges(ctx, clippedRanges, clippings, true, (ctx) => { + ctx.translate(transform.x, transform.y); + const { opacity } = fill.color; + const color = { + ...fill.color, + opacity: opacity / 2, + }; + renderPathFill(ctx, area, { ...fill, color }); + }); + return; + } + withContext(ctx, (ctx) => { + ctx.translate(transform.x, transform.y); + renderPathFill(ctx, area, fill); + }); +} + +function renderPathFill(ctx: CanvasRenderingContext2D, path: string, fill: Fill) { + const path2d = new Path2D(path); + ctx.fillStyle = RGBtoString(fill.color); + ctx.fill(path2d); + + if (fill.texture) { + ctx.clip(path2d); + + const rotation = getRadians(fill.texture.rotation ?? 0); + const { offset } = fill.texture; + + if (offset && offset.global) ctx.translate(offset?.x ?? 0, offset?.y ?? 0); + if (rotation) ctx.rotate(rotation); + if (offset && !offset.global) ctx.translate(offset?.x ?? 0, offset?.y ?? 0); + + ctx.fillStyle = fill.texture.pattern; + + // Use oversized rect to fill rotation/offset beyond path + const rotationRectFillSize = ctx.canvas.clientWidth * ctx.canvas.clientHeight; + ctx.translate(-rotationRectFillSize / 2, -rotationRectFillSize / 2); + ctx.fillRect(0, 0, rotationRectFillSize, rotationRectFillSize); + } +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts new file mode 100644 index 000000000000..0f9caf757a2e --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts @@ -0,0 +1,101 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RGBtoString } from '../../../../../common/color_library_wrappers'; +import { Rect, Fill, Stroke } from '../../../../../geoms/types'; +import { withContext } from '../../../../../renderers/canvas'; +import { getRadians } from '../../../../../utils/common'; + +/** @internal */ +export function renderRect( + ctx: CanvasRenderingContext2D, + rect: Rect, + fill?: Fill, + stroke?: Stroke, + disableBoardOffset: boolean = false, +) { + if (!fill && !stroke) { + return; + } + + if (fill) { + const borderOffset = !disableBoardOffset && stroke && stroke.width > 0.001 ? stroke.width : 0; + const x = rect.x + borderOffset; + const y = rect.y + borderOffset; + const width = rect.width - borderOffset * 2; + const height = rect.height - borderOffset * 2; + + drawRect(ctx, { x, y, width, height }); + ctx.fillStyle = RGBtoString(fill.color); + ctx.fill(); + + if (fill.texture) { + const { texture } = fill; + withContext(ctx, (ctx) => { + drawRect(ctx, { x, y, width, height }); + ctx.clip(); + + const rotation = getRadians(texture.rotation ?? 0); + const { offset } = texture; + + if (offset && offset.global) ctx.translate(offset?.x ?? 0, offset?.y ?? 0); + if (rotation) ctx.rotate(rotation); + if (offset && !offset.global) ctx.translate(offset?.x ?? 0, offset?.y ?? 0); + + ctx.fillStyle = texture.pattern; + + // Use oversized rect to fill rotation/offset beyond path + const rotationRectFillSize = ctx.canvas.clientWidth * ctx.canvas.clientHeight; + ctx.translate(-rotationRectFillSize / 2, -rotationRectFillSize / 2); + ctx.fillRect(0, 0, rotationRectFillSize, rotationRectFillSize); + }); + } + } + + if (stroke && stroke.width > 0.001) { + const borderOffset = !disableBoardOffset && stroke && stroke.width > 0.001 ? stroke.width / 2 : 0; + const x = rect.x + borderOffset; + const y = rect.y + borderOffset; + const width = rect.width - borderOffset * 2; + const height = rect.height - borderOffset * 2; + + ctx.strokeStyle = RGBtoString(stroke.color); + ctx.lineWidth = stroke.width; + drawRect(ctx, { x, y, width, height }); + if (stroke.dash) { + ctx.setLineDash(stroke.dash); + } else { + // Setting linecap with dash causes solid line + ctx.lineCap = 'square'; + } + + ctx.stroke(); + } +} + +/** @internal */ +function drawRect(ctx: CanvasRenderingContext2D, rect: Rect) { + const { x, y, width, height } = rect; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + width, y); + ctx.lineTo(x + width, y + height); + ctx.lineTo(x, y + height); + ctx.lineTo(x, y); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/shapes.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/shapes.ts new file mode 100644 index 000000000000..88cc2e130592 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/shapes.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Circle, Fill, Stroke } from '../../../../../geoms/types'; +import { withContext } from '../../../../../renderers/canvas'; +import { getRadians } from '../../../../../utils/common'; +import { PointShape } from '../../../../../utils/themes/theme'; +import { ShapeRendererFn } from '../../shapes_paths'; +import { fillAndStroke } from './utils'; + +/** @internal */ +export function renderShape( + ctx: CanvasRenderingContext2D, + shape: PointShape, + coordinates: Circle, + fill?: Fill, + stroke?: Stroke, +) { + if (!stroke || !fill) { + return; + } + withContext(ctx, (ctx) => { + const [pathFn, rotation] = ShapeRendererFn[shape]; + const { x, y, radius } = coordinates; + ctx.translate(x, y); + ctx.rotate(getRadians(rotation)); + ctx.beginPath(); + + const path = new Path2D(pathFn(radius)); + fillAndStroke(ctx, fill, stroke, path); + }); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/text.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/text.ts new file mode 100644 index 000000000000..475aea1cc128 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/text.ts @@ -0,0 +1,188 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { cssFontShorthand, Font, measureText, TextAlign, TextBaseline } from '../../../../../common/text_utils'; +import { withContext, withRotatedOrigin } from '../../../../../renderers/canvas'; +import { Point } from '../../../../../utils/point'; + +/** @internal */ +export type TextFont = Font & { + fill: string; + fontSize: number; + align: TextAlign; + baseline: TextBaseline; + shadow?: string; + shadowSize?: number; +}; + +/** @internal */ +export function renderText( + ctx: CanvasRenderingContext2D, + origin: Point, + text: string, + font: TextFont, + degree: number = 0, + translation?: Partial, + scale: number = 1, +) { + if (text === undefined || text === null) { + return; + } + + withRotatedOrigin(ctx, origin, degree, (ctx) => { + withContext(ctx, (ctx) => { + ctx.fillStyle = font.fill; + ctx.textAlign = font.align; + ctx.textBaseline = font.baseline; + ctx.font = cssFontShorthand(font, font.fontSize); + if (translation?.x || translation?.y) { + ctx.translate(translation?.x ?? 0, translation?.y ?? 0); + } + ctx.translate(origin.x, origin.y); + ctx.scale(scale, scale); + const shadowSize = font.shadowSize ?? 0; + if (font.shadow && shadowSize > 0) { + ctx.lineJoin = 'round'; + ctx.lineWidth = shadowSize; + ctx.strokeStyle = font.shadow; + ctx.strokeText(text, 0, 0); + } + ctx.fillText(text, 0, 0); + }); + }); +} + +const SPACE = ' '; +const ELLIPSIS = '…'; +const DASH = '-'; + +interface Options { + wrapAtWord: boolean; + shouldAddEllipsis: boolean; +} + +/** @internal */ +export function wrapLines( + ctx: CanvasRenderingContext2D, + text: string, + font: Font, + fontSize: number, + fixedWidth: number, + fixedHeight: number, + { wrapAtWord, shouldAddEllipsis }: Options = { wrapAtWord: true, shouldAddEllipsis: false }, +) { + const lineHeight = 1; + const lines = text.split('\n'); + let textWidth = 0; + const lineHeightPx = lineHeight * fontSize; + + const padding = 0; + const maxWidth = fixedWidth - padding * 2; + const maxHeightPx = fixedHeight - padding * 2; + let currentHeightPx = 0; + const shouldWrap = true; + const textArr: string[] = []; + const textMeasureProcessor = measureText(ctx); + const getTextWidth = (textString: string) => { + const measuredText = textMeasureProcessor(fontSize, [ + { + text: textString, + ...font, + }, + ]); + const [measure] = measuredText; + if (measure) { + return measure.width; + } + return 0; + }; + + const additionalWidth = shouldAddEllipsis ? getTextWidth(ELLIPSIS) : 0; + for (let i = 0, max = lines.length; i < max; ++i) { + let line = lines[i]; + let lineWidth = getTextWidth(line); + if (fixedWidth && lineWidth > maxWidth) { + while (line.length > 0) { + let low = 0; + let high = line.length; + let match = ''; + let matchWidth = 0; + while (low < high) { + const mid = (low + high) >>> 1; + const substr = line.slice(0, mid + 1); + const substrWidth = getTextWidth(substr) + additionalWidth; + if (substrWidth <= maxWidth) { + low = mid + 1; + match = substr + (shouldAddEllipsis ? ELLIPSIS : ''); + matchWidth = substrWidth; + } else { + high = mid; + } + } + if (match) { + if (wrapAtWord) { + const nextChar = line[match.length]; + const nextIsSpaceOrDash = nextChar === SPACE || nextChar === DASH; + const wrapIndex = + nextIsSpaceOrDash && matchWidth <= maxWidth + ? match.length + : Math.max(match.lastIndexOf(SPACE), match.lastIndexOf(DASH)) + 1; + if (wrapIndex > 0) { + low = wrapIndex; + match = match.slice(0, low); + matchWidth = getTextWidth(match); + } + } + match = match.trimEnd(); + textArr.push(match); + textWidth = Math.max(textWidth, matchWidth); + currentHeightPx += lineHeightPx; + if (!shouldWrap || (fixedHeight && currentHeightPx + lineHeightPx > maxHeightPx)) { + break; + } + line = line.slice(low); + line = line.trimStart(); + if (line.length > 0) { + lineWidth = getTextWidth(line); + if (lineWidth <= maxWidth) { + textArr.push(line); + currentHeightPx += lineHeightPx; + textWidth = Math.max(textWidth, lineWidth); + break; + } + } + } else { + break; + } + } + } else { + textArr.push(line); + currentHeightPx += lineHeightPx; + textWidth = Math.max(textWidth, lineWidth); + } + if (fixedHeight && currentHeightPx + lineHeightPx > maxHeightPx) { + break; + } + } + return { + lines: textArr, + height: fontSize, + width: textWidth, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/utils.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/utils.ts new file mode 100644 index 000000000000..94236d042a69 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/primitives/utils.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RGBtoString } from '../../../../../common/color_library_wrappers'; +import { Fill, Stroke } from '../../../../../geoms/types'; +import { MIN_STROKE_WIDTH } from './line'; + +/** + * WARNING: This function modify directly, without saving, the context calling the fill() and/or stroke() if defined + * @internal + */ +export function fillAndStroke(ctx: CanvasRenderingContext2D, fill?: Fill, stroke?: Stroke, path?: Path2D) { + if (fill) { + ctx.fillStyle = RGBtoString(fill.color); + if (path) { + ctx.fill(path); + } else { + ctx.fill(); + } + } + if (stroke && stroke.width > MIN_STROKE_WIDTH) { + ctx.strokeStyle = RGBtoString(stroke.color); + ctx.lineWidth = stroke.width; + if (stroke.dash) { + ctx.setLineDash(stroke.dash); + } + if (path) { + ctx.stroke(path); + } else { + ctx.stroke(); + } + } +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/renderers.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/renderers.ts new file mode 100644 index 000000000000..ed6d233b1491 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/renderers.ts @@ -0,0 +1,238 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { stringToRGB } from '../../../../common/color_library_wrappers'; +import { Rect } from '../../../../geoms/types'; +import { withContext, renderLayers, clearCanvas } from '../../../../renderers/canvas'; +import { renderAnnotations } from './annotations'; +import { renderAreas } from './areas'; +import { renderAxes } from './axes'; +import { renderBars } from './bars'; +import { renderBubbles } from './bubbles'; +import { renderGrids } from './grids'; +import { renderLines } from './lines'; +import { renderGridPanels } from './panels/panels'; +import { renderDebugRect } from './utils/debug'; +import { renderBarValues } from './values/bar'; +import { ReactiveChartStateProps } from './xy_chart'; + +/** @internal */ +export function renderXYChartCanvas2d( + ctx: CanvasRenderingContext2D, + dpr: number, + clippings: Rect, + props: ReactiveChartStateProps, +) { + const imgCanvas = document.createElement('canvas'); + + withContext(ctx, (ctx) => { + // let's set the devicePixelRatio once and for all; then we'll never worry about it again + ctx.scale(dpr, dpr); + const { + renderingArea, + chartTransform, + rotation, + geometries, + geometriesIndex, + theme: { axes: sharedAxesStyle, sharedStyle, barSeriesStyle }, + highlightedLegendItem, + annotationDimensions, + annotationSpecs, + perPanelAxisGeoms, + perPanelGridLines, + axesSpecs, + axesStyles, + debug, + panelGeoms, + } = props; + const transform = { + x: renderingArea.left + chartTransform.x, + y: renderingArea.top + chartTransform.y, + }; + // painter's algorithm, like that of SVG: the sequence determines what overdraws what; first element of the array is drawn first + // (of course, with SVG, it's for ambiguous situations only, eg. when 3D transforms with different Z values aren't used, but + // unlike SVG and esp. WebGL, Canvas2d doesn't support the 3rd dimension well, see ctx.transform / ctx.setTransform). + // The layers are callbacks, because of the need to not bake in the `ctx`, it feels more composable and uncoupled this way. + renderLayers(ctx, [ + // clear the canvas + (ctx: CanvasRenderingContext2D) => clearCanvas(ctx, 200000, 200000), + // render panel grid + (ctx: CanvasRenderingContext2D) => { + if (debug) { + renderGridPanels(ctx, transform, panelGeoms); + } + }, + (ctx: CanvasRenderingContext2D) => { + renderAxes(ctx, { + axesSpecs, + perPanelAxisGeoms, + renderingArea, + debug, + axesStyles, + sharedAxesStyle, + }); + }, + (ctx: CanvasRenderingContext2D) => { + renderGrids(ctx, { + axesSpecs, + renderingArea, + perPanelGridLines, + axesStyles, + sharedAxesStyle, + }); + }, + // rendering background annotations + (ctx: CanvasRenderingContext2D) => { + withContext(ctx, (ctx) => { + renderAnnotations( + ctx, + { + rotation, + renderingArea, + annotationDimensions, + annotationSpecs, + }, + true, + ); + }); + }, + + // rendering bars + (ctx: CanvasRenderingContext2D) => { + withContext(ctx, (ctx) => { + renderBars( + ctx, + imgCanvas, + geometries.bars, + sharedStyle, + clippings, + renderingArea, + highlightedLegendItem, + rotation, + ); + }); + }, + // rendering areas + (ctx: CanvasRenderingContext2D) => { + withContext(ctx, (ctx) => { + renderAreas(ctx, imgCanvas, { + areas: geometries.areas, + clippings, + renderingArea, + rotation, + highlightedLegendItem, + sharedStyle, + }); + }); + }, + // rendering lines + (ctx: CanvasRenderingContext2D) => { + withContext(ctx, (ctx) => { + renderLines(ctx, { + lines: geometries.lines, + clippings, + renderingArea, + rotation, + highlightedLegendItem, + sharedStyle, + }); + }); + }, + // rendering bubbles + (ctx: CanvasRenderingContext2D) => { + renderBubbles(ctx, { + bubbles: geometries.bubbles, + clippings, + highlightedLegendItem, + sharedStyle, + rotation, + renderingArea, + }); + }, + (ctx: CanvasRenderingContext2D) => { + geometries.bars.forEach(({ value: bars, panel }) => { + withContext(ctx, (ctx) => { + renderBarValues(ctx, { + bars, + panel, + renderingArea, + rotation, + debug, + barSeriesStyle, + }); + }); + }); + }, + // rendering foreground annotations + (ctx: CanvasRenderingContext2D) => { + withContext(ctx, (ctx) => { + renderAnnotations( + ctx, + { + annotationDimensions, + annotationSpecs, + rotation, + renderingArea, + }, + false, + ); + }); + }, + // rendering debugger + (ctx: CanvasRenderingContext2D) => { + if (!debug) { + return; + } + withContext(ctx, (ctx) => { + const { left, top, width, height } = renderingArea; + + renderDebugRect( + ctx, + { + x: left, + y: top, + width, + height, + }, + { + color: stringToRGB('transparent'), + }, + { + color: stringToRGB('red'), + width: 4, + dash: [4, 4], + }, + ); + + const triangulation = geometriesIndex.triangulation([0, 0, width, height]); + + if (triangulation) { + ctx.beginPath(); + ctx.translate(left, top); + ctx.setLineDash([5, 5]); + triangulation.render(ctx); + ctx.lineWidth = 1; + ctx.strokeStyle = 'blue'; + ctx.stroke(); + } + }); + }, + ]); + }); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/area.test.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/area.test.ts new file mode 100644 index 000000000000..75367f1e3b87 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/area.test.ts @@ -0,0 +1,120 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { stringToRGB } from '../../../../../common/color_library_wrappers'; +import { Fill } from '../../../../../geoms/types'; +import { getMockCanvas, getMockCanvasContext2D, MockStyles } from '../../../../../mocks'; +import * as common from '../../../../../utils/common'; +import { getTextureStyles } from '../../../utils/texture'; +import { buildAreaStyles } from './area'; + +import 'jest-canvas-mock'; + +jest.mock('../../../../../common/color_library_wrappers'); +jest.mock('../../../utils/texture'); +jest.spyOn(common, 'getColorFromVariant'); + +const COLOR = 'aquamarine'; + +describe('Area styles', () => { + let ctx: CanvasRenderingContext2D; + let imgCanvas: HTMLCanvasElement; + + beforeEach(() => { + ctx = getMockCanvasContext2D(); + imgCanvas = getMockCanvas(); + }); + + describe('#buildAreaStyles', () => { + let result: Fill; + let baseColor = COLOR; + let themeAreaStyle = MockStyles.area(); + let geometryStateStyle = MockStyles.geometryState(); + + function setDefaults() { + baseColor = COLOR; + themeAreaStyle = MockStyles.area(); + geometryStateStyle = MockStyles.geometryState(); + } + + beforeEach(() => { + result = buildAreaStyles(ctx, imgCanvas, baseColor, themeAreaStyle, geometryStateStyle); + }); + + it('should call getColorFromVariant with correct args for fill', () => { + expect(common.getColorFromVariant).nthCalledWith(1, baseColor, themeAreaStyle.fill); + }); + + describe('Colors', () => { + const fillColor = '#4aefb8'; + + beforeAll(() => { + setDefaults(); + (common.getColorFromVariant as jest.Mock).mockReturnValue(fillColor); + }); + + it('should call stringToRGB with values from getColorFromVariant', () => { + expect(stringToRGB).nthCalledWith(1, fillColor, expect.any(Function)); + }); + + it('should return fill with color', () => { + expect(result.color).toEqual(stringToRGB(fillColor)); + }); + }); + + describe('Opacity', () => { + const fillColorOpacity = 0.5; + const fillColor = `rgba(10,10,10,${fillColorOpacity})`; + const fillOpacity = 0.6; + const geoOpacity = 0.75; + + beforeAll(() => { + setDefaults(); + themeAreaStyle = MockStyles.area({ opacity: fillOpacity }); + geometryStateStyle = MockStyles.geometryState({ opacity: geoOpacity }); + (common.getColorFromVariant as jest.Mock).mockReturnValue(fillColor); + }); + + it('should return correct fill opacity', () => { + const expected = fillColorOpacity * fillOpacity * geoOpacity; + expect(result.color.opacity).toEqual(expected); + }); + }); + + describe('Texture', () => { + const texture = {}; + const mockTexture = {}; + + beforeAll(() => { + setDefaults(); + themeAreaStyle = MockStyles.area({ texture }); + (getTextureStyles as jest.Mock).mockReturnValue(mockTexture); + }); + + it('should return correct texture', () => { + expect(result.texture).toEqual(mockTexture); + }); + + it('should call getTextureStyles with params', () => { + expect(getTextureStyles).toBeCalledTimes(1); + expect(getTextureStyles).toBeCalledWith(ctx, imgCanvas, baseColor, expect.anything(), texture); + }); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/area.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/area.ts new file mode 100644 index 000000000000..66319baa0622 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/area.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { OpacityFn, stringToRGB } from '../../../../../common/color_library_wrappers'; +import { Fill } from '../../../../../geoms/types'; +import { Color, ColorVariant, getColorFromVariant } from '../../../../../utils/common'; +import { GeometryStateStyle, AreaStyle } from '../../../../../utils/themes/theme'; +import { getTextureStyles } from '../../../utils/texture'; + +/** + * Return the rendering props for an area. The color of the area will be overwritten + * by the fill color of the themeAreaStyle parameter if present + * @param baseColor the assigned color of the area for this series + * @param themeAreaStyle the theme style for the area series + * @param geometryStateStyle the highlight geometry style + * @internal + */ +export function buildAreaStyles( + ctx: CanvasRenderingContext2D, + imgCanvas: HTMLCanvasElement, + baseColor: Color | ColorVariant, + themeAreaStyle: AreaStyle, + geometryStateStyle: GeometryStateStyle, +): Fill { + const fillOpacity: OpacityFn = (opacity, seriesOpacity = themeAreaStyle.opacity) => + opacity * seriesOpacity * geometryStateStyle.opacity; + const texture = getTextureStyles(ctx, imgCanvas, baseColor, fillOpacity, themeAreaStyle.texture); + const color = stringToRGB(getColorFromVariant(baseColor, themeAreaStyle.fill), fillOpacity); + + return { + color, + texture, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/bar.test.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/bar.test.ts new file mode 100644 index 000000000000..07336c6606d0 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/bar.test.ts @@ -0,0 +1,185 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { stringToRGB } from '../../../../../common/color_library_wrappers'; +import { Fill, Stroke } from '../../../../../geoms/types'; +import { getMockCanvas, getMockCanvasContext2D, MockStyles } from '../../../../../mocks'; +import * as common from '../../../../../utils/common'; +import { getTextureStyles } from '../../../utils/texture'; +import { buildBarStyles } from './bar'; + +import 'jest-canvas-mock'; + +jest.mock('../../../../../common/color_library_wrappers'); +jest.mock('../../../utils/texture'); +jest.spyOn(common, 'getColorFromVariant'); + +const COLOR = 'aquamarine'; + +describe('Bar styles', () => { + let ctx: CanvasRenderingContext2D; + let imgCanvas: HTMLCanvasElement; + + beforeEach(() => { + ctx = getMockCanvasContext2D(); + imgCanvas = getMockCanvas(); + }); + + describe('#buildBarStyles', () => { + let result: { fill: Fill; stroke: Stroke }; + let baseColor = COLOR; + let themeRectStyle = MockStyles.rect(); + let themeRectBorderStyle = MockStyles.rectBorder(); + let geometryStateStyle = MockStyles.geometryState(); + + function setDefaults() { + baseColor = COLOR; + themeRectStyle = MockStyles.rect(); + themeRectBorderStyle = MockStyles.rectBorder(); + geometryStateStyle = MockStyles.geometryState(); + } + + beforeEach(() => { + result = buildBarStyles(ctx, imgCanvas, baseColor, themeRectStyle, themeRectBorderStyle, geometryStateStyle); + }); + + it('should call getColorFromVariant with correct args for fill', () => { + expect(common.getColorFromVariant).nthCalledWith(1, baseColor, themeRectStyle.fill); + }); + + it('should call getColorFromVariant with correct args for border', () => { + expect(common.getColorFromVariant).nthCalledWith(1, baseColor, themeRectBorderStyle.stroke); + }); + + describe('Colors', () => { + const fillColor = '#4aefb8'; + const strokeColor = '#a740cf'; + + beforeAll(() => { + setDefaults(); + (common.getColorFromVariant as jest.Mock).mockImplementation(() => { + const { length } = (common.getColorFromVariant as jest.Mock).mock.calls; + return length === 1 ? fillColor : strokeColor; + }); + }); + + it('should call stringToRGB with values from getColorFromVariant', () => { + expect(stringToRGB).nthCalledWith(1, fillColor, expect.any(Function)); + expect(stringToRGB).nthCalledWith(2, strokeColor, expect.any(Function)); + }); + + it('should return fill with color', () => { + expect(result.fill.color).toEqual(stringToRGB(fillColor)); + }); + + it('should return stroke with color', () => { + expect(result.stroke.color).toEqual(stringToRGB(strokeColor)); + }); + }); + + describe('Opacity', () => { + const fillColorOpacity = 0.5; + const strokeColorOpacity = 0.25; + const fillColor = `rgba(10,10,10,${fillColorOpacity})`; + const strokeColor = `rgba(10,10,10,${strokeColorOpacity})`; + const fillOpacity = 0.6; + const strokeOpacity = 0.8; + const geoOpacity = 0.75; + + beforeAll(() => { + setDefaults(); + themeRectStyle = MockStyles.rect({ opacity: fillOpacity }); + themeRectBorderStyle = MockStyles.rectBorder({ strokeOpacity }); + geometryStateStyle = MockStyles.geometryState({ opacity: geoOpacity }); + (common.getColorFromVariant as jest.Mock).mockImplementation(() => { + const { length } = (common.getColorFromVariant as jest.Mock).mock.calls; + return length === 1 ? fillColor : strokeColor; + }); + }); + + it('should return correct fill opacity', () => { + const expected = fillColorOpacity * fillOpacity * geoOpacity; + expect(result.fill.color.opacity).toEqual(expected); + }); + + it('should return correct stroke opacity', () => { + const expected = strokeColorOpacity * strokeOpacity * geoOpacity; + expect(result.stroke.color.opacity).toEqual(expected); + }); + + describe('themeRectBorderStyle opacity is undefined', () => { + beforeAll(() => { + themeRectBorderStyle = { + ...MockStyles.rectBorder(), + strokeOpacity: undefined, + }; + }); + + it('should use themeRectStyle opacity', () => { + const expected = strokeColorOpacity * fillOpacity * geoOpacity; + expect(result.stroke.color.opacity).toEqual(expected); + }); + }); + }); + + describe('Width', () => { + describe('visible is set to false', () => { + beforeAll(() => { + themeRectBorderStyle = MockStyles.rectBorder({ visible: false }); + }); + + it('should set stroke width to zero', () => { + expect(result.stroke.width).toEqual(0); + }); + }); + + describe('visible is set to true', () => { + const strokeWidth = 22; + + beforeAll(() => { + themeRectBorderStyle = MockStyles.rectBorder({ visible: true, strokeWidth }); + }); + + it('should set stroke width to strokeWidth', () => { + expect(result.stroke.width).toEqual(strokeWidth); + }); + }); + }); + + describe('Texture', () => { + const texture = {}; + const mockTexture = {}; + + beforeAll(() => { + setDefaults(); + themeRectStyle = MockStyles.rect({ texture }); + (getTextureStyles as jest.Mock).mockReturnValue(mockTexture); + }); + + it('should return correct texture', () => { + expect(result.fill.texture).toEqual(mockTexture); + }); + + it('should call getTextureStyles with params', () => { + expect(getTextureStyles).toBeCalledTimes(1); + expect(getTextureStyles).toBeCalledWith(ctx, imgCanvas, baseColor, expect.anything(), texture); + }); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts new file mode 100644 index 000000000000..e4e697d4c2ae --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { stringToRGB, OpacityFn } from '../../../../../common/color_library_wrappers'; +import { Stroke, Fill } from '../../../../../geoms/types'; +import { getColorFromVariant } from '../../../../../utils/common'; +import { GeometryStateStyle, RectStyle, RectBorderStyle } from '../../../../../utils/themes/theme'; +import { getTextureStyles } from '../../../utils/texture'; + +/** + * Return the rendering styles (stroke and fill) for a bar. + * The full color of the bar will be overwritten by the fill color + * of the themeRectStyle parameter if present. + * The stroke color of the bar will be overwritten by the stroke color + * of the themeRectBorderStyle parameter if present. + * @param baseColor the assigned color of the bar for this series + * @param themeRectStyle the theme style of the rectangle for the bar series + * @param themeRectBorderStyle the theme style of the rectangle borders for the bar series + * @param geometryStateStyle the highlight geometry style + * @internal + */ +export function buildBarStyles( + ctx: CanvasRenderingContext2D, + imgCanvas: HTMLCanvasElement, + baseColor: string, + themeRectStyle: RectStyle, + themeRectBorderStyle: RectBorderStyle, + geometryStateStyle: GeometryStateStyle, +): { fill: Fill; stroke: Stroke } { + const fillOpacity: OpacityFn = (opacity, seriesOpacity = themeRectStyle.opacity) => + opacity * seriesOpacity * geometryStateStyle.opacity; + const texture = getTextureStyles(ctx, imgCanvas, baseColor, fillOpacity, themeRectStyle.texture); + const fillColor = stringToRGB(getColorFromVariant(baseColor, themeRectStyle.fill), fillOpacity); + const fill: Fill = { + color: fillColor, + texture, + }; + const defaultStrokeOpacity = + themeRectBorderStyle.strokeOpacity === undefined ? themeRectStyle.opacity : themeRectBorderStyle.strokeOpacity; + const borderStrokeOpacity = defaultStrokeOpacity * geometryStateStyle.opacity; + const strokeOpacity: OpacityFn = (opacity) => opacity * borderStrokeOpacity; + const strokeColor = stringToRGB(getColorFromVariant(baseColor, themeRectBorderStyle.stroke), strokeOpacity); + const stroke: Stroke = { + color: strokeColor, + width: themeRectBorderStyle.visible ? themeRectBorderStyle.strokeWidth : 0, + }; + return { fill, stroke }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/line.test.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/line.test.ts new file mode 100644 index 000000000000..390c53163e57 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/line.test.ts @@ -0,0 +1,96 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { stringToRGB } from '../../../../../common/color_library_wrappers'; +import { Stroke } from '../../../../../geoms/types'; +import { MockStyles } from '../../../../../mocks'; +import * as common from '../../../../../utils/common'; +import { buildLineStyles } from './line'; + +jest.mock('../../../../../common/color_library_wrappers'); +jest.spyOn(common, 'getColorFromVariant'); + +const COLOR = 'aquamarine'; + +describe('Line styles', () => { + describe('#buildLineStyles', () => { + let result: Stroke; + let baseColor = COLOR; + let themeLineStyle = MockStyles.line(); + let geometryStateStyle = MockStyles.geometryState(); + + function setDefaults() { + baseColor = COLOR; + themeLineStyle = MockStyles.line(); + geometryStateStyle = MockStyles.geometryState(); + } + + beforeEach(() => { + result = buildLineStyles(baseColor, themeLineStyle, geometryStateStyle); + }); + + it('should call getColorFromVariant with correct args for stroke', () => { + expect(common.getColorFromVariant).nthCalledWith(1, baseColor, themeLineStyle.stroke); + }); + + it('should set strokeWidth from themeLineStyle', () => { + expect(result.width).toBe(themeLineStyle.strokeWidth); + }); + + it('should set dash from themeLineStyle', () => { + expect(result.dash).toEqual(themeLineStyle.dash); + }); + + describe('Colors', () => { + const strokeColor = '#4aefb8'; + + beforeAll(() => { + setDefaults(); + (common.getColorFromVariant as jest.Mock).mockReturnValue(strokeColor); + }); + + it('should call stringToRGB with values from getColorFromVariant', () => { + expect(stringToRGB).nthCalledWith(1, strokeColor, expect.any(Function)); + }); + + it('should return stroke with color', () => { + expect(result.color).toEqual(stringToRGB(strokeColor)); + }); + }); + + describe('Opacity', () => { + const strokeColorOpacity = 0.5; + const strokeColor = `rgba(10,10,10,${strokeColorOpacity})`; + const strokeOpacity = 0.6; + const geoOpacity = 0.75; + + beforeAll(() => { + setDefaults(); + themeLineStyle = MockStyles.line({ opacity: strokeOpacity }); + geometryStateStyle = MockStyles.geometryState({ opacity: geoOpacity }); + (common.getColorFromVariant as jest.Mock).mockReturnValue(strokeColor); + }); + + it('should return correct stroke opacity', () => { + const expected = strokeColorOpacity * strokeOpacity * geoOpacity; + expect(result.color.opacity).toEqual(expected); + }); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/line.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/line.ts new file mode 100644 index 000000000000..f95996a71f1a --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/styles/line.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { stringToRGB, OpacityFn } from '../../../../../common/color_library_wrappers'; +import { Stroke } from '../../../../../geoms/types'; +import { getColorFromVariant } from '../../../../../utils/common'; +import { GeometryStateStyle, LineStyle } from '../../../../../utils/themes/theme'; + +/** + * Return the rendering props for a line. The color of the line will be overwritten + * by the stroke color of the themeLineStyle parameter if present + * @param baseColor the assigned color of the line for this series + * @param themeLineStyle the theme style for the line series + * @param geometryStateStyle the highlight geometry style + * @internal + */ +export function buildLineStyles( + baseColor: string, + themeLineStyle: LineStyle, + geometryStateStyle: GeometryStateStyle, +): Stroke { + const strokeOpacity: OpacityFn = (opacity) => opacity * themeLineStyle.opacity * geometryStateStyle.opacity; + const strokeColor = stringToRGB(getColorFromVariant(baseColor, themeLineStyle.stroke), strokeOpacity); + return { + color: strokeColor, + width: themeLineStyle.strokeWidth, + dash: themeLineStyle.dash, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/utils/debug.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/utils/debug.ts new file mode 100644 index 000000000000..6e2224b95971 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/utils/debug.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Fill, Stroke, Rect } from '../../../../../geoms/types'; +import { withContext } from '../../../../../renderers/canvas'; +import { getRadians } from '../../../../../utils/common'; +import { Point } from '../../../../../utils/point'; +import { renderRect } from '../primitives/rect'; + +const DEFAULT_DEBUG_FILL: Fill = { + color: { + r: 238, + g: 130, + b: 238, + opacity: 0.2, + }, +}; +const DEFAULT_DEBUG_STROKE: Stroke = { + color: { + r: 0, + g: 0, + b: 0, + opacity: 0.2, + }, + width: 1, +}; + +/** @internal */ +export function renderDebugRect( + ctx: CanvasRenderingContext2D, + rect: Rect, + fill = DEFAULT_DEBUG_FILL, // violet + stroke = DEFAULT_DEBUG_STROKE, + rotation: number = 0, +) { + withContext(ctx, (ctx) => { + ctx.translate(rect.x, rect.y); + ctx.rotate(getRadians(rotation)); + renderRect( + ctx, + { + ...rect, + x: 0, + y: 0, + }, + fill, + stroke, + true, + ); + }); +} + +/** @internal */ +export function renderDebugRectCenterRotated( + ctx: CanvasRenderingContext2D, + center: Point, + rect: Rect, + fill = DEFAULT_DEBUG_FILL, // violet + stroke = DEFAULT_DEBUG_STROKE, + rotation: number = 0, +) { + const { x, y } = center; + + withContext(ctx, (ctx) => { + ctx.translate(x, y); + ctx.rotate(getRadians(rotation)); + ctx.translate(-x, -y); + renderRect( + ctx, + { + ...rect, + x: x - rect.width / 2, + y: y - rect.height / 2, + }, + fill, + stroke, + ); + }); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/utils/panel_transform.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/utils/panel_transform.ts new file mode 100644 index 000000000000..71f594f146e8 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/utils/panel_transform.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Rect } from '../../../../../geoms/types'; +import { withContext } from '../../../../../renderers/canvas'; +import { getRadians, Rotation } from '../../../../../utils/common'; +import { Dimensions } from '../../../../../utils/dimensions'; +import { computeChartTransform } from '../../../state/utils/utils'; + +/** @internal */ +export function withPanelTransform( + context: CanvasRenderingContext2D, + panel: Dimensions, + rotation: Rotation, + renderingArea: Dimensions, + fn: (ctx: CanvasRenderingContext2D) => void, + clippings?: { + area: Rect; + shouldClip?: boolean; + }, +) { + const transform = computeChartTransform(panel, rotation); + const left = renderingArea.left + panel.left + transform.x; + const top = renderingArea.top + panel.top + transform.y; + withContext(context, (ctx) => { + ctx.translate(left, top); + ctx.rotate(getRadians(rotation)); + + if (clippings?.shouldClip) { + const { x, y, width, height } = clippings.area; + ctx.save(); + ctx.beginPath(); + ctx.rect(x, y, width, height); + ctx.clip(); + } + fn(ctx); + if (clippings?.shouldClip) { + ctx.restore(); + } + }); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/values/bar.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/values/bar.ts new file mode 100644 index 000000000000..de79df4cdd88 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/values/bar.ts @@ -0,0 +1,422 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { colorIsDark, getTextColorIfTextInvertible } from '../../../../../common/color_calcs'; +import { fillTextColor } from '../../../../../common/fill_text_color'; +import { Font, FontStyle, TextAlign, TextBaseline } from '../../../../../common/text_utils'; +import { Rect } from '../../../../../geoms/types'; +import { Rotation, VerticalAlignment, HorizontalAlignment } from '../../../../../utils/common'; +import { Dimensions } from '../../../../../utils/dimensions'; +import { BarGeometry } from '../../../../../utils/geometry'; +import { Point } from '../../../../../utils/point'; +import { Theme, TextAlignment } from '../../../../../utils/themes/theme'; +import { renderText, wrapLines } from '../primitives/text'; +import { renderDebugRect } from '../utils/debug'; +import { withPanelTransform } from '../utils/panel_transform'; + +interface BarValuesProps { + barSeriesStyle: Theme['barSeriesStyle']; + renderingArea: Dimensions; + rotation: Rotation; + debug: boolean; + bars: BarGeometry[]; + panel: Dimensions; +} + +const CHART_DIRECTION: Record = { + BottomUp: 0, + TopToBottom: 180, + LeftToRight: 90, + RightToLeft: -90, +}; + +/** @internal */ +export function renderBarValues(ctx: CanvasRenderingContext2D, props: BarValuesProps) { + const { bars, debug, rotation, renderingArea, barSeriesStyle, panel } = props; + const { fontFamily, fontStyle, fill, alignment } = barSeriesStyle.displayValue; + const barsLength = bars.length; + for (let i = 0; i < barsLength; i++) { + const { displayValue } = bars[i]; + if (!displayValue) { + continue; + } + const { text, fontSize, fontScale } = displayValue; + let textLines = { + lines: [text], + width: displayValue.width, + height: displayValue.height, + }; + const font: Font = { + fontFamily, + fontStyle: fontStyle ? (fontStyle as FontStyle) : 'normal', + fontVariant: 'normal', + fontWeight: 'normal', + textColor: 'black', + textOpacity: 1, + }; + + const { x, y, align, baseline, rect } = positionText( + bars[i], + displayValue, + rotation, + barSeriesStyle.displayValue, + alignment, + ); + + if (displayValue.isValueContainedInElement) { + const width = rotation === 0 || rotation === 180 ? bars[i].width : bars[i].height; + textLines = wrapLines(ctx, textLines.lines[0], font, fontSize, width, 100); + } + if (displayValue.hideClippedValue && isOverflow(rect, renderingArea, rotation)) { + continue; + } + if (debug) { + renderDebugRect(ctx, rect); + } + const { width, height } = textLines; + const linesLength = textLines.lines.length; + const shadowSize = getTextBorderSize(fill); + const { fillColor, shadowColor } = getTextColors(fill, bars[i].color, shadowSize); + + for (let j = 0; j < linesLength; j++) { + const textLine = textLines.lines[j]; + const origin = repositionTextLine({ x, y }, rotation, j, linesLength, { height, width }); + withPanelTransform(ctx, panel, rotation, renderingArea, (ctx) => { + renderText( + ctx, + origin, + textLine, + { + ...font, + fill: fillColor, + fontSize, + align, + baseline, + shadow: shadowColor, + shadowSize, + }, + -rotation, + undefined, + fontScale, + ); + }); + } + } +} +function repositionTextLine( + origin: Point, + chartRotation: Rotation, + i: number, + max: number, + box: { height: number; width: number }, +) { + const { x, y } = origin; + const { width, height } = box; + let lineX: number; + let lineY: number; + switch (chartRotation) { + case 180: + lineX = x; + lineY = y - (i - max + 1) * height; + break; + case -90: + lineX = x; + lineY = y; + break; + case 90: + lineX = x; + lineY = y - (i - max + 1) * width; + break; + case 0: + default: + lineX = x; + lineY = y + i * height; + } + + return { x: lineX, y: lineY }; +} + +function computeHorizontalOffset( + geom: BarGeometry, + valueBox: { width: number; height: number }, + chartRotation: Rotation, + { horizontal }: Partial = {}, +) { + switch (chartRotation) { + case CHART_DIRECTION.LeftToRight: { + if (horizontal === HorizontalAlignment.Left) { + return geom.height - valueBox.width; + } + if (horizontal === HorizontalAlignment.Center) { + return geom.height / 2 - valueBox.width / 2; + } + break; + } + case CHART_DIRECTION.RightToLeft: { + if (horizontal === HorizontalAlignment.Right) { + return geom.height - valueBox.width; + } + if (horizontal === HorizontalAlignment.Center) { + return geom.height / 2 - valueBox.width / 2; + } + break; + } + case CHART_DIRECTION.TopToBottom: { + if (horizontal === HorizontalAlignment.Left) { + return geom.width / 2 - valueBox.width / 2; + } + if (horizontal === HorizontalAlignment.Right) { + return -geom.width / 2 + valueBox.width / 2; + } + break; + } + case CHART_DIRECTION.BottomUp: + default: { + if (horizontal === HorizontalAlignment.Left) { + return -geom.width / 2 + valueBox.width / 2; + } + if (horizontal === HorizontalAlignment.Right) { + return geom.width / 2 - valueBox.width / 2; + } + } + } + return 0; +} + +function computeVerticalOffset( + geom: BarGeometry, + valueBox: { width: number; height: number }, + chartRotation: Rotation, + { vertical }: Partial = {}, +) { + switch (chartRotation) { + case CHART_DIRECTION.LeftToRight: { + if (vertical === VerticalAlignment.Bottom) { + return geom.width - valueBox.height; + } + if (vertical === VerticalAlignment.Middle) { + return geom.width / 2 - valueBox.height / 2; + } + break; + } + case CHART_DIRECTION.RightToLeft: { + if (vertical === VerticalAlignment.Bottom) { + return -geom.width + valueBox.height; + } + if (vertical === VerticalAlignment.Middle) { + return -geom.width / 2 + valueBox.height / 2; + } + break; + } + case CHART_DIRECTION.TopToBottom: { + if (vertical === VerticalAlignment.Top) { + return geom.height - valueBox.height; + } + if (vertical === VerticalAlignment.Middle) { + return geom.height / 2 - valueBox.height / 2; + } + break; + } + case CHART_DIRECTION.BottomUp: + default: { + if (vertical === VerticalAlignment.Bottom) { + return geom.height - valueBox.height; + } + if (vertical === VerticalAlignment.Middle) { + return geom.height / 2 - valueBox.height / 2; + } + } + } + return 0; +} + +function computeAlignmentOffset( + geom: BarGeometry, + valueBox: { width: number; height: number }, + chartRotation: Rotation, + textAlignment: Partial = {}, +) { + return { + alignmentOffsetX: computeHorizontalOffset(geom, valueBox, chartRotation, textAlignment), + alignmentOffsetY: computeVerticalOffset(geom, valueBox, chartRotation, textAlignment), + }; +} + +function positionText( + geom: BarGeometry, + valueBox: { width: number; height: number }, + chartRotation: Rotation, + offsets: { offsetX: number; offsetY: number }, + alignment?: TextAlignment, +): { x: number; y: number; align: TextAlign; baseline: TextBaseline; rect: Rect } { + const { offsetX, offsetY } = offsets; + + const { alignmentOffsetX, alignmentOffsetY } = computeAlignmentOffset(geom, valueBox, chartRotation, alignment); + + switch (chartRotation) { + case CHART_DIRECTION.TopToBottom: { + const x = geom.x + geom.width / 2 - offsetX + alignmentOffsetX; + const y = geom.y + offsetY + alignmentOffsetY; + return { + x, + y, + align: 'center', + baseline: 'bottom', + rect: { + x: x - valueBox.width / 2, + y, + width: valueBox.width, + height: valueBox.height, + }, + }; + } + case CHART_DIRECTION.RightToLeft: { + const x = geom.x + geom.width + offsetY + alignmentOffsetY; + const y = geom.y - offsetX + alignmentOffsetX; + return { + x, + y, + align: 'left', + baseline: 'top', + rect: { + x: x - valueBox.height, + y, + width: valueBox.height, + height: valueBox.width, + }, + }; + } + case CHART_DIRECTION.LeftToRight: { + const x = geom.x - offsetY + alignmentOffsetY; + const y = geom.y + offsetX + alignmentOffsetX; + return { + x, + y, + align: 'right', + baseline: 'top', + rect: { + x, + y, + width: valueBox.height, + height: valueBox.width, + }, + }; + } + case CHART_DIRECTION.BottomUp: + default: { + const x = geom.x + geom.width / 2 - offsetX + alignmentOffsetX; + const y = geom.y - offsetY + alignmentOffsetY; + return { + x, + y, + align: 'center', + baseline: 'top', + rect: { + x: x - valueBox.width / 2, + y, + width: valueBox.width, + height: valueBox.height, + }, + }; + } + } +} + +function isOverflow(rect: Rect, chartDimensions: Dimensions, chartRotation: Rotation) { + let cWidth = chartDimensions.width; + let cHeight = chartDimensions.height; + if (chartRotation === 90 || chartRotation === -90) { + cWidth = chartDimensions.height; + cHeight = chartDimensions.width; + } + + if (rect.x < 0 || rect.x + rect.width > cWidth) { + return true; + } + if (rect.y < 0 || rect.y + rect.height > cHeight) { + return true; + } + + return false; +} + +const DEFAULT_VALUE_COLOR = 'black'; +// a little bit of alpha makes black font more readable +const DEFAULT_VALUE_BORDER_COLOR = 'rgba(255, 255, 255, 0.8)'; +const DEFAULT_VALUE_BORDER_SOLID_COLOR = 'rgb(255, 255, 255)'; +const TRANSPARENT_COLOR = 'rgba(0,0,0,0)'; +type ValueFillDefinition = Theme['barSeriesStyle']['displayValue']['fill']; + +function getTextColors( + fillDefinition: ValueFillDefinition, + geometryColor: string, + borderSize: number, +): { fillColor: string; shadowColor: string } { + if (typeof fillDefinition === 'string') { + return { fillColor: fillDefinition, shadowColor: TRANSPARENT_COLOR }; + } + if ('color' in fillDefinition) { + return { + fillColor: fillDefinition.color, + shadowColor: fillDefinition.borderColor || TRANSPARENT_COLOR, + }; + } + const fillColor = + fillTextColor( + DEFAULT_VALUE_COLOR, + fillDefinition.textInvertible, + fillDefinition.textContrast || false, + geometryColor, + 'white', + ) || DEFAULT_VALUE_COLOR; + + // If the border is too wide it can overlap between a letter or another + // therefore use a solid color for thinker borders + const defaultBorderColor = borderSize < 2 ? DEFAULT_VALUE_BORDER_COLOR : DEFAULT_VALUE_BORDER_SOLID_COLOR; + const shadowColor = + 'textBorder' in fillDefinition + ? getTextColorIfTextInvertible( + colorIsDark(fillColor), + colorIsDark(defaultBorderColor), + defaultBorderColor, + false, + geometryColor, + ) || TRANSPARENT_COLOR + : TRANSPARENT_COLOR; + + return { + fillColor, + shadowColor, + }; +} + +const DEFAULT_BORDER_WIDTH = 1.5; +const MAX_BORDER_WIDTH = 8; + +function getTextBorderSize(fill: ValueFillDefinition): number { + if (typeof fill === 'string') { + return DEFAULT_BORDER_WIDTH; + } + if ('borderWidth' in fill) { + return Math.min(fill.borderWidth ?? DEFAULT_BORDER_WIDTH, MAX_BORDER_WIDTH); + } + const borderWidth = + 'textBorder' in fill && typeof fill.textBorder === 'number' ? fill.textBorder : DEFAULT_BORDER_WIDTH; + return Math.min(borderWidth, MAX_BORDER_WIDTH); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx new file mode 100644 index 000000000000..5338c08a6652 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx @@ -0,0 +1,273 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { RefObject } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; + +import { LegendItem } from '../../../../common/legend'; +import { ScreenReaderSummary } from '../../../../components/accessibility'; +import { onChartRendered } from '../../../../state/actions/chart'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { + A11ySettings, + DEFAULT_A11Y_SETTINGS, + getA11ySettingsSelector, +} from '../../../../state/selectors/get_accessibility_config'; +import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions'; +import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { Rotation } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { deepEqual } from '../../../../utils/fast_deep_equal'; +import { AnnotationId, AxisId } from '../../../../utils/ids'; +import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; +import { Theme, AxisStyle } from '../../../../utils/themes/theme'; +import { AnnotationDimensions } from '../../annotations/types'; +import { computeAnnotationDimensionsSelector } from '../../state/selectors/compute_annotations'; +import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; +import { computeChartTransformSelector } from '../../state/selectors/compute_chart_transform'; +import { computePerPanelGridLinesSelector } from '../../state/selectors/compute_grid_lines'; +import { computePanelsSelectors, PanelGeoms } from '../../state/selectors/compute_panels'; +import { + computePerPanelAxesGeomsSelector, + PerPanelAxisGeoms, +} from '../../state/selectors/compute_per_panel_axes_geoms'; +import { computeSeriesGeometriesSelector } from '../../state/selectors/compute_series_geometries'; +import { getAxesStylesSelector } from '../../state/selectors/get_axis_styles'; +import { getHighlightedSeriesSelector } from '../../state/selectors/get_highlighted_series'; +import { getAnnotationSpecsSelector, getAxisSpecsSelector } from '../../state/selectors/get_specs'; +import { isChartEmptySelector } from '../../state/selectors/is_chart_empty'; +import { Geometries, Transform } from '../../state/utils/types'; +import { LinesGrid } from '../../utils/grid_lines'; +import { IndexedGeometryMap } from '../../utils/indexed_geometry_map'; +import { AxisSpec, AnnotationSpec } from '../../utils/specs'; +import { renderXYChartCanvas2d } from './renderers'; + +/** @internal */ +export interface ReactiveChartStateProps { + initialized: boolean; + debug: boolean; + isChartEmpty: boolean; + geometries: Geometries; + geometriesIndex: IndexedGeometryMap; + theme: Theme; + chartContainerDimensions: Dimensions; + rotation: Rotation; + renderingArea: Dimensions; + chartTransform: Transform; + highlightedLegendItem?: LegendItem; + axesSpecs: AxisSpec[]; + perPanelAxisGeoms: Array; + perPanelGridLines: Array; + axesStyles: Map; + annotationDimensions: Map; + annotationSpecs: AnnotationSpec[]; + panelGeoms: PanelGeoms; + a11ySettings: A11ySettings; +} + +interface ReactiveChartDispatchProps { + onChartRendered: typeof onChartRendered; +} +interface ReactiveChartOwnProps { + forwardCanvasRef: RefObject; +} +const CLIPPING_MARGINS = 0.5; + +type XYChartProps = ReactiveChartStateProps & ReactiveChartDispatchProps & ReactiveChartOwnProps; +class XYChartComponent extends React.Component { + static displayName = 'XYChart'; + + private ctx: CanvasRenderingContext2D | null; + + // see example https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#Example + private readonly devicePixelRatio: number; // fixme this be no constant: multi-monitor window drag may necessitate modifying the `` dimensions + + constructor(props: Readonly) { + super(props); + this.ctx = null; + this.devicePixelRatio = window.devicePixelRatio; + } + + componentDidMount() { + /* + * the DOM element has just been appended, and getContext('2d') is always non-null, + * so we could use a couple of ! non-null assertions but no big plus + */ + this.tryCanvasContext(); + if (this.props.initialized) { + this.drawCanvas(); + this.props.onChartRendered(); + } + } + + shouldComponentUpdate(nextProps: ReactiveChartStateProps) { + return !deepEqual(this.props, nextProps); + } + + componentDidUpdate() { + if (!this.ctx) { + this.tryCanvasContext(); + } + if (this.props.initialized) { + this.drawCanvas(); + this.props.onChartRendered(); + } + } + + private drawCanvas() { + if (this.ctx) { + const { renderingArea, rotation } = this.props; + const clippings = { + x: -CLIPPING_MARGINS, + y: -CLIPPING_MARGINS, + width: ([90, -90].includes(rotation) ? renderingArea.height : renderingArea.width) + CLIPPING_MARGINS * 2, + height: ([90, -90].includes(rotation) ? renderingArea.width : renderingArea.height) + CLIPPING_MARGINS * 2, + }; + renderXYChartCanvas2d(this.ctx, this.devicePixelRatio, clippings, this.props); + } + } + + private tryCanvasContext() { + const canvas = this.props.forwardCanvasRef.current; + this.ctx = canvas && canvas.getContext('2d'); + } + + // eslint-disable-next-line @typescript-eslint/member-ordering + render() { + const { + forwardCanvasRef, + initialized, + isChartEmpty, + chartContainerDimensions: { width, height }, + a11ySettings, + } = this.props; + + if (!initialized || isChartEmpty) { + this.ctx = null; + return null; + } + + return ( +
+ + + +
+ ); + } +} + +const mapDispatchToProps = (dispatch: Dispatch): ReactiveChartDispatchProps => + bindActionCreators( + { + onChartRendered, + }, + dispatch, + ); + +const DEFAULT_PROPS: ReactiveChartStateProps = { + initialized: false, + debug: false, + isChartEmpty: true, + geometries: { + areas: [], + bars: [], + lines: [], + points: [], + bubbles: [], + }, + geometriesIndex: new IndexedGeometryMap(), + theme: LIGHT_THEME, + chartContainerDimensions: { + width: 0, + height: 0, + left: 0, + top: 0, + }, + rotation: 0 as const, + renderingArea: { + width: 0, + height: 0, + left: 0, + top: 0, + }, + chartTransform: { + x: 0, + y: 0, + rotate: 0, + }, + + axesSpecs: [], + perPanelAxisGeoms: [], + perPanelGridLines: [], + axesStyles: new Map(), + annotationDimensions: new Map(), + annotationSpecs: [], + panelGeoms: [], + a11ySettings: DEFAULT_A11Y_SETTINGS, +}; + +const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return DEFAULT_PROPS; + } + + const { geometries, geometriesIndex } = computeSeriesGeometriesSelector(state); + const { debug } = getSettingsSpecSelector(state); + + return { + initialized: true, + isChartEmpty: isChartEmptySelector(state), + debug, + geometries, + geometriesIndex, + theme: getChartThemeSelector(state), + chartContainerDimensions: getChartContainerDimensionsSelector(state), + highlightedLegendItem: getHighlightedSeriesSelector(state), + rotation: getChartRotationSelector(state), + renderingArea: computeChartDimensionsSelector(state).chartDimensions, + chartTransform: computeChartTransformSelector(state), + axesSpecs: getAxisSpecsSelector(state), + perPanelAxisGeoms: computePerPanelAxesGeomsSelector(state), + perPanelGridLines: computePerPanelGridLinesSelector(state), + axesStyles: getAxesStylesSelector(state), + annotationDimensions: computeAnnotationDimensionsSelector(state), + annotationSpecs: getAnnotationSpecsSelector(state), + panelGeoms: computePanelsSelectors(state), + a11ySettings: getA11ySettingsSelector(state), + }; +}; + +/** @internal */ +export const XYChart = connect(mapStateToProps, mapDispatchToProps)(XYChartComponent); diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_crosshair.scss b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_crosshair.scss new file mode 100644 index 000000000000..71bd90d8ae8a --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_crosshair.scss @@ -0,0 +1,8 @@ +.echCrosshair, +.echCrosshair__cursor, +.echCrosshair__crossLine { + position: absolute; + top: 0; + left: 0; + pointer-events: none; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_highlighter.scss b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_highlighter.scss new file mode 100644 index 000000000000..775cc040ac85 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_highlighter.scss @@ -0,0 +1,22 @@ +.echHighlighter { + position: absolute; + pointer-events: none; + top: 0; + bottom: 0; + left: 0; + right: 0; + width: 100%; + height: 100%; +} + +.echHighlighterOverlay__fill { + fill: transparentize($euiColorGhost, 0.8); +} + +.echHighlighterOverlay__stroke { + stroke: transparentize($euiColorGhost, 0.8); +} + +.echHighlighter__mask { + fill: transparentize($euiColorEmptyShade, 0.5); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_index.scss b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_index.scss new file mode 100644 index 000000000000..df6c1c310104 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_index.scss @@ -0,0 +1,4 @@ +@import 'highlighter'; +@import 'crosshair'; +@import 'screen_reader'; +@import 'annotations/index'; diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_screen_reader.scss b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_screen_reader.scss new file mode 100644 index 000000000000..0bd8bafaf95a --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_screen_reader.scss @@ -0,0 +1,8 @@ +.echScreenReaderOnly { + position: absolute; + left: -10000px; + top: auto; + width: 1px; + height: 1px; + overflow: hidden; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/_annotations.scss b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/_annotations.scss new file mode 100644 index 000000000000..1aa9a493188c --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/_annotations.scss @@ -0,0 +1,33 @@ +.echAnnotation { + position: absolute; + user-select: none; + font-size: $euiFontSizeXS; + font-weight: $euiFontWeightBold; + + &__tooltip { + @include euiToolTipStyle; + @include euiFontSizeXS; + padding: 0; + transition: opacity $euiAnimSpeedNormal; + pointer-events: none; + user-select: none; + max-width: 256px; + } + + &__header { + @include euiToolTipTitle; + padding: $euiSizeXS ($euiSizeXS * 2); + } + + &__details { + padding: $euiSizeXS ($euiSizeXS * 2); + } + + &__icon { + position: relative; + } + + &__body { + white-space: nowrap; + } +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/_index.scss b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/_index.scss new file mode 100644 index 000000000000..70a1b1086d21 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/_index.scss @@ -0,0 +1 @@ +@import 'annotations'; diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/annotation_tooltip.tsx b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/annotation_tooltip.tsx new file mode 100644 index 000000000000..d48982e3bbab --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/annotation_tooltip.tsx @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useCallback, useMemo, useEffect, RefObject } from 'react'; + +import { TooltipPortal, Placement, TooltipPortalSettings } from '../../../../../components/portal'; +import { AnnotationTooltipState } from '../../../annotations/types'; +import { TooltipContent } from './tooltip_content'; + +interface AnnotationTooltipProps { + state: AnnotationTooltipState | null; + chartRef: RefObject; + chartId: string; + zIndex: number; + onScroll?: () => void; +} + +/** @internal */ +export const AnnotationTooltip = ({ state, chartRef, chartId, onScroll, zIndex }: AnnotationTooltipProps) => { + const renderTooltip = useCallback(() => { + if (!state || !state.isVisible) { + return null; + } + + return ; + }, [state]); + + const handleScroll = () => { + // TODO: handle scroll cursor update + if (onScroll) { + onScroll(); + } + }; + + useEffect(() => { + if (onScroll) { + window.addEventListener('scroll', handleScroll, true); + return () => window.removeEventListener('scroll', handleScroll, true); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const popperSettings = useMemo((): TooltipPortalSettings | undefined => { + const settings = state?.tooltipSettings; + if (!settings) { + return; + } + + const { placement, boundary, ...rest } = settings; + + return { + ...rest, + placement: placement ?? Placement.Right, + boundary: boundary === 'chart' ? chartRef.current ?? undefined : boundary, + }; + }, [state?.tooltipSettings, chartRef]); + + const position = useMemo(() => state?.anchor ?? null, [state?.anchor]); + if (!state?.isVisible) { + return null; + } + return ( + + {renderTooltip()} + + ); +}; diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/annotations.tsx b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/annotations.tsx new file mode 100644 index 000000000000..84d9d7263a23 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/annotations.tsx @@ -0,0 +1,195 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { RefObject, useCallback } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; + +import { + onDOMElementEnter as onDOMElementEnterAction, + onDOMElementLeave as onDOMElementLeaveAction, +} from '../../../../../state/actions/dom_element'; +import { onPointerMove as onPointerMoveAction } from '../../../../../state/actions/mouse'; +import { GlobalChartState, BackwardRef } from '../../../../../state/chart_state'; +import { + getInternalIsInitializedSelector, + InitStatus, +} from '../../../../../state/selectors/get_internal_is_intialized'; +import { Dimensions } from '../../../../../utils/dimensions'; +import { AnnotationId } from '../../../../../utils/ids'; +import { AnnotationLineProps } from '../../../annotations/line/types'; +import { AnnotationDimensions, AnnotationTooltipState } from '../../../annotations/types'; +import { computeAnnotationDimensionsSelector } from '../../../state/selectors/compute_annotations'; +import { computeChartDimensionsSelector } from '../../../state/selectors/compute_chart_dimensions'; +import { getAnnotationTooltipStateSelector } from '../../../state/selectors/get_annotation_tooltip_state'; +import { getAnnotationSpecsSelector } from '../../../state/selectors/get_specs'; +import { isChartEmptySelector } from '../../../state/selectors/is_chart_empty'; +import { getSpecsById } from '../../../state/utils/spec'; +import { isLineAnnotation, AnnotationSpec } from '../../../utils/specs'; +import { AnnotationTooltip } from './annotation_tooltip'; +import { LineMarker } from './line_marker'; + +interface AnnotationsDispatchProps { + onPointerMove: typeof onPointerMoveAction; + onDOMElementEnter: typeof onDOMElementEnterAction; + onDOMElementLeave: typeof onDOMElementLeaveAction; +} + +interface AnnotationsStateProps { + isChartEmpty: boolean; + tooltipState: AnnotationTooltipState | null; + chartDimensions: Dimensions; + annotationDimensions: Map; + annotationSpecs: AnnotationSpec[]; + chartId: string; + zIndex: number; +} + +interface AnnotationsOwnProps { + getChartContainerRef: BackwardRef; + chartAreaRef: RefObject; +} + +type AnnotationsProps = AnnotationsDispatchProps & AnnotationsStateProps & AnnotationsOwnProps; + +function renderAnnotationLineMarkers( + chartAreaRef: RefObject, + chartDimensions: Dimensions, + annotationLines: AnnotationLineProps[], + onDOMElementEnter: typeof onDOMElementEnterAction, + onDOMElementLeave: typeof onDOMElementLeaveAction, +) { + return annotationLines.reduce((acc, props: AnnotationLineProps) => { + if (props.markers.length === 0) { + return acc; + } + + acc.push( + , + ); + + return acc; + }, []); +} +const AnnotationsComponent = ({ + tooltipState, + isChartEmpty, + chartDimensions, + annotationSpecs, + annotationDimensions, + getChartContainerRef, + chartAreaRef, + chartId, + zIndex, + onPointerMove, + onDOMElementEnter, + onDOMElementLeave, +}: AnnotationsProps) => { + const renderAnnotationMarkers = useCallback((): JSX.Element[] => { + const markers: JSX.Element[] = []; + + annotationDimensions.forEach((dimensions: AnnotationDimensions, id: AnnotationId) => { + const annotationSpec = getSpecsById(annotationSpecs, id); + if (!annotationSpec) { + return; + } + + if (isLineAnnotation(annotationSpec)) { + const annotationLines = dimensions as AnnotationLineProps[]; + const lineMarkers = renderAnnotationLineMarkers( + chartAreaRef, + chartDimensions, + annotationLines, + onDOMElementEnter, + onDOMElementLeave, + ); + markers.push(...lineMarkers); + } + }); + + return markers; + }, [annotationDimensions, annotationSpecs, chartAreaRef, chartDimensions, onDOMElementEnter, onDOMElementLeave]); + + const onScroll = useCallback(() => { + onPointerMove({ x: -1, y: -1 }, Date.now()); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + if (isChartEmpty) { + return null; + } + + return ( + <> + {renderAnnotationMarkers()} + + + ); +}; + +AnnotationsComponent.displayName = 'Annotations'; + +const mapDispatchToProps = (dispatch: Dispatch): AnnotationsDispatchProps => + bindActionCreators( + { + onPointerMove: onPointerMoveAction, + onDOMElementLeave: onDOMElementLeaveAction, + onDOMElementEnter: onDOMElementEnterAction, + }, + dispatch, + ); + +const mapStateToProps = (state: GlobalChartState): AnnotationsStateProps => { + const { zIndex, chartId } = state; + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return { + isChartEmpty: true, + chartDimensions: { top: 0, left: 0, width: 0, height: 0 }, + annotationDimensions: new Map(), + annotationSpecs: [], + tooltipState: null, + chartId, + zIndex, + }; + } + return { + isChartEmpty: isChartEmptySelector(state), + chartDimensions: computeChartDimensionsSelector(state).chartDimensions, + annotationDimensions: computeAnnotationDimensionsSelector(state), + annotationSpecs: getAnnotationSpecsSelector(state), + tooltipState: getAnnotationTooltipStateSelector(state), + chartId, + zIndex, + }; +}; + +/** @internal */ +export const Annotations = connect(mapStateToProps, mapDispatchToProps)(AnnotationsComponent); diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/index.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/index.ts new file mode 100644 index 000000000000..b6b5e126cf66 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export { Annotations } from './annotations'; diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/line_marker.tsx b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/line_marker.tsx new file mode 100644 index 000000000000..82b5d34c31f3 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/line_marker.tsx @@ -0,0 +1,143 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createPopper, Instance } from '@popperjs/core'; +import React, { RefObject, useRef, useEffect, useCallback } from 'react'; + +import { + DOMElementType, + onDOMElementEnter as onDOMElementEnterAction, + onDOMElementLeave as onDOMElementLeaveAction, +} from '../../../../../state/actions/dom_element'; +import { Position, renderWithProps } from '../../../../../utils/common'; +import { Dimensions } from '../../../../../utils/dimensions'; +import { AnnotationLineProps } from '../../../annotations/line/types'; + +type LineMarkerProps = Pick & { + chartAreaRef: RefObject; + chartDimensions: Dimensions; + onDOMElementEnter: typeof onDOMElementEnterAction; + onDOMElementLeave: typeof onDOMElementLeaveAction; +}; + +const MARKER_TRANSFORMS = { + [Position.Right]: 'translate(0%, -50%)', + [Position.Left]: 'translate(-100%, -50%)', + [Position.Top]: 'translate(-50%, -100%)', + [Position.Bottom]: 'translate(-50%, 0%)', +}; + +function getMarkerCentredTransform(alignment: Position, hasMarkerDimensions: boolean): string | undefined { + return hasMarkerDimensions ? undefined : MARKER_TRANSFORMS[alignment]; +} + +/** + * LineMarker component used to display line annotation markers + * @internal + */ +export function LineMarker({ + id, + specId, + datum, + panel, + markers: [{ icon, body, color, position, alignment, dimension }], + chartAreaRef, + chartDimensions, + onDOMElementEnter, + onDOMElementLeave, +}: LineMarkerProps) { + const iconRef = useRef(null); + const testRef = useRef(null); + const popper = useRef(null); + const style = { + color, + top: chartDimensions.top + position.top + panel.top, + left: chartDimensions.left + position.left + panel.left, + }; + const transform = { transform: getMarkerCentredTransform(alignment, Boolean(dimension)) }; + + const setPopper = useCallback(() => { + if (!iconRef.current || !testRef.current) return; + + popper.current = createPopper(iconRef.current, testRef.current, { + strategy: 'absolute', + placement: alignment, + modifiers: [ + { + name: 'offset', + options: { + offset: [0, 0], + }, + }, + { + name: 'preventOverflow', + options: { + boundary: chartAreaRef.current, + }, + }, + { + name: 'flip', + options: { + // prevents default flip modifier + fallbackPlacements: [], + }, + }, + ], + }); + }, [chartAreaRef, alignment]); + + useEffect(() => { + if (!popper.current && body) { + setPopper(); + } + + return () => { + popper?.current?.destroy?.(); + popper.current = null; + }; + }, [setPopper, body]); + + void popper?.current?.update?.(); + + return ( +
{ + onDOMElementEnter({ + createdBySpecId: specId, + id, + type: DOMElementType.LineAnnotationMarker, + datum, + }); + }} + onMouseLeave={onDOMElementLeave} + style={{ ...style, ...transform }} + > +
+ {renderWithProps(icon, datum)} +
+ {body && ( +
+ {renderWithProps(body, datum)} +
+ )} +
+ ); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/tooltip_content.tsx b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/tooltip_content.tsx new file mode 100644 index 000000000000..cdd05223a333 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/annotations/tooltip_content.tsx @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useCallback } from 'react'; + +import { AnnotationType, LineAnnotationDatum, RectAnnotationDatum } from '../../../../specs'; +import { AnnotationTooltipState } from '../../../annotations/types'; + +/** @internal */ +export const TooltipContent = ({ + annotationType, + datum, + customTooltip: CustomTooltip, + customTooltipDetails, +}: AnnotationTooltipState) => { + const renderLine = useCallback(() => { + const { details, dataValue, header = dataValue.toString() } = datum as LineAnnotationDatum; + return ( +
+

{header}

+
{customTooltipDetails ? customTooltipDetails(details) : details}
+
+ ); + }, [datum, customTooltipDetails]); + + const renderRect = useCallback(() => { + const { details } = datum as RectAnnotationDatum; + const tooltipContent = customTooltipDetails ? customTooltipDetails(details) : details; + if (!tooltipContent) { + return null; + } + + return ( +
+
+
{tooltipContent}
+
+
+ ); + }, [datum, customTooltipDetails]); + + if (CustomTooltip) { + const { details } = datum; + if ('header' in datum) { + return ; + } + return ; + } + + switch (annotationType) { + case AnnotationType.Line: { + return renderLine(); + } + case AnnotationType.Rectangle: { + return renderRect(); + } + default: + return null; + } +}; diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/crosshair.tsx b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/crosshair.tsx new file mode 100644 index 000000000000..6272f14e06d8 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/crosshair.tsx @@ -0,0 +1,154 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { connect } from 'react-redux'; + +import { Line, Rect } from '../../../../geoms/types'; +import { getTooltipType } from '../../../../specs'; +import { TooltipType } from '../../../../specs/constants'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { Rotation } from '../../../../utils/common'; +import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; +import { Theme } from '../../../../utils/themes/theme'; +import { getCursorBandPositionSelector } from '../../state/selectors/get_cursor_band'; +import { getCursorLinePositionSelector } from '../../state/selectors/get_cursor_line'; + +interface CrosshairProps { + theme: Theme; + chartRotation: Rotation; + cursorPosition?: Rect; + cursorCrossLinePosition?: Line; + tooltipType: TooltipType; + fromExternalEvent?: boolean; + zIndex: number; +} + +function canRenderBand(type: TooltipType, visible: boolean, fromExternalEvent?: boolean) { + return visible && (type === TooltipType.Crosshairs || type === TooltipType.VerticalCursor || fromExternalEvent); +} + +function canRenderHelpLine(type: TooltipType, visible: boolean) { + return visible && type === TooltipType.Crosshairs; +} + +class CrosshairComponent extends React.Component { + static displayName = 'Crosshair'; + + renderCursor() { + const { + zIndex, + theme: { + crosshair: { band, line }, + }, + cursorPosition, + tooltipType, + fromExternalEvent, + } = this.props; + + if (!cursorPosition || !canRenderBand(tooltipType, band.visible, fromExternalEvent)) { + return null; + } + const { x, y, width, height } = cursorPosition; + const isLine = width === 0 || height === 0; + const { strokeWidth, stroke, dash } = line; + const { fill } = band; + const strokeDasharray = (dash ?? []).join(' '); + return ( + + {isLine && } + {!isLine && } + + ); + } + + renderCrossLine() { + const { + zIndex, + theme: { + crosshair: { crossLine }, + }, + cursorCrossLinePosition, + tooltipType, + } = this.props; + + if (!cursorCrossLinePosition || !canRenderHelpLine(tooltipType, crossLine.visible)) { + return null; + } + + const { strokeWidth, stroke, dash } = crossLine; + const style = { + strokeWidth, + stroke, + strokeDasharray: (dash ?? []).join(' '), + }; + + return ( + + + + ); + } + + render() { + return ( + <> + {this.renderCursor()} + {this.renderCrossLine()} + + ); + } +} + +const mapStateToProps = (state: GlobalChartState): CrosshairProps => { + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return { + theme: LIGHT_THEME, + chartRotation: 0, + tooltipType: TooltipType.None, + zIndex: 0, + }; + } + const settings = getSettingsSpecSelector(state); + const cursorBandPosition = getCursorBandPositionSelector(state); + const fromExternalEvent = cursorBandPosition?.fromExternalEvent; + const tooltipType = getTooltipType(settings, fromExternalEvent); + + return { + theme: getChartThemeSelector(state), + chartRotation: getChartRotationSelector(state), + cursorPosition: cursorBandPosition, + cursorCrossLinePosition: getCursorLinePositionSelector(state), + tooltipType, + fromExternalEvent, + zIndex: state.zIndex, + }; +}; + +/** @internal */ +export const Crosshair = connect(mapStateToProps)(CrosshairComponent); diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx new file mode 100644 index 000000000000..8d87cdbc06d9 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx @@ -0,0 +1,152 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { connect } from 'react-redux'; + +import { RGBtoString } from '../../../../common/color_library_wrappers'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation'; +import { InitStatus, getInternalIsInitializedSelector } from '../../../../state/selectors/get_internal_is_intialized'; +import { Rotation } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { isPointGeometry, IndexedGeometry, PointGeometry } from '../../../../utils/geometry'; +import { DEFAULT_HIGHLIGHT_PADDING } from '../../rendering/constants'; +import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; +import { computeChartTransformSelector } from '../../state/selectors/compute_chart_transform'; +import { getHighlightedGeomsSelector } from '../../state/selectors/get_tooltip_values_highlighted_geoms'; +import { Transform } from '../../state/utils/types'; +import { computeChartTransform } from '../../state/utils/utils'; +import { ShapeRendererFn } from '../shapes_paths'; + +interface HighlighterProps { + initialized: boolean; + chartId: string; + zIndex: number; + highlightedGeometries: IndexedGeometry[]; + chartTransform: Transform; + chartDimensions: Dimensions; + chartRotation: Rotation; +} + +function getTransformForPanel(panel: Dimensions, rotation: Rotation, { left, top }: Pick) { + const { x, y } = computeChartTransform(panel, rotation); + return `translate(${left + panel.left + x}, ${top + panel.top + y}) rotate(${rotation})`; +} + +function renderPath(geom: PointGeometry) { + // keep the highlighter radius to a minimum + const radius = Math.max(geom.radius, DEFAULT_HIGHLIGHT_PADDING); + const [shapeFn, rotate] = ShapeRendererFn[geom.style.shape]; + return { + d: shapeFn(radius), + rotate, + }; +} + +class HighlighterComponent extends React.Component { + static displayName = 'Highlighter'; + + render() { + const { highlightedGeometries, chartDimensions, chartRotation, chartId, zIndex } = this.props; + const clipWidth = [90, -90].includes(chartRotation) ? chartDimensions.height : chartDimensions.width; + const clipHeight = [90, -90].includes(chartRotation) ? chartDimensions.width : chartDimensions.height; + const clipPathId = `echHighlighterClipPath__${chartId}`; + return ( + + + + + + + + {highlightedGeometries.map((geom, i) => { + const { panel } = geom; + const x = geom.x + geom.transform.x; + const y = geom.y + geom.transform.y; + const geomTransform = getTransformForPanel(panel, chartRotation, chartDimensions); + + if (isPointGeometry(geom)) { + const { color } = geom.style.stroke; + const { d, rotate } = renderPath(geom); + return ( + + + + ); + } + return ( + + ); + })} + + ); + } +} + +const mapStateToProps = (state: GlobalChartState): HighlighterProps => { + const { chartId, zIndex } = state; + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return { + initialized: false, + chartId, + zIndex, + highlightedGeometries: [], + chartTransform: { + x: 0, + y: 0, + rotate: 0, + }, + chartDimensions: { top: 0, left: 0, width: 0, height: 0 }, + chartRotation: 0, + }; + } + + return { + initialized: true, + chartId, + zIndex, + highlightedGeometries: getHighlightedGeomsSelector(state), + chartTransform: computeChartTransformSelector(state), + chartDimensions: computeChartDimensionsSelector(state).chartDimensions, + chartRotation: getChartRotationSelector(state), + }; +}; + +/** @internal */ +export const Highlighter = connect(mapStateToProps)(HighlighterComponent); diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/shapes_paths.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/shapes_paths.ts new file mode 100644 index 000000000000..fbd58036fbcf --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/shapes_paths.ts @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PointShape, TextureShape } from '../../../utils/themes/theme'; + +/** @internal */ +export type SVGPath = string; + +/** @internal */ +export type SVGPathFn = (radius: number) => SVGPath; + +/** @internal */ +export const cross: SVGPathFn = (r: number) => { + return `M ${-r} 0 L ${r} 0 M 0 ${r} L 0 ${-r}`; +}; + +/** @internal */ +export const triangle: SVGPathFn = (r: number) => { + const h = (r * Math.sqrt(3)) / 2; + const hr = r / 2; + return `M ${-h} ${hr} L ${h} ${hr} L 0 ${-r} Z`; +}; + +/** @internal */ +export const square: SVGPathFn = (r: number) => { + return `M ${-r} ${-r} L ${-r} ${r} L ${r} ${r} L ${r} ${-r} Z`; +}; + +/** @internal */ +export const circle: SVGPathFn = (r: number) => { + return `M ${-r} 0 a ${r},${r} 0 1,0 ${r * 2},0 a ${r},${r} 0 1,0 ${-r * 2},0`; +}; + +/** @internal */ +export const line: SVGPathFn = (r: number) => { + return `M 0 ${-r} l 0 ${r * 2}`; +}; + +/** @internal */ +export const ShapeRendererFn: Record = { + [PointShape.Circle]: [circle, 0], + [PointShape.X]: [cross, 45], + [PointShape.Plus]: [cross, 0], + [PointShape.Diamond]: [square, 45], + [PointShape.Square]: [square, 0], + [PointShape.Triangle]: [triangle, 0], +}; + +/** @internal */ +export const TextureRendererFn: Record = { + ...ShapeRendererFn, + [TextureShape.Line]: [line, 0], +}; diff --git a/packages/osd-charts/src/chart_types/xy_chart/rendering/area.ts b/packages/osd-charts/src/chart_types/xy_chart/rendering/area.ts new file mode 100644 index 000000000000..97c3ad9ee69d --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/rendering/area.ts @@ -0,0 +1,156 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { area } from 'd3-shape'; + +import { Scale } from '../../../scales'; +import { Color } from '../../../utils/common'; +import { CurveType, getCurveFactory } from '../../../utils/curves'; +import { Dimensions } from '../../../utils/dimensions'; +import { AreaGeometry } from '../../../utils/geometry'; +import { AreaSeriesStyle } from '../../../utils/themes/theme'; +import { IndexedGeometryMap } from '../utils/indexed_geometry_map'; +import { DataSeries, DataSeriesDatum } from '../utils/series'; +import { PointStyleAccessor } from '../utils/specs'; +import { renderPoints } from './points'; +import { + getClippedRanges, + getY0ScaledValueOrThrowFn, + getY1ScaledValueOrThrowFn, + getYDatumValueFn, + isYValueDefinedFn, + MarkSizeOptions, +} from './utils'; + +/** @internal */ +export function renderArea( + shift: number, + dataSeries: DataSeries, + xScale: Scale, + yScale: Scale, + panel: Dimensions, + color: Color, + curve: CurveType, + hasY0Accessors: boolean, + xScaleOffset: number, + seriesStyle: AreaSeriesStyle, + markSizeOptions: MarkSizeOptions, + isStacked = false, + pointStyleAccessor?: PointStyleAccessor, + hasFit?: boolean, +): { + areaGeometry: AreaGeometry; + indexedGeometryMap: IndexedGeometryMap; +} { + const y1Fn = getY1ScaledValueOrThrowFn(yScale); + const y0Fn = getY0ScaledValueOrThrowFn(yScale); + const definedFn = isYValueDefinedFn(yScale, xScale); + const y1DatumAccessor = getYDatumValueFn(); + const y0DatumAccessor = getYDatumValueFn('y0'); + const pathGenerator = area() + .x(({ x }) => xScale.scaleOrThrow(x) - xScaleOffset) + .y1(y1Fn) + .y0(y0Fn) + .defined((datum) => { + return definedFn(datum, y1DatumAccessor) && (hasY0Accessors ? definedFn(datum, y0DatumAccessor) : true); + }) + .curve(getCurveFactory(curve)); + + const clippedRanges = getClippedRanges(dataSeries.data, xScale, xScaleOffset); + + let y1Line: string | null; + + try { + y1Line = pathGenerator.lineY1()(dataSeries.data); + } catch { + // When values are not scalable + y1Line = null; + } + + const lines: string[] = []; + if (y1Line) { + lines.push(y1Line); + } + if (hasY0Accessors) { + let y0Line: string | null; + + try { + y0Line = pathGenerator.lineY0()(dataSeries.data); + } catch { + // When values are not scalable + y0Line = null; + } + if (y0Line) { + lines.push(y0Line); + } + } + + const { pointGeometries, indexedGeometryMap } = renderPoints( + shift - xScaleOffset, + dataSeries, + xScale, + yScale, + panel, + color, + seriesStyle.point, + hasY0Accessors, + markSizeOptions, + pointStyleAccessor, + false, + ); + + let areaPath: string; + + try { + areaPath = pathGenerator(dataSeries.data) || ''; + } catch { + // When values are not scalable + areaPath = ''; + } + + const areaGeometry: AreaGeometry = { + area: areaPath, + lines, + points: pointGeometries, + color, + transform: { + y: 0, + x: shift, + }, + seriesIdentifier: { + key: dataSeries.key, + specId: dataSeries.specId, + yAccessor: dataSeries.yAccessor, + splitAccessors: dataSeries.splitAccessors, + seriesKeys: dataSeries.seriesKeys, + smHorizontalAccessorValue: dataSeries.smHorizontalAccessorValue, + smVerticalAccessorValue: dataSeries.smVerticalAccessorValue, + }, + seriesAreaStyle: seriesStyle.area, + seriesAreaLineStyle: seriesStyle.line, + seriesPointStyle: seriesStyle.point, + isStacked, + clippedRanges, + hideClippedRanges: !hasFit, + }; + return { + areaGeometry, + indexedGeometryMap, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/rendering/bars.ts b/packages/osd-charts/src/chart_types/xy_chart/rendering/bars.ts new file mode 100644 index 000000000000..c37652bf7b9c --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/rendering/bars.ts @@ -0,0 +1,296 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Scale } from '../../../scales'; +import { ScaleType } from '../../../scales/constants'; +import { CanvasTextBBoxCalculator } from '../../../utils/bbox/canvas_text_bbox_calculator'; +import { clamp, Color, mergePartial } from '../../../utils/common'; +import { Dimensions } from '../../../utils/dimensions'; +import { BandedAccessorType, BarGeometry } from '../../../utils/geometry'; +import { BarSeriesStyle, DisplayValueStyle } from '../../../utils/themes/theme'; +import { IndexedGeometryMap } from '../utils/indexed_geometry_map'; +import { DataSeries, DataSeriesDatum, XYChartSeriesIdentifier } from '../utils/series'; +import { BarStyleAccessor, DisplayValueSpec, StackMode } from '../utils/specs'; + +/** @internal */ +export function renderBars( + orderIndex: number, + dataSeries: DataSeries, + xScale: Scale, + yScale: Scale, + panel: Dimensions, + color: Color, + sharedSeriesStyle: BarSeriesStyle, + displayValueSettings?: DisplayValueSpec, + styleAccessor?: BarStyleAccessor, + minBarHeight: number = 0, + stackMode?: StackMode, + chartRotation?: number, +): { + barGeometries: BarGeometry[]; + indexedGeometryMap: IndexedGeometryMap; +} { + const indexedGeometryMap = new IndexedGeometryMap(); + const barGeometries: BarGeometry[] = []; + + const bboxCalculator = new CanvasTextBBoxCalculator(); + + // default padding to 1 for now + const padding = 1; + const { fontSize, fontFamily } = sharedSeriesStyle.displayValue; + + dataSeries.data.forEach((datum) => { + const { y0, y1, initialY1, filled } = datum; + // don't create a bar if the initialY1 value is null. + if (y1 === null || initialY1 === null || (filled && filled.y1 !== undefined)) { + return; + } + // don't create a bar if not within the xScale domain + if (!xScale.isValueInDomain(datum.x)) { + return; + } + + let y: number | null; + let y0Scaled; + if (yScale.type === ScaleType.Log) { + y = y1 === 0 || y1 === null ? yScale.range[0] : yScale.scale(y1); + if (yScale.isInverted) { + y0Scaled = y0 === 0 || y0 === null ? yScale.range[1] : yScale.scale(y0); + } else { + y0Scaled = y0 === 0 || y0 === null ? yScale.range[0] : yScale.scale(y0); + } + } else { + y = yScale.scale(y1); + // use always zero as baseline if y0 is null + y0Scaled = y0 === null ? yScale.scale(0) : yScale.scale(y0); + } + + if (y === null || y0Scaled === null) { + return; + } + + const absMinHeight = Math.abs(minBarHeight); + let height = y0Scaled - y; + if (absMinHeight !== undefined && height !== 0 && Math.abs(height) < absMinHeight) { + const heightDelta = absMinHeight - Math.abs(height); + if (height < 0) { + height = -absMinHeight; + y += heightDelta; + } else { + height = absMinHeight; + y -= heightDelta; + } + } + const isUpsideDown = height < 0; + height = Math.abs(height); + y = isUpsideDown ? y - height : y; + + const xScaled = xScale.scale(datum.x); + + if (xScaled === null) { + return; + } + + const seriesIdentifier: XYChartSeriesIdentifier = { + key: dataSeries.key, + specId: dataSeries.specId, + yAccessor: dataSeries.yAccessor, + splitAccessors: dataSeries.splitAccessors, + seriesKeys: dataSeries.seriesKeys, + smHorizontalAccessorValue: dataSeries.smHorizontalAccessorValue, + smVerticalAccessorValue: dataSeries.smVerticalAccessorValue, + }; + + const seriesStyle = getBarStyleOverrides(datum, seriesIdentifier, sharedSeriesStyle, styleAccessor); + + const maxPixelWidth = clamp(seriesStyle.rect.widthRatio ?? 1, 0, 1) * xScale.bandwidth; + const minPixelWidth = clamp(seriesStyle.rect.widthPixel ?? 0, 0, maxPixelWidth); + + const width = clamp(seriesStyle.rect.widthPixel ?? xScale.bandwidth, minPixelWidth, maxPixelWidth); + const x = xScaled + xScale.bandwidth * orderIndex + xScale.bandwidth / 2 - width / 2; + + const originalY1Value = stackMode === StackMode.Percentage ? y1 - (y0 ?? 0) : initialY1; + const formattedDisplayValue = + displayValueSettings && displayValueSettings.valueFormatter + ? displayValueSettings.valueFormatter(originalY1Value) + : undefined; + + // only show displayValue for even bars if showOverlappingValue + const displayValueText = + displayValueSettings && displayValueSettings.isAlternatingValueLabel && barGeometries.length % 2 + ? undefined + : formattedDisplayValue; + + const { displayValueWidth, fixedFontScale } = computeBoxWidth( + displayValueText || '', + { padding, fontSize, fontFamily, bboxCalculator, width }, + displayValueSettings, + ); + + const isHorizontalRotation = chartRotation == null || [0, 180].includes(chartRotation); + // Take 70% of space for the label text + const fontSizeFactor = 0.7; + // Pick the right side of the label's box to use as factor reference + const referenceWidth = Math.max(isHorizontalRotation ? displayValueWidth : fixedFontScale, 1); + + const textScalingFactor = getFinalFontScalingFactor( + (width * fontSizeFactor) / referenceWidth, + fixedFontScale, + fontSize, + ); + + const hideClippedValue = displayValueSettings ? displayValueSettings.hideClippedValue : undefined; + // Based on rotation scale the width of the text box + const bboxWidthFactor = isHorizontalRotation ? textScalingFactor : 1; + + const displayValue = + displayValueSettings && displayValueSettings.showValueLabel + ? { + fontScale: textScalingFactor, + fontSize: fixedFontScale, + text: displayValueText, + width: bboxWidthFactor * displayValueWidth, + height: textScalingFactor * fixedFontScale, + hideClippedValue, + isValueContainedInElement: displayValueSettings.isValueContainedInElement, + } + : undefined; + + const barGeometry: BarGeometry = { + displayValue, + x, + y, + transform: { + x: 0, + y: 0, + }, + width, + height, + color, + value: { + x: datum.x, + y: originalY1Value, + mark: null, + accessor: BandedAccessorType.Y1, + datum: datum.datum, + }, + seriesIdentifier, + seriesStyle, + panel, + }; + indexedGeometryMap.set(barGeometry); + barGeometries.push(barGeometry); + }); + + bboxCalculator.destroy(); + + return { + barGeometries, + indexedGeometryMap, + }; +} + +/** + * Workout the text box size and fixedFontSize based on a collection of options + * @internal + */ +function computeBoxWidth( + text: string, + { + padding, + fontSize, + fontFamily, + bboxCalculator, + width, + }: { + padding: number; + fontSize: number | { min: number; max: number }; + fontFamily: string; + bboxCalculator: CanvasTextBBoxCalculator; + width: number; + }, + displayValueSettings: DisplayValueSpec | undefined, +): { fixedFontScale: number; displayValueWidth: number } { + const fixedFontScale = Math.max(typeof fontSize === 'number' ? fontSize : fontSize.min, 1); + + const computedDisplayValueWidth = bboxCalculator.compute(text || '', padding, fixedFontScale, fontFamily).width; + if (typeof fontSize !== 'number') { + return { + fixedFontScale, + displayValueWidth: computedDisplayValueWidth, + }; + } + return { + fixedFontScale, + displayValueWidth: + displayValueSettings && displayValueSettings.isValueContainedInElement ? width : computedDisplayValueWidth, + }; +} + +/** + * Returns a safe scaling factor for label text for fixed or range size inputs + * @internal + */ +function getFinalFontScalingFactor( + scale: number, + fixedFontSize: number, + limits: DisplayValueStyle['fontSize'], +): number { + if (typeof limits === 'number') { + // it's a fixed size, so it's always ok + return 1; + } + const finalFontSize = scale * fixedFontSize; + if (finalFontSize > limits.max) { + return limits.max / fixedFontSize; + } + if (finalFontSize < limits.min) { + // it's technically 1, but keep it generic in case the fixedFontSize changes + return limits.min / fixedFontSize; + } + return scale; +} + +/** @internal */ +export function getBarStyleOverrides( + datum: DataSeriesDatum, + seriesIdentifier: XYChartSeriesIdentifier, + seriesStyle: BarSeriesStyle, + styleAccessor?: BarStyleAccessor, +): BarSeriesStyle { + const styleOverride = styleAccessor && styleAccessor(datum, seriesIdentifier); + + if (!styleOverride) { + return seriesStyle; + } + + if (typeof styleOverride === 'string') { + return { + ...seriesStyle, + rect: { + ...seriesStyle.rect, + fill: styleOverride, + }, + }; + } + + return mergePartial(seriesStyle, styleOverride, { + mergeOptionalPartialValues: true, + }); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/rendering/bubble.ts b/packages/osd-charts/src/chart_types/xy_chart/rendering/bubble.ts new file mode 100644 index 000000000000..5ad7de04cf55 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/rendering/bubble.ts @@ -0,0 +1,81 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Scale } from '../../../scales'; +import { Color } from '../../../utils/common'; +import { Dimensions } from '../../../utils/dimensions'; +import { BubbleGeometry } from '../../../utils/geometry'; +import { BubbleSeriesStyle } from '../../../utils/themes/theme'; +import { IndexedGeometryMap } from '../utils/indexed_geometry_map'; +import { DataSeries } from '../utils/series'; +import { PointStyleAccessor } from '../utils/specs'; +import { renderPoints } from './points'; +import { MarkSizeOptions } from './utils'; + +/** @internal */ +export function renderBubble( + shift: number, + dataSeries: DataSeries, + xScale: Scale, + yScale: Scale, + color: Color, + panel: Dimensions, + hasY0Accessors: boolean, + xScaleOffset: number, + seriesStyle: BubbleSeriesStyle, + markSizeOptions: MarkSizeOptions, + isMixedChart: boolean, + pointStyleAccessor?: PointStyleAccessor, +): { + bubbleGeometry: BubbleGeometry; + indexedGeometryMap: IndexedGeometryMap; +} { + const { pointGeometries, indexedGeometryMap } = renderPoints( + shift - xScaleOffset, + dataSeries, + xScale, + yScale, + panel, + color, + seriesStyle.point, + hasY0Accessors, + markSizeOptions, + pointStyleAccessor, + !isMixedChart, + ); + + const bubbleGeometry = { + points: pointGeometries, + color, + seriesIdentifier: { + key: dataSeries.key, + specId: dataSeries.specId, + yAccessor: dataSeries.yAccessor, + splitAccessors: dataSeries.splitAccessors, + seriesKeys: dataSeries.seriesKeys, + smHorizontalAccessorValue: dataSeries.smHorizontalAccessorValue, + smVerticalAccessorValue: dataSeries.smVerticalAccessorValue, + }, + seriesPointStyle: seriesStyle.point, + }; + return { + bubbleGeometry, + indexedGeometryMap, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/rendering/constants.ts b/packages/osd-charts/src/chart_types/xy_chart/rendering/constants.ts new file mode 100644 index 000000000000..02dcd8492665 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/rendering/constants.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const DEFAULT_HIGHLIGHT_PADDING = 10; diff --git a/packages/osd-charts/src/chart_types/xy_chart/rendering/line.ts b/packages/osd-charts/src/chart_types/xy_chart/rendering/line.ts new file mode 100644 index 000000000000..7fe19d71c2dc --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/rendering/line.ts @@ -0,0 +1,120 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { line } from 'd3-shape'; + +import { Scale } from '../../../scales'; +import { Color } from '../../../utils/common'; +import { CurveType, getCurveFactory } from '../../../utils/curves'; +import { Dimensions } from '../../../utils/dimensions'; +import { LineGeometry } from '../../../utils/geometry'; +import { LineSeriesStyle } from '../../../utils/themes/theme'; +import { IndexedGeometryMap } from '../utils/indexed_geometry_map'; +import { DataSeries, DataSeriesDatum } from '../utils/series'; +import { PointStyleAccessor } from '../utils/specs'; +import { renderPoints } from './points'; +import { + getClippedRanges, + getY1ScaledValueOrThrowFn, + getYDatumValueFn, + isYValueDefinedFn, + MarkSizeOptions, +} from './utils'; + +/** @internal */ +export function renderLine( + shift: number, + dataSeries: DataSeries, + xScale: Scale, + yScale: Scale, + panel: Dimensions, + color: Color, + curve: CurveType, + hasY0Accessors: boolean, + xScaleOffset: number, + seriesStyle: LineSeriesStyle, + markSizeOptions: MarkSizeOptions, + pointStyleAccessor?: PointStyleAccessor, + hasFit?: boolean, +): { + lineGeometry: LineGeometry; + indexedGeometryMap: IndexedGeometryMap; +} { + const y1Fn = getY1ScaledValueOrThrowFn(yScale); + const definedFn = isYValueDefinedFn(yScale, xScale); + const y1Accessor = getYDatumValueFn(); + + const pathGenerator = line() + .x(({ x }) => xScale.scaleOrThrow(x) - xScaleOffset) + .y(y1Fn) + .defined((datum) => { + return definedFn(datum, y1Accessor); + }) + .curve(getCurveFactory(curve)); + + const { pointGeometries, indexedGeometryMap } = renderPoints( + shift - xScaleOffset, + dataSeries, + xScale, + yScale, + panel, + color, + seriesStyle.point, + hasY0Accessors, + markSizeOptions, + pointStyleAccessor, + ); + + const clippedRanges = getClippedRanges(dataSeries.data, xScale, xScaleOffset); + let linePath: string; + + try { + linePath = pathGenerator(dataSeries.data) || ''; + } catch { + // When values are not scalable + linePath = ''; + } + + const lineGeometry = { + line: linePath, + points: pointGeometries, + color, + transform: { + x: shift, + y: 0, + }, + seriesIdentifier: { + key: dataSeries.key, + specId: dataSeries.specId, + yAccessor: dataSeries.yAccessor, + splitAccessors: dataSeries.splitAccessors, + seriesKeys: dataSeries.seriesKeys, + smHorizontalAccessorValue: dataSeries.smHorizontalAccessorValue, + smVerticalAccessorValue: dataSeries.smVerticalAccessorValue, + }, + seriesLineStyle: seriesStyle.line, + seriesPointStyle: seriesStyle.point, + clippedRanges, + hideClippedRanges: !hasFit, + }; + return { + lineGeometry, + indexedGeometryMap, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/rendering/point_style.ts b/packages/osd-charts/src/chart_types/xy_chart/rendering/point_style.ts new file mode 100644 index 000000000000..59471109a478 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/rendering/point_style.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { OpacityFn, stringToRGB } from '../../../common/color_library_wrappers'; +import { getColorFromVariant, mergePartial } from '../../../utils/common'; +import { PointGeometryStyle } from '../../../utils/geometry'; +import { PointShape, PointStyle } from '../../../utils/themes/theme'; + +/** @internal */ +export function buildPointGeometryStyles( + color: string, + themePointStyle: PointStyle, + overrides?: Partial, +): PointGeometryStyle { + const pointStyle = mergePartial(themePointStyle, overrides, { mergeOptionalPartialValues: true }); + + const opacityFn: OpacityFn = (opacity) => opacity * pointStyle.opacity; + + return { + fill: { + color: stringToRGB(getColorFromVariant(color, pointStyle.fill), opacityFn), + }, + stroke: { + color: stringToRGB(getColorFromVariant(color, pointStyle.stroke), opacityFn), + width: pointStyle.strokeWidth, + }, + shape: pointStyle.shape ?? PointShape.Circle, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/rendering/points.ts b/packages/osd-charts/src/chart_types/xy_chart/rendering/points.ts new file mode 100644 index 000000000000..efd056475898 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/rendering/points.ts @@ -0,0 +1,267 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Scale } from '../../../scales'; +import { Color, isNil } from '../../../utils/common'; +import { Dimensions } from '../../../utils/dimensions'; +import { BandedAccessorType, PointGeometry } from '../../../utils/geometry'; +import { PointStyle } from '../../../utils/themes/theme'; +import { GeometryType, IndexedGeometryMap } from '../utils/indexed_geometry_map'; +import { DataSeries, DataSeriesDatum, FilledValues, XYChartSeriesIdentifier } from '../utils/series'; +import { PointStyleAccessor, StackMode } from '../utils/specs'; +import { buildPointGeometryStyles } from './point_style'; +import { + getY0ScaledValueOrThrowFn, + getY1ScaledValueOrThrowFn, + getYDatumValueFn, + isDatumFilled, + isYValueDefinedFn, + MarkSizeOptions, + YDefinedFn, +} from './utils'; + +/** @internal */ +export function renderPoints( + shift: number, + dataSeries: DataSeries, + xScale: Scale, + yScale: Scale, + panel: Dimensions, + color: Color, + pointStyle: PointStyle, + hasY0Accessors: boolean, + markSizeOptions: MarkSizeOptions, + styleAccessor?: PointStyleAccessor, + spatial = false, +): { + pointGeometries: PointGeometry[]; + indexedGeometryMap: IndexedGeometryMap; +} { + const indexedGeometryMap = new IndexedGeometryMap(); + const getRadius = markSizeOptions.enabled + ? getRadiusFn(dataSeries.data, pointStyle.strokeWidth, markSizeOptions.ratio) + : () => 0; + const geometryType = spatial ? GeometryType.spatial : GeometryType.linear; + + const y1Fn = getY1ScaledValueOrThrowFn(yScale); + const y0Fn = getY0ScaledValueOrThrowFn(yScale); + const yDefined = isYValueDefinedFn(yScale, xScale); + + const pointGeometries = dataSeries.data.reduce((acc, datum, dataIndex) => { + const { x: xValue, mark } = datum; + const prev = dataSeries.data[dataIndex - 1]; + const next = dataSeries.data[dataIndex + 1]; + // don't create the point if not within the xScale domain + if (!xScale.isValueInDomain(xValue)) { + return acc; + } + // don't create the point if it that point was filled + if (isDatumFilled(datum)) { + return acc; + } + const x = xScale.scale(xValue); + + if (x === null) { + return acc; + } + + const points: PointGeometry[] = []; + const yDatumKeyNames: Array> = hasY0Accessors ? ['y0', 'y1'] : ['y1']; + + yDatumKeyNames.forEach((yDatumKeyName, keyIndex) => { + const valueAccessor = getYDatumValueFn(yDatumKeyName); + + let y: number | null; + try { + y = yDatumKeyName === 'y1' ? y1Fn(datum) : y0Fn(datum); + // skip rendering point if y1 is null + if (y === null) { + return; + } + } catch { + return; + } + + const originalY = getDatumYValue(datum, keyIndex === 0, hasY0Accessors, dataSeries.stackMode); + const seriesIdentifier: XYChartSeriesIdentifier = { + key: dataSeries.key, + specId: dataSeries.specId, + yAccessor: dataSeries.yAccessor, + splitAccessors: dataSeries.splitAccessors, + seriesKeys: dataSeries.seriesKeys, + smVerticalAccessorValue: dataSeries.smVerticalAccessorValue, + smHorizontalAccessorValue: dataSeries.smHorizontalAccessorValue, + }; + const styleOverrides = getPointStyleOverrides(datum, seriesIdentifier, styleAccessor); + const style = buildPointGeometryStyles(color, pointStyle, styleOverrides); + const orphan = isOrphanDataPoint(dataIndex, dataSeries.data.length, yDefined, prev, next); + // if radius is defined with the mark, limit the minimum radius to the theme radius value + const radius = markSizeOptions.enabled + ? Math.max(getRadius(mark), pointStyle.radius) + : styleOverrides?.radius ?? pointStyle.radius; + const pointGeometry: PointGeometry = { + x, + y, + radius, + color, + style, + value: { + x: xValue, + y: originalY, + mark, + accessor: hasY0Accessors && keyIndex === 0 ? BandedAccessorType.Y0 : BandedAccessorType.Y1, + datum: datum.datum, + }, + transform: { + x: shift, + y: 0, + }, + seriesIdentifier, + panel, + orphan, + }; + indexedGeometryMap.set(pointGeometry, geometryType); + // use the geometry only if the yDatum in contained in the current yScale domain + if (yDefined(datum, valueAccessor)) { + points.push(pointGeometry); + } + }); + return [...acc, ...points]; + }, [] as PointGeometry[]); + return { + pointGeometries, + indexedGeometryMap, + }; +} + +/** @internal */ +export function getPointStyleOverrides( + datum: DataSeriesDatum, + seriesIdentifier: XYChartSeriesIdentifier, + pointStyleAccessor?: PointStyleAccessor, +): Partial | undefined { + const styleOverride = pointStyleAccessor && pointStyleAccessor(datum, seriesIdentifier); + + if (!styleOverride) { + return; + } + + if (typeof styleOverride === 'string') { + return { + stroke: styleOverride, + }; + } + + return styleOverride; +} + +/** + * Get the original/initial Y value from the datum + * @param datum a DataSeriesDatum + * @param lookingForY0 if we are interested in the y0 value, false for y1 + * @param isBandChart if the chart is a band chart + * @param stackMode an optional stack mode + */ +function getDatumYValue( + { y1, y0, initialY1, initialY0 }: DataSeriesDatum, + lookingForY0: boolean, + isBandChart: boolean, + stackMode?: StackMode, +) { + if (isBandChart) { + return stackMode === StackMode.Percentage + ? // on band stacked charts in percentage mode, the values I'm looking for are the percentage value + // that are already computed and available on y0 and y1 + lookingForY0 + ? y0 + : y1 + : // in all other cases for band charts, I want to get back the original/initial value of y0 and y1 + // not the computed value + lookingForY0 + ? initialY0 + : initialY1; + } + // if not a band chart get use the original/initial value in every case except for stack as percentage + // in this case, we should take the difference between the bottom position of the bar and the top position + // of the bar + return stackMode === StackMode.Percentage ? (y1 ?? 0) - (y0 ?? 0) : initialY1; +} + +/** + * Get radius function form ratio and min/max mark size + * + * @todo add continuous/non-stepped function + * + * @param {DataSeriesDatum[]} data + * @param {number} lineWidth + * @param {number=50} markSizeRatio - 0 to 100 + * @internal + */ +export function getRadiusFn( + data: DataSeriesDatum[], + lineWidth: number, + markSizeRatio: number = 50, +): (mark: number | null, defaultRadius?: number) => number { + if (data.length === 0) { + return () => 0; + } + const { min, max } = data.reduce( + (acc, { mark }) => + mark === null + ? acc + : { + min: Math.min(acc.min, mark / 2), + max: Math.max(acc.max, mark / 2), + }, + { min: Infinity, max: -Infinity }, + ); + const adjustedMarkSizeRatio = Math.min(Math.max(markSizeRatio, 0), 100); + const radiusStep = (max - min || max * 100) / Math.pow(adjustedMarkSizeRatio, 2); + return function getRadius(mark, defaultRadius = 0): number { + if (mark === null) { + return defaultRadius; + } + const circleRadius = (mark / 2 - min) / radiusStep; + const baseMagicNumber = 2; + return circleRadius ? Math.sqrt(circleRadius + baseMagicNumber) + lineWidth : lineWidth; + }; +} + +function yAccessorForOrphanCheck(datum: DataSeriesDatum): number | null { + return datum.filled?.y1 ? null : datum.y1; +} + +function isOrphanDataPoint( + index: number, + length: number, + yDefined: YDefinedFn, + prev?: DataSeriesDatum, + next?: DataSeriesDatum, +): boolean { + if (index === 0 && (isNil(next) || !yDefined(next, yAccessorForOrphanCheck))) { + return true; + } + if (index === length - 1 && (isNil(prev) || !yDefined(prev, yAccessorForOrphanCheck))) { + return true; + } + return ( + (isNil(prev) || !yDefined(prev, yAccessorForOrphanCheck)) && + (isNil(next) || !yDefined(next, yAccessorForOrphanCheck)) + ); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.areas.test.ts b/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.areas.test.ts new file mode 100644 index 000000000000..5ae4ce509664 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.areas.test.ts @@ -0,0 +1,1003 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Store } from 'redux'; + +import { MockPointGeometry } from '../../../mocks/geometries'; +import { MockSeriesIdentifier } from '../../../mocks/series/series_identifiers'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store'; +import { ScaleType } from '../../../scales/constants'; +import { Spec } from '../../../specs'; +import { GlobalChartState } from '../../../state/chart_state'; +import { PointGeometry, AreaGeometry } from '../../../utils/geometry'; +import { LIGHT_THEME } from '../../../utils/themes/light_theme'; +import { computeSeriesDomainsSelector } from '../state/selectors/compute_series_domains'; +import { computeSeriesGeometriesSelector } from '../state/selectors/compute_series_geometries'; +import { ComputedGeometries } from '../state/utils/types'; +import { IndexedGeometryMap } from '../utils/indexed_geometry_map'; +import { AreaSeriesSpec, StackMode } from '../utils/specs'; + +const SPEC_ID = 'spec_1'; +const GROUP_ID = 'group_1'; + +function initStore(specs: Spec[], vizColors: string[] = ['red'], width = 100): Store { + const store = MockStore.default({ width, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + ...specs, + MockGlobalSpec.settingsNoMargins({ + theme: { + colors: { + vizColors, + }, + }, + }), + ], + store, + ); + return store; +} + +describe('Rendering points - areas', () => { + test('Missing geometry if no data', () => { + const store = initStore([ + MockSeriesSpec.area({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [], + }), + ]); + const { + geometries: { areas }, + } = computeSeriesGeometriesSelector(store.getState()); + expect(areas).toHaveLength(0); + }); + describe('Single series area chart - ordinal', () => { + let areaGeometry: AreaGeometry; + let geometriesIndex: IndexedGeometryMap; + beforeEach(() => { + const store = initStore([ + MockSeriesSpec.area({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 10], + [1, 5], + ], + }), + ]); + const geometries = computeSeriesGeometriesSelector(store.getState()); + [{ value: areaGeometry }] = geometries.geometries.areas; + geometriesIndex = geometries.geometriesIndex; + }); + test('Can render an line and area paths', () => { + const { lines, area, color, seriesIdentifier, transform } = areaGeometry; + expect(lines[0]).toBe('M0,0L50,50'); + expect(area).toBe('M0,0L50,50L50,100L0,100Z'); + expect(color).toBe('red'); + expect(seriesIdentifier.seriesKeys).toEqual([1]); + expect(seriesIdentifier.specId).toEqual(SPEC_ID); + expect(transform).toEqual({ x: 25, y: 0 }); + }); + + test('Can render two points', () => { + const { points } = areaGeometry; + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'red', + seriesIdentifier: { + specId: SPEC_ID, + yAccessor: 1, + splitAccessors: new Map(), + seriesKeys: [1], + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', + }, + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + transform: { + x: 25, + y: 0, + }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 50, + color: 'red', + seriesIdentifier: { + specId: SPEC_ID, + yAccessor: 1, + splitAccessors: new Map(), + seriesKeys: [1], + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', + }, + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + transform: { + x: 25, + y: 0, + }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); + }); + }); + describe('Multi series area chart - ordinal', () => { + let geometries: ComputedGeometries; + beforeEach(() => { + const store = initStore( + [ + MockSeriesSpec.area({ + id: 'spec_1', + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 10], + [1, 5], + ], + }), + MockSeriesSpec.area({ + id: 'spec_2', + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 20], + [1, 10], + ], + }), + ], + ['red', 'blue'], + ); + geometries = computeSeriesGeometriesSelector(store.getState()); + }); + + test('Can render two ordinal areas', () => { + const { areas } = geometries.geometries; + const [{ value: firstArea }, { value: secondArea }] = areas; + expect(firstArea.lines[0]).toBe('M0,50L50,75'); + expect(firstArea.area).toBe('M0,50L50,75L50,100L0,100Z'); + expect(firstArea.color).toBe('red'); + expect(firstArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstArea.seriesIdentifier.specId).toEqual('spec_1'); + expect(firstArea.transform).toEqual({ x: 25, y: 0 }); + + expect(secondArea.lines[0]).toBe('M0,0L50,50'); + expect(secondArea.area).toBe('M0,0L50,50L50,100L0,100Z'); + expect(secondArea.color).toBe('blue'); + expect(secondArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(secondArea.seriesIdentifier.specId).toEqual('spec_2'); + expect(secondArea.transform).toEqual({ x: 25, y: 0 }); + }); + test('can render first spec points', () => { + const { areas } = geometries.geometries; + const [{ value: firstArea }] = areas; + expect(firstArea.points.length).toEqual(2); + expect(firstArea.points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 50, + color: 'red', + seriesIdentifier: { + specId: 'spec_1', + yAccessor: 1, + splitAccessors: new Map(), + seriesKeys: [1], + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', + }, + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + transform: { + x: 25, + y: 0, + }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, + }), + ); + expect(firstArea.points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 75, + color: 'red', + seriesIdentifier: { + specId: 'spec_1', + yAccessor: 1, + splitAccessors: new Map(), + seriesKeys: [1], + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', + }, + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + transform: { + x: 25, + y: 0, + }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, + }), + ); + }); + test('can render second spec points', () => { + const { areas } = geometries.geometries; + const [, { value: secondArea }] = areas; + expect(secondArea.points.length).toEqual(2); + expect(secondArea.points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'blue', + seriesIdentifier: { + specId: 'spec_2', + yAccessor: 1, + splitAccessors: new Map(), + seriesKeys: [1], + key: 'groupId{group_1}spec{spec_2}yAccessor{1}splitAccessors{}', + }, + value: { + accessor: 'y1', + x: 0, + y: 20, + mark: null, + datum: [0, 20], + }, + transform: { + x: 25, + y: 0, + }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, + }), + ); + expect(secondArea.points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 50, + color: 'blue', + seriesIdentifier: { + specId: 'spec_2', + yAccessor: 1, + splitAccessors: new Map(), + seriesKeys: [1], + key: 'groupId{group_1}spec{spec_2}yAccessor{1}splitAccessors{}', + }, + value: { + accessor: 'y1', + x: 1, + y: 10, + mark: null, + datum: [1, 10], + }, + transform: { + x: 25, + y: 0, + }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, + }), + ); + }); + test('has the right number of geometry in the indexes', () => { + const { areas } = geometries.geometries; + const [{ value: firstArea }] = areas; + expect(geometries.geometriesIndex.size).toEqual(firstArea.points.length); + }); + }); + + describe('Single series area chart - linear', () => { + let geometries: ComputedGeometries; + const spec = MockSeriesSpec.area({ + id: 'spec_1', + groupId: GROUP_ID, + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 10], + [1, 5], + ], + }); + beforeEach(() => { + const store = initStore([spec], ['red']); + geometries = computeSeriesGeometriesSelector(store.getState()); + }); + + test('Can render a linear area', () => { + const { areas } = geometries.geometries; + const [{ value: firstArea }] = areas; + expect(firstArea.lines[0]).toBe('M0,0L100,50'); + expect(firstArea.area).toBe('M0,0L100,50L100,100L0,100Z'); + expect(firstArea.color).toBe('red'); + expect(firstArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstArea.seriesIdentifier.specId).toEqual(SPEC_ID); + expect(firstArea.transform).toEqual({ x: 0, y: 0 }); + }); + test('Can render two points', () => { + const { areas } = geometries.geometries; + const [{ value: firstArea }] = areas; + expect(firstArea.points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec), + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + }), + ); + expect(firstArea.points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + }), + ); + expect(geometries.geometriesIndex.size).toEqual(firstArea.points.length); + }); + }); + + describe('Multi series area chart - linear', () => { + let geometries: ComputedGeometries; + const spec1 = MockSeriesSpec.area({ + id: 'spec_1', + groupId: GROUP_ID, + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 10], + [1, 5], + ], + }); + const spec2 = MockSeriesSpec.area({ + id: 'spec_2', + groupId: GROUP_ID, + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 20], + [1, 10], + ], + }); + beforeEach(() => { + const store = initStore([spec1, spec2], ['red', 'blue']); + geometries = computeSeriesGeometriesSelector(store.getState()); + }); + test('can render two linear areas', () => { + const { areas } = geometries.geometries; + const [{ value: firstArea }, { value: secondArea }] = areas; + expect(firstArea.lines[0]).toBe('M0,50L100,75'); + expect(firstArea.area).toBe('M0,50L100,75L100,100L0,100Z'); + expect(firstArea.color).toBe('red'); + expect(firstArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstArea.seriesIdentifier.specId).toEqual('spec_1'); + expect(firstArea.transform).toEqual({ x: 0, y: 0 }); + + expect(secondArea.lines[0]).toBe('M0,0L100,50'); + expect(secondArea.area).toBe('M0,0L100,50L100,100L0,100Z'); + expect(secondArea.color).toBe('blue'); + expect(secondArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(secondArea.seriesIdentifier.specId).toEqual('spec_2'); + expect(secondArea.transform).toEqual({ x: 0, y: 0 }); + }); + test('can render first spec points', () => { + const { areas } = geometries.geometries; + const [{ value: firstArea }] = areas; + expect(firstArea.points.length).toEqual(2); + expect(firstArea.points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 50, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec1), + + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + }), + ); + expect(firstArea.points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 75, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec1), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + }), + ); + expect(geometries.geometriesIndex.size).toEqual(firstArea.points.length); + }); + test('can render second spec points', () => { + const { areas } = geometries.geometries; + const [, { value: secondArea }] = areas; + expect(secondArea.points.length).toEqual(2); + expect(secondArea.points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec2), + + value: { + accessor: 'y1', + x: 0, + y: 20, + mark: null, + datum: [0, 20], + }, + }), + ); + expect(secondArea.points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec2), + value: { + accessor: 'y1', + x: 1, + y: 10, + mark: null, + datum: [1, 10], + }, + }), + ); + expect(geometries.geometriesIndex.size).toEqual(secondArea.points.length); + }); + }); + describe('Single series area chart - time', () => { + let geometries: ComputedGeometries; + const spec = MockSeriesSpec.area({ + id: 'spec_1', + groupId: GROUP_ID, + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [1546300800000, 10], + [1546387200000, 5], + ], + }); + beforeEach(() => { + const store = initStore([spec], ['red']); + geometries = computeSeriesGeometriesSelector(store.getState()); + }); + + test('Can render a time area', () => { + const { areas } = geometries.geometries; + const [{ value: firstArea }] = areas; + expect(firstArea.lines[0]).toBe('M0,0L100,50'); + expect(firstArea.area).toBe('M0,0L100,50L100,100L0,100Z'); + expect(firstArea.color).toBe('red'); + expect(firstArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstArea.seriesIdentifier.specId).toEqual(SPEC_ID); + expect(firstArea.transform).toEqual({ x: 0, y: 0 }); + }); + test('Can render two points', () => { + const { areas } = geometries.geometries; + const [{ value: firstArea }] = areas; + expect(firstArea.points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec), + + value: { + accessor: 'y1', + x: 1546300800000, + y: 10, + mark: null, + datum: [1546300800000, 10], + }, + }), + ); + expect(firstArea.points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec), + value: { + accessor: 'y1', + x: 1546387200000, + y: 5, + mark: null, + datum: [1546387200000, 5], + }, + }), + ); + expect(geometries.geometriesIndex.size).toEqual(firstArea.points.length); + }); + }); + describe('Multi series area chart - time', () => { + let geometries: ComputedGeometries; + const spec1 = MockSeriesSpec.area({ + id: 'spec_1', + groupId: GROUP_ID, + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [1546300800000, 10], + [1546387200000, 5], + ], + }); + const spec2 = MockSeriesSpec.area({ + id: 'spec_2', + groupId: GROUP_ID, + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [1546300800000, 20], + [1546387200000, 10], + ], + }); + beforeEach(() => { + const store = initStore([spec1, spec2], ['red', 'blue']); + geometries = computeSeriesGeometriesSelector(store.getState()); + }); + + test('can render first spec points', () => { + const { areas } = geometries.geometries; + const [{ value: firstArea }] = areas; + expect(firstArea.points.length).toEqual(2); + expect(firstArea.points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 50, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec1), + + value: { + accessor: 'y1', + x: 1546300800000, + y: 10, + mark: null, + datum: [1546300800000, 10], + }, + }), + ); + expect(firstArea.points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 75, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec1), + + value: { + accessor: 'y1', + x: 1546387200000, + y: 5, + mark: null, + datum: [1546387200000, 5], + }, + }), + ); + expect(geometries.geometriesIndex.size).toEqual(firstArea.points.length); + }); + test('can render second spec points', () => { + const { areas } = geometries.geometries; + const [, { value: secondArea }] = areas; + + expect(secondArea.points.length).toEqual(2); + expect(secondArea.points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec2), + + value: { + accessor: 'y1', + x: 1546300800000, + y: 20, + mark: null, + datum: [1546300800000, 20], + }, + }), + ); + expect(secondArea.points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec2), + + value: { + accessor: 'y1', + x: 1546387200000, + y: 10, + mark: null, + datum: [1546387200000, 10], + }, + }), + ); + expect(geometries.geometriesIndex.size).toEqual(secondArea.points.length); + }); + }); + describe('Single series area chart - y log', () => { + let geometries: ComputedGeometries; + const spec = MockSeriesSpec.area({ + id: 'spec_1', + groupId: GROUP_ID, + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Log, + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 10], + [1, 5], + [2, null], + [3, 5], + [4, 5], + [5, 0], + [6, 10], + [7, 10], + [8, 10], + ], + }); + beforeEach(() => { + const store = initStore([spec], ['red'], 90); + geometries = computeSeriesGeometriesSelector(store.getState()); + }); + + test('Can render a split area and line', () => { + const { areas } = geometries.geometries; + + const [{ value: firstArea }] = areas; + expect(firstArea.lines[0].split('M').length - 1).toBe(3); + expect(firstArea.area.split('M').length - 1).toBe(3); + expect(firstArea.color).toBe('red'); + expect(firstArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstArea.seriesIdentifier.specId).toEqual(SPEC_ID); + expect(firstArea.transform).toEqual({ x: 0, y: 0 }); + }); + test('Can render points', () => { + const { + geometriesIndex, + geometries: { areas }, + } = geometries; + const [ + { + value: { points }, + }, + ] = areas; + // all the points minus the undefined ones on a log scale + expect(points.length).toBe(7); + // all the points expect null geometries + expect(geometriesIndex.size).toEqual(8); + const nullIndexdGeometry = geometriesIndex.find(2)!; + expect(nullIndexdGeometry).toEqual([]); + + const zeroValueIndexdGeometry = geometriesIndex.find(5)!; + expect(zeroValueIndexdGeometry).toBeDefined(); + expect(zeroValueIndexdGeometry.length).toBe(1); + // moved to the bottom of the chart + expect(zeroValueIndexdGeometry[0].y).toBe(Infinity); + // default area theme point radius + expect((zeroValueIndexdGeometry[0] as PointGeometry).radius).toBe(LIGHT_THEME.areaSeriesStyle.point.radius); + }); + }); + it('Stacked areas with 0 values', () => { + const pointSeriesSpec1: AreaSeriesSpec = MockSeriesSpec.area({ + id: 'spec_1', + data: [ + [1546300800000, 0], + [1546387200000, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + stackAccessors: [0], + stackMode: StackMode.Percentage, + }); + const pointSeriesSpec2: AreaSeriesSpec = MockSeriesSpec.area({ + id: 'spec_2', + data: [ + [1546300800000, 0], + [1546387200000, 2], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + stackAccessors: [0], + stackMode: StackMode.Percentage, + }); + + const store = initStore([pointSeriesSpec1, pointSeriesSpec2]); + const domains = computeSeriesDomainsSelector(store.getState()); + + expect(domains.formattedDataSeries[0].data).toMatchObject([ + { + datum: [1546300800000, 0], + initialY0: null, + initialY1: 0, + x: 1546300800000, + y0: 0, + y1: 0, + mark: null, + }, + { + datum: [1546387200000, 5], + initialY0: null, + initialY1: 5, + x: 1546387200000, + y0: 0, + y1: 0.7142857142857143, + mark: null, + }, + ]); + }); + it('Stacked areas with null values', () => { + const pointSeriesSpec1: AreaSeriesSpec = MockSeriesSpec.area({ + id: 'spec_1', + data: [ + [1546300800000, null], + [1546387200000, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + stackAccessors: [0], + }); + const pointSeriesSpec2: AreaSeriesSpec = MockSeriesSpec.area({ + id: 'spec_2', + data: [ + [1546300800000, 3], + [1546387200000, null], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + stackAccessors: [0], + }); + const store = initStore([pointSeriesSpec1, pointSeriesSpec2]); + const domains = computeSeriesDomainsSelector(store.getState()); + + expect(domains.formattedDataSeries[0].data).toMatchObject([ + { + datum: [1546300800000, null], + initialY0: null, + initialY1: null, + x: 1546300800000, + y0: 0, + y1: 0, + mark: null, + }, + { + datum: [1546387200000, 5], + initialY0: null, + initialY1: 5, + x: 1546387200000, + y0: 0, + y1: 5, + mark: null, + }, + ]); + + expect(domains.formattedDataSeries[1].data).toEqual([ + { + datum: [1546300800000, 3], + initialY0: null, + initialY1: 3, + x: 1546300800000, + y0: 0, + y1: 3, + mark: null, + }, + { + datum: [1546387200000, null], + initialY0: null, + initialY1: null, + x: 1546387200000, + y0: 5, + y1: 5, + mark: null, + }, + ]); + }); + + // describe('Error guards for scaled values', () => { + // const pointSeriesSpec: AreaSeriesSpec = { + // chartType: ChartType.XYAxis, + // specType: SpecType.Series, + // id: SPEC_ID, + // groupId: GROUP_ID, + // seriesType: SeriesType.Area, + // data: [ + // [0, 10], + // [1, 5], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Ordinal, + // yScaleType: ScaleType.Linear, + // }; + // const pointSeriesMap = [pointSeriesSpec]; + // const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); + // const xScale = computeXScale({ + // xDomain: pointSeriesDomains.xDomain, + // totalBarsInCluster: pointSeriesMap.length, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); + // let renderedArea: { + // areaGeometry: AreaGeometry; + // indexedGeometryMap: IndexedGeometryMap; + // }; + // + // beforeEach(() => { + // renderedArea = renderArea( + // 25, // adding a ideal 25px shift, generally applied by renderGeometries + // pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // CurveType.LINEAR, + // false, + // 0, + // LIGHT_THEME.areaSeriesStyle, + // { + // enabled: false, + // }, + // ); + // }); + // + // describe('xScale values throw error', () => { + // beforeAll(() => { + // jest.spyOn(xScale, 'scaleOrThrow').mockImplementation(() => { + // throw new Error(); + // }); + // }); + // + // test('Should include no lines nor area', () => { + // const { + // areaGeometry: { lines, area, color, seriesIdentifier, transform }, + // } = renderedArea; + // expect(lines).toHaveLength(0); + // expect(area).toBe(''); + // expect(color).toBe('red'); + // expect(seriesIdentifier.seriesKeys).toEqual([1]); + // expect(seriesIdentifier.specId).toEqual(SPEC_ID); + // expect(transform).toEqual({ x: 25, y: 0 }); + // }); + // }); + // + // describe('yScale values throw error', () => { + // beforeAll(() => { + // jest.spyOn(yScales.get(GROUP_ID)!, 'scaleOrThrow').mockImplementation(() => { + // throw new Error(); + // }); + // }); + // + // test('Should include no lines nor area', () => { + // const { + // areaGeometry: { lines, area, color, seriesIdentifier, transform }, + // } = renderedArea; + // expect(lines).toHaveLength(0); + // expect(area).toBe(''); + // expect(color).toBe('red'); + // expect(seriesIdentifier.seriesKeys).toEqual([1]); + // expect(seriesIdentifier.specId).toEqual(SPEC_ID); + // expect(transform).toEqual({ x: 25, y: 0 }); + // }); + // }); + // }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.bands.test.ts b/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.bands.test.ts new file mode 100644 index 000000000000..630a89928e3e --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.bands.test.ts @@ -0,0 +1,436 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MockBarGeometry, MockPointGeometry } from '../../../mocks'; +import { MockSeriesIdentifier } from '../../../mocks/series/series_identifiers'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store'; +import { ScaleType } from '../../../scales/constants'; +import { computeSeriesGeometriesSelector } from '../state/selectors/compute_series_geometries'; + +const SPEC_ID = 'spec_1'; +const GROUP_ID = 'group_1'; + +describe('Rendering bands - areas', () => { + describe('Single band area chart', () => { + const pointSeriesSpec = MockSeriesSpec.area({ + id: SPEC_ID, + data: [ + [0, 2, 10], + [1, 3, 5], + ], + xAccessor: 0, + y0Accessors: [1], + yAccessors: [2], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }); + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec, settings], store); + const { + geometries: { areas }, + } = computeSeriesGeometriesSelector(store.getState()); + + test('Can render upper and lower lines and area paths', () => { + const [ + { + value: { lines, area, color, seriesIdentifier, transform }, + }, + ] = areas; + expect(lines.length).toBe(2); + expect(lines[0]).toBe('M0,0L50,50'); + expect(lines[1]).toBe('M0,80L50,70'); + expect(area).toBe('M0,0L50,50L50,70L0,80Z'); + expect(color).toBe('red'); + expect(seriesIdentifier.seriesKeys).toEqual([2]); + expect(seriesIdentifier.specId).toEqual(SPEC_ID); + expect(transform).toEqual({ x: 25, y: 0 }); + }); + + test('Can render two points', () => { + const [ + { + value: { points }, + }, + ] = areas; + expect(points.length).toBe(4); + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 80, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y0', + x: 0, + y: 2, + mark: null, + datum: [0, 2, 10], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 2, 10], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(points[2]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 70, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y0', + x: 1, + y: 3, + mark: null, + datum: [1, 3, 5], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(points[3]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 50, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 3, 5], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + }); + }); + describe('Single band area chart with null values', () => { + const pointSeriesSpec = MockSeriesSpec.area({ + id: SPEC_ID, + groupId: GROUP_ID, + data: [ + [0, 2, 10], + [1, 2, null], + [2, 3, 5], + [3, 3, 5], + ], + xAccessor: 0, + y0Accessors: [1], + yAccessors: [2], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }); + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec, settings], store); + const { + geometries: { areas }, + } = computeSeriesGeometriesSelector(store.getState()); + + test('Can render upper and lower lines and area paths', () => { + const [ + { + value: { lines, area, color, seriesIdentifier, transform }, + }, + ] = areas; + expect(lines.length).toBe(2); + expect(lines[0]).toBe('M0,0ZM50,50L75,50'); + expect(lines[1]).toBe('M0,80ZM50,70L75,70'); + expect(area).toBe('M0,0L0,80ZM50,50L75,50L75,70L50,70Z'); + expect(color).toBe('red'); + expect(seriesIdentifier.seriesKeys).toEqual([2]); + expect(seriesIdentifier.specId).toEqual(SPEC_ID); + expect(transform).toEqual({ x: 12.5, y: 0 }); + }); + + test('Can render two points', () => { + const [ + { + value: { points }, + }, + ] = areas; + expect(points.length).toBe(6); + const getPointGeo = MockPointGeometry.fromBaseline( + { + x: 0, + y: 0, + color: 'red', + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 2, 10], + }, + transform: { + x: 12.5, + y: 0, + }, + }, + 'seriesIdentifier', + ); + expect(points[0]).toMatchObject( + getPointGeo({ + x: 0, + y: 80, + value: { + accessor: 'y0', + x: 0, + y: 2, + mark: null, + datum: [0, 2, 10], + }, + // the first point is also an orphan because the next one is null + orphan: true, + }), + ); + expect(points[1]).toMatchObject( + getPointGeo({ + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 2, 10], + }, + orphan: true, + }), + ); + expect(points[2]).toMatchObject( + getPointGeo({ + x: 50, + y: 70, + value: { + accessor: 'y0', + x: 2, + y: 3, + mark: null, + datum: [2, 3, 5], + }, + }), + ); + expect(points[3]).toMatchObject( + getPointGeo({ + x: 50, + y: 50, + value: { + accessor: 'y1', + x: 2, + y: 5, + mark: null, + datum: [2, 3, 5], + }, + }), + ); + expect(points[4]).toMatchObject( + getPointGeo({ + x: 75, + y: 70, + value: { + accessor: 'y0', + x: 3, + y: 3, + mark: null, + datum: [3, 3, 5], + }, + }), + ); + expect(points[5]).toMatchObject( + getPointGeo({ + x: 75, + y: 50, + value: { + accessor: 'y1', + x: 3, + y: 5, + mark: null, + datum: [3, 3, 5], + }, + }), + ); + }); + }); + describe('Single series band bar chart - ordinal', () => { + const barSeriesSpec = MockSeriesSpec.bar({ + id: SPEC_ID, + groupId: GROUP_ID, + data: [ + [0, 2, 10], + [1, 3, null], + [2, 3, 5], + [3, 4, 8], + ], + xAccessor: 0, + y0Accessors: [1], + yAccessors: [2], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }); + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([barSeriesSpec, settings], store); + const { + geometries: { + bars: [{ value: bars }], + }, + } = computeSeriesGeometriesSelector(store.getState()); + + test('Can render two bars', () => { + expect(bars.length).toBe(3); + expect(bars[0]).toEqual( + MockBarGeometry.default({ + x: 0, + y: 0, + width: 25, + height: 80, + color: 'red', + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 2, 10], + }, + seriesIdentifier: MockSeriesIdentifier.fromSpec(barSeriesSpec), + displayValue: undefined, + seriesStyle: { + displayValue: { + fill: '#777', + fontFamily: 'sans-serif', + fontSize: 8, + fontStyle: 'normal', + offsetX: 0, + offsetY: 0, + padding: 0, + }, + rect: { + opacity: 1, + }, + rectBorder: { + strokeWidth: 1, + visible: false, + }, + }, + }), + ); + expect(bars[1]).toEqual( + MockBarGeometry.default({ + x: 50, + y: 50, + width: 25, + height: 20, + color: 'red', + value: { + accessor: 'y1', + x: 2, + y: 5, + mark: null, + datum: [2, 3, 5], + }, + seriesIdentifier: MockSeriesIdentifier.fromSpec(barSeriesSpec), + displayValue: undefined, + seriesStyle: { + displayValue: { + fill: '#777', + fontFamily: 'sans-serif', + fontSize: 8, + fontStyle: 'normal', + offsetX: 0, + offsetY: 0, + padding: 0, + }, + rect: { + opacity: 1, + }, + rectBorder: { + strokeWidth: 1, + visible: false, + }, + }, + }), + ); + expect(bars[2]).toEqual( + MockBarGeometry.default({ + x: 75, + y: 20, + width: 25, + height: 40, + color: 'red', + value: { + accessor: 'y1', + x: 3, + y: 8, + mark: null, + datum: [3, 4, 8], + }, + seriesIdentifier: MockSeriesIdentifier.fromSpec(barSeriesSpec), + displayValue: undefined, + seriesStyle: { + displayValue: { + fill: '#777', + fontFamily: 'sans-serif', + fontSize: 8, + fontStyle: 'normal', + offsetX: 0, + offsetY: 0, + padding: 0, + }, + rect: { + opacity: 1, + }, + rectBorder: { + strokeWidth: 1, + visible: false, + }, + }, + }), + ); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.bars.test.ts b/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.bars.test.ts new file mode 100644 index 000000000000..14702b121e4b --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.bars.test.ts @@ -0,0 +1,862 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MockBarGeometry } from '../../../mocks'; +import { MockSeriesIdentifier } from '../../../mocks/series/series_identifiers'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store'; +import { ScaleType } from '../../../scales/constants'; +import { identity } from '../../../utils/common'; +import { computeSeriesGeometriesSelector } from '../state/selectors/compute_series_geometries'; + +const SPEC_ID = 'spec_1'; +const GROUP_ID = 'group_1'; + +describe('Rendering bars', () => { + test('Can render two bars within domain', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const spec = MockSeriesSpec.bar({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [-200, 0], + [0, 10], + [1, 5], + ], // first datum should be skipped as it's out of domain + }); + MockStore.addSpecs( + [spec, MockGlobalSpec.settingsNoMargins({ xDomain: [0, 1], theme: { colors: { vizColors: ['red'] } } })], + store, + ); + const { geometries } = computeSeriesGeometriesSelector(store.getState()); + + const getBarGeometry = MockBarGeometry.fromBaseline( + { + x: 0, + y: 0, + width: 50, + height: 100, + color: 'red', + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec), + }, + 'displayValue', + ); + expect(geometries.bars[0].value[0]).toEqual(getBarGeometry()); + expect(geometries.bars[0].value[1]).toEqual( + getBarGeometry({ + x: 50, + y: 50, + width: 50, + height: 50, + value: { + x: 1, + y: 5, + datum: [1, 5], + }, + }), + ); + expect(geometries.bars[0].value.length).toBe(2); + }); + + describe('Single series bar chart - ordinal', () => { + test('Can render bars with value labels', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [-200, 0], + [0, 10], + [1, 5], + ], // first datum should be skipped as it's out of domain + displayValueSettings: { + showValueLabel: true, + isAlternatingValueLabel: true, + valueFormatter: identity, + }, + }), + MockGlobalSpec.settingsNoMargins({ xDomain: [0, 1], theme: { colors: { vizColors: ['red'] } } }), + ], + store, + ); + const { geometries } = computeSeriesGeometriesSelector(store.getState()); + expect(geometries.bars[0].value[0].displayValue).toBeDefined(); + }); + + test('Can hide value labels if no formatter or showValueLabels is false/undefined', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [-200, 0], + [0, 10], + [1, 5], + ], // first datum should be skipped as it's out of domain + displayValueSettings: { + showValueLabel: false, + isAlternatingValueLabel: true, + valueFormatter: identity, + }, + }), + MockGlobalSpec.settingsNoMargins({ xDomain: [0, 1], theme: { colors: { vizColors: ['red'] } } }), + ], + store, + ); + const { geometries } = computeSeriesGeometriesSelector(store.getState()); + expect(geometries.bars[0].value[0].displayValue).toBeUndefined(); + }); + + test('Can render bars with alternating value labels', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [-200, 0], + [0, 10], + [1, 5], + ], // first datum should be skipped as it's out of domain + displayValueSettings: { + showValueLabel: true, + isAlternatingValueLabel: true, + valueFormatter: identity, + }, + }), + MockGlobalSpec.settingsNoMargins({ xDomain: [0, 1], theme: { colors: { vizColors: ['red'] } } }), + ], + store, + ); + const { geometries } = computeSeriesGeometriesSelector(store.getState()); + + expect(geometries.bars[0].value[0].displayValue?.text).toBeDefined(); + expect(geometries.bars[0].value[1].displayValue?.text).toBeUndefined(); + }); + + test('Can render bars with contained value labels', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [-200, 0], + [0, 10], + [1, 5], + ], // first datum should be skipped as it's out of domain + displayValueSettings: { + showValueLabel: true, + isValueContainedInElement: true, + valueFormatter: identity, + }, + }), + MockGlobalSpec.settingsNoMargins({ xDomain: [0, 1], theme: { colors: { vizColors: ['red'] } } }), + ], + store, + ); + const { geometries } = computeSeriesGeometriesSelector(store.getState()); + + expect(geometries.bars[0].value[0].displayValue?.width).toBe(50); + }); + }); + describe('Multi series bar chart - ordinal', () => { + const spec1Id = 'bar1'; + const spec2Id = 'bar2'; + const barSeriesSpec1 = MockSeriesSpec.bar({ + id: spec1Id, + groupId: GROUP_ID, + data: [ + [0, 10], + [1, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }); + const barSeriesSpec2 = MockSeriesSpec.bar({ + id: spec2Id, + groupId: GROUP_ID, + data: [ + [0, 20], + [1, 10], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }); + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + barSeriesSpec1, + barSeriesSpec2, + MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }), + ], + store, + ); + + const getBarGeometry = MockBarGeometry.fromBaseline( + { + x: 0, + y: 0, + width: 50, + height: 100, + color: 'red', + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + }, + seriesIdentifier: MockSeriesIdentifier.fromSpec(barSeriesSpec1), + }, + 'displayValue', + ); + const { + geometries: { bars }, + } = computeSeriesGeometriesSelector(store.getState()); + + test('can render first spec bars', () => { + expect(bars[0].value.length).toEqual(2); + expect(bars[0].value[0]).toEqual( + getBarGeometry({ + x: 0, + y: 50, + width: 25, + height: 50, + value: { + x: 0, + y: 10, + datum: [0, 10], + }, + }), + ); + expect(bars[0].value[1]).toEqual( + getBarGeometry({ + x: 50, + y: 75, + width: 25, + height: 25, + value: { + x: 1, + y: 5, + datum: [1, 5], + }, + }), + ); + }); + test('can render second spec bars', () => { + const getBarGeometry = MockBarGeometry.fromBaseline( + { + x: 0, + y: 0, + width: 50, + height: 100, + color: 'blue', + value: { + accessor: 'y1', + x: 0, + y: 10, + }, + seriesIdentifier: MockSeriesIdentifier.fromSpec(barSeriesSpec2), + }, + 'displayValue', + ); + expect(bars[1].value.length).toEqual(2); + expect(bars[1].value[0]).toEqual( + getBarGeometry({ + x: 25, + y: 0, + width: 25, + height: 100, + value: { + x: 0, + y: 20, + datum: [0, 20], + }, + }), + ); + expect(bars[1].value[1]).toEqual( + getBarGeometry({ + x: 75, + y: 50, + width: 25, + height: 50, + value: { + x: 1, + y: 10, + datum: [1, 10], + }, + }), + ); + }); + }); + // describe('Single series bar chart - linear', () => { + // const barSeriesSpec: BarSeriesSpec = { + // chartType: ChartType.XYAxis, + // specType: SpecType.Series, + // id: SPEC_ID, + // groupId: GROUP_ID, + // seriesType: SeriesType.Bar, + // data: [ + // [0, 10], + // [1, 5], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Linear, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesMap = [barSeriesSpec]; + // const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: barSeriesMap.length, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // + // test('Can render two bars', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // ); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 0, + // y: 0, + // width: 50, + // height: 100, + // color: 'red', + // value: { + // accessor: 'y1', + // x: 0, + // y: 10, + // mark: null, + // }, + // seriesIdentifier: { + // specId: SPEC_ID, + // key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // expect(barGeometries[0]).toEqual(getBarGeometry()); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 50, + // y: 50, + // width: 50, + // height: 50, + // value: { + // x: 1, + // y: 5, + // }, + // }), + // ); + // }); + // }); + // describe('Single series bar chart - log', () => { + // const barSeriesSpec: BarSeriesSpec = { + // chartType: ChartType.XYAxis, + // specType: SpecType.Series, + // id: SPEC_ID, + // groupId: GROUP_ID, + // seriesType: SeriesType.Bar, + // data: [ + // [1, 0], + // [2, 1], + // [3, 2], + // [4, 3], + // [5, 4], + // [6, 5], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Linear, + // yScaleType: ScaleType.Log, + // }; + // const barSeriesDomains = computeSeriesDomains([barSeriesSpec], new Map()); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: 1, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // + // test('Can render correct bar height', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // ); + // expect(barGeometries.length).toBe(6); + // expect(barGeometries[0].height).toBe(0); + // expect(barGeometries[1].height).toBe(0); + // expect(barGeometries[2].height).toBeGreaterThan(0); + // expect(barGeometries[3].height).toBeGreaterThan(0); + // }); + // }); + // describe('Multi series bar chart - linear', () => { + // const spec1Id = 'bar1'; + // const spec2Id = 'bar2'; + // const barSeriesSpec1: BarSeriesSpec = { + // chartType: ChartType.XYAxis, + // specType: SpecType.Series, + // id: spec1Id, + // groupId: GROUP_ID, + // seriesType: SeriesType.Bar, + // data: [ + // [0, 10], + // [1, 5], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Linear, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesSpec2: BarSeriesSpec = { + // chartType: ChartType.XYAxis, + // specType: SpecType.Series, + // id: spec2Id, + // groupId: GROUP_ID, + // seriesType: SeriesType.Bar, + // data: [ + // [0, 20], + // [1, 10], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Linear, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesMap = [barSeriesSpec1, barSeriesSpec2]; + // const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: barSeriesMap.length, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // + // test('can render first spec bars', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // ); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 0, + // y: 50, + // width: 25, + // height: 50, + // color: 'red', + // value: { + // accessor: 'y1', + // x: 0, + // y: 10, + // }, + // seriesIdentifier: { + // specId: spec1Id, + // key: 'spec{bar1}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // expect(barGeometries.length).toEqual(2); + // expect(barGeometries[0]).toEqual(getBarGeometry()); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 50, + // y: 75, + // width: 25, + // height: 25, + // value: { + // x: 1, + // y: 5, + // }, + // }), + // ); + // }); + // test('can render second spec bars', () => { + // const { barGeometries } = renderBars( + // 1, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], + // xScale, + // yScales.get(GROUP_ID)!, + // 'blue', + // LIGHT_THEME.barSeriesStyle, + // ); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 25, + // y: 0, + // width: 25, + // height: 100, + // color: 'blue', + // value: { + // accessor: 'y1', + // x: 0, + // y: 20, + // }, + // seriesIdentifier: { + // specId: spec2Id, + // key: 'spec{bar2}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // expect(barGeometries.length).toEqual(2); + // expect(barGeometries[0]).toEqual(getBarGeometry()); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 75, + // y: 50, + // width: 25, + // height: 50, + // color: 'blue', + // value: { + // x: 1, + // y: 10, + // }, + // }), + // ); + // }); + // }); + // describe('Multi series bar chart - time', () => { + // const spec1Id = 'bar1'; + // const spec2Id = 'bar2'; + // const barSeriesSpec1: BarSeriesSpec = { + // chartType: ChartType.XYAxis, + // specType: SpecType.Series, + // id: spec1Id, + // groupId: GROUP_ID, + // seriesType: SeriesType.Bar, + // data: [ + // [1546300800000, 10], + // [1546387200000, 5], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Time, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesSpec2: BarSeriesSpec = { + // chartType: ChartType.XYAxis, + // specType: SpecType.Series, + // id: spec2Id, + // groupId: GROUP_ID, + // seriesType: SeriesType.Bar, + // data: [ + // [1546300800000, 20], + // [1546387200000, 10], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Time, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesMap = [barSeriesSpec1, barSeriesSpec2]; + // const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: barSeriesMap.length, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // + // test('can render first spec bars', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // ); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 0, + // y: 50, + // width: 25, + // height: 50, + // color: 'red', + // value: { + // accessor: 'y1', + // x: 1546300800000, + // y: 10, + // }, + // seriesIdentifier: { + // specId: spec1Id, + // key: 'spec{bar1}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // expect(barGeometries.length).toEqual(2); + // expect(barGeometries[0]).toEqual(getBarGeometry()); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 50, + // y: 75, + // width: 25, + // height: 25, + // value: { + // accessor: 'y1', + // x: 1546387200000, + // y: 5, + // mark: null, + // }, + // }), + // ); + // }); + // test('can render second spec bars', () => { + // const { barGeometries } = renderBars( + // 1, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], + // xScale, + // yScales.get(GROUP_ID)!, + // 'blue', + // LIGHT_THEME.barSeriesStyle, + // ); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 25, + // y: 0, + // width: 25, + // height: 100, + // color: 'blue', + // value: { + // accessor: 'y1', + // x: 1546300800000, + // y: 20, + // mark: null, + // }, + // seriesIdentifier: { + // specId: spec2Id, + // key: 'spec{bar2}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // + // expect(barGeometries.length).toEqual(2); + // expect(barGeometries[0]).toEqual(getBarGeometry()); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 75, + // y: 50, + // width: 25, + // height: 50, + // value: { + // accessor: 'y1', + // x: 1546387200000, + // y: 10, + // mark: null, + // }, + // }), + // ); + // }); + // }); + // describe('Remove points datum is not in domain', () => { + // const barSeriesSpec: BarSeriesSpec = { + // chartType: ChartType.XYAxis, + // specType: SpecType.Series, + // id: SPEC_ID, + // groupId: GROUP_ID, + // seriesType: SeriesType.Bar, + // data: [ + // [0, 0], + // [1, 1], + // [2, 10], + // [3, 3], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Linear, + // yScaleType: ScaleType.Linear, + // }; + // const customYDomain = new Map(); + // customYDomain.set(GROUP_ID, { + // max: 1, + // }); + // const barSeriesDomains = computeSeriesDomains([barSeriesSpec], customYDomain, [], { + // max: 2, + // }); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: 1, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // + // test('Can render 3 bars', () => { + // const { barGeometries, indexedGeometryMap } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // ); + // expect(barGeometries.length).toBe(3); + // // will be cut by the clipping areas in the rendering component + // expect(barGeometries[2].height).toBe(1000); + // expect(indexedGeometryMap.size).toBe(3); + // }); + // }); + // describe('Renders minBarHeight', () => { + // const minBarHeight = 8; + // const data = [ + // [1, -100000], + // [2, -10000], + // [3, -1000], + // [4, -100], + // [5, -10], + // [6, -1], + // [7, 0], + // [8, -1], + // [9, 0], + // [10, 0], + // [11, 1], + // [12, 0], + // [13, 1], + // [14, 10], + // [15, 100], + // [16, 1000], + // [17, 10000], + // [18, 100000], + // ]; + // const barSeriesSpec: BarSeriesSpec = { + // chartType: ChartType.XYAxis, + // specType: SpecType.Series, + // id: SPEC_ID, + // groupId: GROUP_ID, + // seriesType: SeriesType.Bar, + // data, + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Linear, + // yScaleType: ScaleType.Linear, + // minBarHeight, + // }; + // + // const customYDomain = new Map(); + // const barSeriesDomains = computeSeriesDomains([barSeriesSpec], customYDomain); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: 1, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // const expected = [-50, -8, -8, -8, -8, -8, 0, -8, 0, 0, 8, 0, 8, 8, 8, 8, 8, 50]; + // + // it('should render correct heights with positive minBarHeight', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // undefined, + // undefined, + // minBarHeight, + // ); + // const barHeights = barGeometries.map(({ height }) => height); + // expect(barHeights).toEqual(expected); + // }); + // it('should render correct heights with negative minBarHeight', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // undefined, + // undefined, + // -minBarHeight, + // ); + // const barHeights = barGeometries.map(({ height }) => height); + // expect(barHeights).toEqual(expected); + // }); + // }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.bubble.test.ts b/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.bubble.test.ts new file mode 100644 index 000000000000..8a6060f971c5 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.bubble.test.ts @@ -0,0 +1,793 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MockPointGeometry } from '../../../mocks'; +import { MockSeriesIdentifier } from '../../../mocks/series/series_identifiers'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store'; +import { ScaleType } from '../../../scales/constants'; +import { Position } from '../../../utils/common'; +import { computeSeriesGeometriesSelector } from '../state/selectors/compute_series_geometries'; + +const SPEC_ID = 'spec_1'; +const GROUP_ID = 'group_1'; + +describe('Rendering points - bubble', () => { + describe('Single series bubble chart - ordinal', () => { + const spec = MockSeriesSpec.bubble({ + id: SPEC_ID, + groupId: GROUP_ID, + data: [ + [0, 10], + [1, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }); + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([spec, settings], store); + const { + geometries: { bubbles }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + test('Can render a bubble', () => { + const [{ value: bubbleGeometry }] = bubbles; + expect(bubbleGeometry.points).toHaveLength(2); + expect(bubbleGeometry.color).toBe('red'); + expect(bubbleGeometry.seriesIdentifier.seriesKeys).toEqual([1]); + expect(bubbleGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); + }); + test('Can render two points', () => { + const [ + { + value: { points }, + }, + ] = bubbles; + + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec), + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 50, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); + }); + }); + describe('Multi series bubble chart - ordinal', () => { + const spec1Id = 'point1'; + const spec2Id = 'point2'; + const pointSeriesSpec1 = MockSeriesSpec.bubble({ + id: spec1Id, + groupId: GROUP_ID, + data: [ + [0, 10], + [1, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }); + const pointSeriesSpec2 = MockSeriesSpec.bubble({ + id: spec2Id, + groupId: GROUP_ID, + data: [ + [0, 20], + [1, 10], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }); + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec1, pointSeriesSpec2, settings], store); + const { + geometries: { bubbles }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + + test('Can render two ordinal bubbles', () => { + const [{ value: firstBubble }, { value: secondBubble }] = bubbles; + expect(firstBubble.points).toHaveLength(2); + expect(firstBubble.color).toBe('red'); + expect(firstBubble.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstBubble.seriesIdentifier.specId).toEqual(spec1Id); + + expect(secondBubble.points).toHaveLength(2); + expect(secondBubble.color).toBe('blue'); + expect(secondBubble.seriesIdentifier.seriesKeys).toEqual([1]); + expect(secondBubble.seriesIdentifier.specId).toEqual(spec2Id); + expect(geometriesIndex.size).toEqual(4); + }); + test('can render first spec points', () => { + const [ + { + value: { points }, + }, + ] = bubbles; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 50, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 75, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + }); + test('can render second spec points', () => { + const [ + , + { + value: { points }, + }, + ] = bubbles; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 0, + y: 20, + mark: null, + datum: [0, 20], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 50, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 1, + y: 10, + mark: null, + datum: [1, 10], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + }); + }); + describe('Single series bubble chart - linear', () => { + const pointSeriesSpec = MockSeriesSpec.bubble({ + id: SPEC_ID, + groupId: GROUP_ID, + data: [ + [0, 10], + [1, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + }); + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec, settings], store); + const { + geometries: { bubbles }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + + test('Can render a linear bubble', () => { + const [{ value: bubbleGeometry }] = bubbles; + expect(bubbleGeometry.points).toHaveLength(2); + expect(bubbleGeometry.color).toBe('red'); + expect(bubbleGeometry.seriesIdentifier.seriesKeys).toEqual([1]); + expect(bubbleGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); + }); + test('Can render two points', () => { + const [ + { + value: { points }, + }, + ] = bubbles; + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); + }); + }); + describe('Multi series bubble chart - linear', () => { + const spec1Id = 'point1'; + const spec2Id = 'point2'; + const pointSeriesSpec1 = MockSeriesSpec.bubble({ + id: spec1Id, + groupId: GROUP_ID, + data: [ + [0, 10], + [1, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + }); + const pointSeriesSpec2 = MockSeriesSpec.bubble({ + id: spec2Id, + groupId: GROUP_ID, + data: [ + [0, 20], + [1, 10], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + }); + + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec1, pointSeriesSpec2, settings], store); + const { + geometries: { bubbles }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + + test('can render two linear bubbles', () => { + const [{ value: firstBubble }, { value: secondBubble }] = bubbles; + expect(firstBubble.points).toHaveLength(2); + expect(firstBubble.color).toBe('red'); + expect(firstBubble.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstBubble.seriesIdentifier.specId).toEqual(spec1Id); + + expect(secondBubble.points).toHaveLength(2); + expect(secondBubble.color).toBe('blue'); + expect(secondBubble.seriesIdentifier.seriesKeys).toEqual([1]); + expect(secondBubble.seriesIdentifier.specId).toEqual(spec2Id); + expect(geometriesIndex.size).toEqual(4); + }); + test('can render first spec points', () => { + const [ + { + value: { points }, + }, + ] = bubbles; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 50, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 75, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + }); + test('can render second spec points', () => { + const [ + , + { + value: { points }, + }, + ] = bubbles; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 0, + y: 20, + mark: null, + datum: [0, 20], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 1, + y: 10, + mark: null, + datum: [1, 10], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + }); + }); + describe('Single series bubble chart - time', () => { + const pointSeriesSpec = MockSeriesSpec.bubble({ + id: SPEC_ID, + groupId: GROUP_ID, + data: [ + [1546300800000, 10], + [1546387200000, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + }); + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec, settings], store); + const { + geometries: { bubbles }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + + test('Can render a time bubble', () => { + const [{ value: renderedBubble }] = bubbles; + expect(renderedBubble.points).toHaveLength(2); + expect(renderedBubble.color).toBe('red'); + expect(renderedBubble.seriesIdentifier.seriesKeys).toEqual([1]); + expect(renderedBubble.seriesIdentifier.specId).toEqual(SPEC_ID); + }); + test('Can render two points', () => { + const [ + { + value: { points }, + }, + ] = bubbles; + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1546300800000, + y: 10, + mark: null, + datum: [1546300800000, 10], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1546387200000, + y: 5, + mark: null, + datum: [1546387200000, 5], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); + }); + }); + describe('Multi series bubble chart - time', () => { + const spec1Id = 'point1'; + const spec2Id = 'point2'; + const pointSeriesSpec1 = MockSeriesSpec.bubble({ + id: spec1Id, + groupId: GROUP_ID, + data: [ + [1546300800000, 10], + [1546387200000, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + }); + const pointSeriesSpec2 = MockSeriesSpec.bubble({ + id: spec2Id, + groupId: GROUP_ID, + data: [ + [1546300800000, 20], + [1546387200000, 10], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + }); + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec1, pointSeriesSpec2, settings], store); + const { + geometries: { bubbles }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + test('can render first spec points', () => { + const [ + { + value: { points }, + }, + ] = bubbles; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 50, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 1546300800000, + y: 10, + mark: null, + datum: [1546300800000, 10], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 75, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 1546387200000, + y: 5, + mark: null, + datum: [1546387200000, 5], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + expect(geometriesIndex.size).toEqual(4); + }); + test('can render second spec points', () => { + const [ + , + { + value: { points }, + }, + ] = bubbles; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 1546300800000, + y: 20, + mark: null, + datum: [1546300800000, 20], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 1546387200000, + y: 10, + mark: null, + datum: [1546387200000, 10], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + }); + }); + describe('Single series bubble chart - y log', () => { + const pointSeriesSpec = MockSeriesSpec.bubble({ + id: SPEC_ID, + groupId: GROUP_ID, + data: [ + [0, 10], + [1, 5], + [2, null], + [3, 5], + [4, 5], + [5, 0], + [6, 10], + [7, 10], + [8, 10], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Log, + }); + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec, settings], store); + const { + geometries: { bubbles }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + + test('Can render a split bubble', () => { + const [{ value: renderedBubble }] = bubbles; + expect(renderedBubble.points).toHaveLength(7); + expect(renderedBubble.color).toBe('red'); + expect(renderedBubble.seriesIdentifier.seriesKeys).toEqual([1]); + expect(renderedBubble.seriesIdentifier.specId).toEqual(SPEC_ID); + }); + test('Can render points', () => { + const [ + { + value: { points }, + }, + ] = bubbles; + // all the points minus the undefined ones on a log scale + expect(points.length).toBe(7); + // all the points expect null geometries + expect(geometriesIndex.size).toEqual(8); + + const zeroValueIndexdGeometry = geometriesIndex.find(null, { + x: 56.25, + y: 100, + }); + expect(zeroValueIndexdGeometry).toBeDefined(); + expect(zeroValueIndexdGeometry.length).toBe(3); + expect(zeroValueIndexdGeometry.find(({ value: { x } }) => x === 5)).toBeDefined(); + }); + }); + describe('Remove points datum is not in domain', () => { + const pointSeriesSpec = MockSeriesSpec.bubble({ + id: SPEC_ID, + data: [ + [0, 0], + [1, 1], + [2, 10], + [3, 3], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + }); + const settings = MockGlobalSpec.settingsNoMargins({ + xDomain: { max: 2 }, + theme: { colors: { vizColors: ['red', 'blue'] } }, + }); + const axis = MockGlobalSpec.axis({ position: Position.Left, hide: true, domain: { max: 1 } }); + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs([pointSeriesSpec, axis, settings], store); + const { + geometries: { bubbles }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + test('Should render 3 points', () => { + const [ + { + value: { points }, + }, + ] = bubbles; + // will not render the 4th point that is out of x domain + expect(points.length).toBe(3); + // will keep the 3rd point as an indexedGeometry + expect(geometriesIndex.size).toEqual(3); + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 99.5, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 0, + y: 0, + mark: null, + datum: [0, 0], + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1, + y: 1, + mark: null, + datum: [1, 1], + }, + }), + ); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.lines.test.ts b/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.lines.test.ts new file mode 100644 index 000000000000..86023bb0d56a --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.lines.test.ts @@ -0,0 +1,781 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MockPointGeometry } from '../../../mocks'; +import { MockSeriesIdentifier } from '../../../mocks/series/series_identifiers'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store'; +import { ScaleType } from '../../../scales/constants'; +import { Position } from '../../../utils/common'; +import { PointGeometry } from '../../../utils/geometry'; +import { LIGHT_THEME } from '../../../utils/themes/light_theme'; +import { computeSeriesGeometriesSelector } from '../state/selectors/compute_series_geometries'; +import { SeriesType } from '../utils/specs'; + +const SPEC_ID = 'spec_1'; +const GROUP_ID = 'group_1'; + +describe('Rendering points - line', () => { + describe('Single series line chart - ordinal', () => { + const pointSeriesSpec = MockSeriesSpec.line({ + id: SPEC_ID, + groupId: GROUP_ID, + data: [ + [0, 10], + [1, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }); + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red'] } } }); + MockStore.addSpecs([pointSeriesSpec, settings], store); + const { + geometries: { lines }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + + test('Can render a line', () => { + const [{ value: lineGeometry }] = lines; + expect(lineGeometry.line).toBe('M0,0L50,50'); + expect(lineGeometry.color).toBe('red'); + expect(lineGeometry.seriesIdentifier.seriesKeys).toEqual([1]); + expect(lineGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); + expect(lineGeometry.transform).toEqual({ x: 25, y: 0 }); + }); + test('Can render two points', () => { + const [ + { + value: { points }, + }, + ] = lines; + + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 50, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); + }); + }); + describe('Multi series line chart - ordinal', () => { + const spec1Id = 'point1'; + const spec2Id = 'point2'; + const pointSeriesSpec1 = MockSeriesSpec.line({ + id: spec1Id, + groupId: GROUP_ID, + seriesType: SeriesType.Line, + data: [ + [0, 10], + [1, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }); + const pointSeriesSpec2 = MockSeriesSpec.line({ + id: spec2Id, + groupId: GROUP_ID, + data: [ + [0, 20], + [1, 10], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }); + + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec1, pointSeriesSpec2, settings], store); + const { + geometries: { lines }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + + test('Can render two ordinal lines', () => { + const [{ value: firstLine }, { value: secondLine }] = lines; + expect(firstLine.color).toBe('red'); + expect(firstLine.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstLine.seriesIdentifier.specId).toEqual(spec1Id); + expect(firstLine.transform).toEqual({ x: 25, y: 0 }); + + expect(secondLine.line).toBe('M0,0L50,50'); + expect(secondLine.color).toBe('blue'); + expect(secondLine.seriesIdentifier.seriesKeys).toEqual([1]); + expect(secondLine.seriesIdentifier.specId).toEqual(spec2Id); + expect(secondLine.transform).toEqual({ x: 25, y: 0 }); + }); + test('can render first spec points', () => { + const [ + { + value: { points }, + }, + ] = lines; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 50, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 75, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); + }); + test('can render second spec points', () => { + const [ + , + { + value: { points }, + }, + ] = lines; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 0, + y: 20, + mark: null, + datum: [0, 20], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 50, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 1, + y: 10, + mark: null, + datum: [1, 10], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); + }); + }); + describe('Single series line chart - linear', () => { + const pointSeriesSpec = MockSeriesSpec.line({ + id: SPEC_ID, + groupId: GROUP_ID, + data: [ + [0, 10], + [1, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + }); + + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec, settings], store); + const { + geometries: { lines }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + + test('Can render a linear line', () => { + const [{ value: renderedLine }] = lines; + expect(renderedLine.line).toBe('M0,0L100,50'); + expect(renderedLine.color).toBe('red'); + expect(renderedLine.seriesIdentifier.seriesKeys).toEqual([1]); + expect(renderedLine.seriesIdentifier.specId).toEqual(SPEC_ID); + expect(renderedLine.transform).toEqual({ x: 0, y: 0 }); + }); + test('Can render two points', () => { + const [ + { + value: { points }, + }, + ] = lines; + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); + }); + }); + describe('Multi series line chart - linear', () => { + const spec1Id = 'point1'; + const spec2Id = 'point2'; + const pointSeriesSpec1 = MockSeriesSpec.line({ + id: spec1Id, + groupId: GROUP_ID, + data: [ + [0, 10], + [1, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + }); + const pointSeriesSpec2 = MockSeriesSpec.line({ + id: spec2Id, + groupId: GROUP_ID, + data: [ + [0, 20], + [1, 10], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + }); + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec1, pointSeriesSpec2, settings], store); + const { + geometries: { lines }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + + test('can render two linear lines', () => { + const [{ value: firstLine }, { value: secondLine }] = lines; + expect(firstLine.line).toBe('M0,50L100,75'); + expect(firstLine.color).toBe('red'); + expect(firstLine.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstLine.seriesIdentifier.specId).toEqual(spec1Id); + expect(firstLine.transform).toEqual({ x: 0, y: 0 }); + + expect(secondLine.line).toBe('M0,0L100,50'); + expect(secondLine.color).toBe('blue'); + expect(secondLine.seriesIdentifier.seriesKeys).toEqual([1]); + expect(secondLine.seriesIdentifier.specId).toEqual(spec2Id); + expect(secondLine.transform).toEqual({ x: 0, y: 0 }); + }); + test('can render first spec points', () => { + const [ + { + value: { points }, + }, + ] = lines; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 50, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 75, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); + }); + test('can render second spec points', () => { + const [ + , + { + value: { points }, + }, + ] = lines; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 0, + y: 20, + mark: null, + datum: [0, 20], + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 1, + y: 10, + mark: null, + datum: [1, 10], + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); + }); + }); + describe('Single series line chart - time', () => { + const pointSeriesSpec = MockSeriesSpec.line({ + id: SPEC_ID, + groupId: GROUP_ID, + data: [ + [1546300800000, 10], + [1546387200000, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + }); + + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec, settings], store); + const { + geometries: { lines }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + + test('Can render a time line', () => { + const [{ value: renderedLine }] = lines; + expect(renderedLine.line).toBe('M0,0L100,50'); + expect(renderedLine.color).toBe('red'); + expect(renderedLine.seriesIdentifier.seriesKeys).toEqual([1]); + expect(renderedLine.seriesIdentifier.specId).toEqual(SPEC_ID); + expect(renderedLine.transform).toEqual({ x: 0, y: 0 }); + }); + test('Can render two points', () => { + const [ + { + value: { points }, + }, + ] = lines; + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1546300800000, + y: 10, + mark: null, + datum: [1546300800000, 10], + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1546387200000, + y: 5, + mark: null, + datum: [1546387200000, 5], + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); + }); + }); + describe('Multi series line chart - time', () => { + const spec1Id = 'point1'; + const spec2Id = 'point2'; + const pointSeriesSpec1 = MockSeriesSpec.line({ + id: spec1Id, + groupId: GROUP_ID, + data: [ + [1546300800000, 10], + [1546387200000, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + }); + const pointSeriesSpec2 = MockSeriesSpec.line({ + id: spec2Id, + groupId: GROUP_ID, + data: [ + [1546300800000, 20], + [1546387200000, 10], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + }); + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec1, pointSeriesSpec2, settings], store); + const { + geometries: { + lines: [firstLine, secondLine], + }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + + test('can render first spec points', () => { + const { + value: { points }, + } = firstLine; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 50, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 1546300800000, + y: 10, + mark: null, + datum: [1546300800000, 10], + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 75, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 1546387200000, + y: 5, + mark: null, + datum: [1546387200000, 5], + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); + }); + test('can render second spec points', () => { + const { + value: { points }, + } = secondLine; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 0, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 1546300800000, + y: 20, + mark: null, + datum: [1546300800000, 20], + }, + style: { + stroke: { + color: { + r: 0, + g: 0, + b: 255, + }, + }, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 1546387200000, + y: 10, + mark: null, + datum: [1546387200000, 10], + }, + style: { + stroke: { + color: { + r: 0, + g: 0, + b: 255, + }, + }, + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); + }); + }); + describe('Single series line chart - y log', () => { + const pointSeriesSpec = MockSeriesSpec.line({ + id: SPEC_ID, + groupId: GROUP_ID, + data: [ + [0, 10], + [1, 5], + [2, null], + [3, 5], + [4, 5], + [5, 0], + [6, 10], + [7, 10], + [8, 10], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Log, + }); + const store = MockStore.default({ width: 90, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec, settings], store); + const { + geometries: { lines }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + + test('should render a split line', () => { + const [{ value: renderedLine }] = lines; + expect(renderedLine.line.split('M').length - 1).toBe(3); + expect(renderedLine.color).toBe('red'); + expect(renderedLine.seriesIdentifier.seriesKeys).toEqual([1]); + expect(renderedLine.seriesIdentifier.specId).toEqual(SPEC_ID); + expect(renderedLine.transform).toEqual({ x: 0, y: 0 }); + }); + test('should render points', () => { + const [ + { + value: { points }, + }, + ] = lines; + // all the points minus the undefined ones on a log scale + expect(points.length).toBe(7); + // all the points expect null geometries + expect(geometriesIndex.size).toEqual(8); + const nullIndexdGeometry = geometriesIndex.find(2)!; + expect(nullIndexdGeometry).toEqual([]); + + const zeroValueIndexdGeometry = geometriesIndex.find(5)!; + expect(zeroValueIndexdGeometry).toBeDefined(); + expect(zeroValueIndexdGeometry.length).toBe(1); + // the zero value is moved vertically to infinity + expect((zeroValueIndexdGeometry[0] as PointGeometry).y).toBe(Infinity); + expect((zeroValueIndexdGeometry[0] as PointGeometry).radius).toBe(LIGHT_THEME.lineSeriesStyle.point.radius); + }); + }); + describe('Removing out-of-domain points', () => { + const pointSeriesSpec = MockSeriesSpec.line({ + id: SPEC_ID, + // groupId: GROUP_ID, + data: [ + [0, 0], + [1, 1], + [2, 10], + [3, 3], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + }); + const settings = MockGlobalSpec.settingsNoMargins({ + xDomain: { max: 2 }, + theme: { colors: { vizColors: ['red', 'blue'] } }, + }); + const axis = MockGlobalSpec.axis({ position: Position.Left, hide: true, domain: { max: 1 } }); + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs([pointSeriesSpec, axis, settings], store); + + const { + geometries: { lines }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + test('should render 3 points', () => { + const [ + { + value: { points }, + }, + ] = lines; + // will not render the 4th point is out of the x domain + expect(points.length).toBe(3); + // will keep the 3rd point as an indexedGeometry + expect(geometriesIndex.size).toEqual(3); + expect(points[0]).toEqual( + MockPointGeometry.default({ + x: 0, + y: 99.5, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 0, + y: 0, + mark: null, + datum: [0, 0], + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1, + y: 1, + mark: null, + datum: [1, 1], + }, + }), + ); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.test.ts b/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.test.ts new file mode 100644 index 000000000000..d62428750ca6 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.test.ts @@ -0,0 +1,599 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegendItem } from '../../../common/legend'; +import { MockBarGeometry, MockDataSeries, MockPointGeometry } from '../../../mocks'; +import { MockScale } from '../../../mocks/scale'; +import { mergePartial, RecursivePartial } from '../../../utils/common'; +import { BarSeriesStyle, SharedGeometryStateStyle, PointStyle } from '../../../utils/themes/theme'; +import { DataSeriesDatum, XYChartSeriesIdentifier } from '../utils/series'; +import { getBarStyleOverrides } from './bars'; +import { getPointStyleOverrides, getRadiusFn } from './points'; +import { getGeometryStateStyle, isPointOnGeometry, getClippedRanges } from './utils'; + +describe('Rendering utils', () => { + test('check if point is on geometry', () => { + const seriesStyle = { + rect: { + opacity: 1, + }, + rectBorder: { + strokeWidth: 1, + visible: false, + }, + displayValue: { + fill: 'black', + fontFamily: '', + fontSize: 2, + offsetX: 0, + offsetY: 0, + padding: 2, + }, + }; + + const geometry = MockBarGeometry.default({ + color: 'red', + seriesIdentifier: { + specId: 'id', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: [], + key: '', + }, + value: { + accessor: 'y1', + x: 0, + y: 0, + mark: null, + datum: { x: 0, y: 0 }, + }, + x: 0, + y: 0, + width: 10, + height: 10, + seriesStyle, + }); + expect(isPointOnGeometry(0, 0, geometry)).toBe(true); + expect(isPointOnGeometry(10, 10, geometry)).toBe(true); + expect(isPointOnGeometry(0, 10, geometry)).toBe(true); + expect(isPointOnGeometry(10, 0, geometry)).toBe(true); + expect(isPointOnGeometry(-10, 0, geometry)).toBe(false); + expect(isPointOnGeometry(-11, 0, geometry)).toBe(false); + expect(isPointOnGeometry(11, 11, geometry)).toBe(false); + }); + test('check if point is on point geometry', () => { + const geometry = MockPointGeometry.default({ + color: 'red', + seriesIdentifier: { + specId: 'id', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: [], + key: '', + }, + value: { + accessor: 'y1', + x: 0, + y: 0, + mark: null, + datum: { x: 0, y: 0 }, + }, + transform: { + x: 0, + y: 0, + }, + x: 0, + y: 0, + radius: 10, + }); + // with buffer + expect(isPointOnGeometry(10, 10, geometry, 10)).toBe(true); + expect(isPointOnGeometry(20, 20, geometry, 5)).toBe(false); + + // without buffer + expect(isPointOnGeometry(0, 0, geometry, 0)).toBe(true); + expect(isPointOnGeometry(0, 10, geometry, 0)).toBe(true); + expect(isPointOnGeometry(10, 0, geometry, 0)).toBe(true); + expect(isPointOnGeometry(11, 11, geometry, 0)).toBe(false); + expect(isPointOnGeometry(-10, 0, geometry, 0)).toBe(true); + expect(isPointOnGeometry(-11, 0, geometry, 0)).toBe(false); + expect(isPointOnGeometry(11, 11, geometry, 0)).toBe(false); + + // should use radial check + expect(isPointOnGeometry(9, 9, geometry, 0)).toBe(false); + expect(isPointOnGeometry(-9, 9, geometry, 0)).toBe(false); + expect(isPointOnGeometry(9, -9, geometry, 0)).toBe(false); + expect(isPointOnGeometry(-9, -9, geometry, 0)).toBe(false); + }); + + describe('should get common geometry style dependent on legend item highlight state', () => { + const seriesIdentifier: XYChartSeriesIdentifier = { + specId: 'id', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: [], + key: 'somekey', + }; + const highlightedLegendItem: LegendItem = { + color: '', + label: '', + seriesIdentifiers: [seriesIdentifier], + isSeriesHidden: false, + defaultExtra: { + formatted: null, + raw: null, + legendSizingLabel: null, + }, + path: [], + keys: [], + }; + + const unhighlightedLegendItem: LegendItem = { + ...highlightedLegendItem, + seriesIdentifiers: [ + { + ...seriesIdentifier, + key: 'not me', + }, + ], + keys: [], + }; + + const sharedThemeStyle: SharedGeometryStateStyle = { + default: { + opacity: 1, + }, + highlighted: { + opacity: 0.5, + }, + unhighlighted: { + opacity: 0.25, + }, + }; + + it('no highlighted elements', () => { + const defaultStyle = getGeometryStateStyle(seriesIdentifier, sharedThemeStyle); + expect(defaultStyle).toBe(sharedThemeStyle.default); + }); + + it('should equal highlighted opacity', () => { + const highlightedStyle = getGeometryStateStyle(seriesIdentifier, sharedThemeStyle, highlightedLegendItem); + expect(highlightedStyle).toBe(sharedThemeStyle.highlighted); + }); + + it('should equal unhighlighted when not highlighted item', () => { + const unhighlightedStyle = getGeometryStateStyle(seriesIdentifier, sharedThemeStyle, unhighlightedLegendItem); + expect(unhighlightedStyle).toBe(sharedThemeStyle.unhighlighted); + }); + + it('should equal custom spec highlighted opacity', () => { + const customHighlightedStyle = getGeometryStateStyle(seriesIdentifier, sharedThemeStyle, highlightedLegendItem); + expect(customHighlightedStyle).toBe(sharedThemeStyle.highlighted); + }); + + it('unhighlighted elements remain unchanged with custom opacity', () => { + const customUnhighlightedStyle = getGeometryStateStyle( + seriesIdentifier, + sharedThemeStyle, + unhighlightedLegendItem, + ); + expect(customUnhighlightedStyle).toBe(sharedThemeStyle.unhighlighted); + }); + + it('has individual highlight', () => { + const hasIndividualHighlight = getGeometryStateStyle(seriesIdentifier, sharedThemeStyle, undefined, { + hasHighlight: true, + hasGeometryHover: true, + }); + expect(hasIndividualHighlight).toBe(sharedThemeStyle.highlighted); + }); + + it('no highlight', () => { + const noHighlight = getGeometryStateStyle(seriesIdentifier, sharedThemeStyle, undefined, { + hasHighlight: false, + hasGeometryHover: true, + }); + expect(noHighlight).toBe(sharedThemeStyle.unhighlighted); + }); + + it('no geometry hover', () => { + const noHover = getGeometryStateStyle(seriesIdentifier, sharedThemeStyle, undefined, { + hasHighlight: true, + hasGeometryHover: false, + }); + expect(noHover).toBe(sharedThemeStyle.highlighted); + }); + }); + + describe('getBarStyleOverrides', () => { + let mockAccessor: jest.Mock; + + const sampleSeriesStyle: BarSeriesStyle = { + rect: { + opacity: 1, + }, + rectBorder: { + visible: true, + strokeWidth: 1, + }, + displayValue: { + fontSize: 10, + fontFamily: 'helvetica', + fill: 'blue', + padding: 1, + offsetX: 1, + offsetY: 1, + }, + }; + const datum: DataSeriesDatum = { + x: 1, + y1: 2, + y0: 3, + initialY1: 4, + initialY0: 5, + mark: null, + datum: null, + }; + const seriesIdentifier: XYChartSeriesIdentifier = { + specId: 'test', + yAccessor: 'test', + splitAccessors: new Map(), + seriesKeys: ['test'], + key: '', + }; + + beforeEach(() => { + mockAccessor = jest.fn(); + }); + + it('should return input seriesStyle if no barStyleAccessor is passed', () => { + const styleOverrides = getBarStyleOverrides(datum, seriesIdentifier, sampleSeriesStyle); + + expect(styleOverrides).toBe(sampleSeriesStyle); + }); + + it('should return input seriesStyle if barStyleAccessor returns null', () => { + mockAccessor.mockReturnValue(null); + const styleOverrides = getBarStyleOverrides(datum, seriesIdentifier, sampleSeriesStyle, mockAccessor); + + expect(styleOverrides).toBe(sampleSeriesStyle); + }); + + it('should call barStyleAccessor with datum and seriesIdentifier', () => { + getBarStyleOverrides(datum, seriesIdentifier, sampleSeriesStyle, mockAccessor); + + expect(mockAccessor).toBeCalledWith(datum, seriesIdentifier); + }); + + it('should return seriesStyle with updated fill color', () => { + const color = 'blue'; + mockAccessor.mockReturnValue(color); + const styleOverrides = getBarStyleOverrides(datum, seriesIdentifier, sampleSeriesStyle, mockAccessor); + const expectedStyles: BarSeriesStyle = { + ...sampleSeriesStyle, + rect: { + ...sampleSeriesStyle.rect, + fill: color, + }, + }; + expect(styleOverrides).toEqual(expectedStyles); + }); + + it('should return a new seriesStyle object with color', () => { + mockAccessor.mockReturnValue('blue'); + const styleOverrides = getBarStyleOverrides(datum, seriesIdentifier, sampleSeriesStyle, mockAccessor); + + expect(styleOverrides).not.toBe(sampleSeriesStyle); + }); + + it('should return seriesStyle with updated partial style', () => { + const partialStyle: RecursivePartial = { + rect: { + fill: 'blue', + }, + rectBorder: { + strokeWidth: 10, + }, + }; + mockAccessor.mockReturnValue(partialStyle); + const styleOverrides = getBarStyleOverrides(datum, seriesIdentifier, sampleSeriesStyle, mockAccessor); + const expectedStyles = mergePartial(sampleSeriesStyle, partialStyle, { + mergeOptionalPartialValues: true, + }); + + expect(styleOverrides).toEqual(expectedStyles); + }); + + it('should return a new seriesStyle object with partial styles', () => { + mockAccessor.mockReturnValue({ + rect: { + fill: 'blue', + }, + }); + const styleOverrides = getBarStyleOverrides(datum, seriesIdentifier, sampleSeriesStyle, mockAccessor); + + expect(styleOverrides).not.toBe(sampleSeriesStyle); + }); + }); + + describe('getPointStyleOverrides', () => { + let mockAccessor: jest.Mock; + + const datum: DataSeriesDatum = { + x: 1, + y1: 2, + y0: 3, + initialY1: 4, + initialY0: 5, + mark: null, + datum: null, + }; + const seriesIdentifier: XYChartSeriesIdentifier = { + specId: 'test', + yAccessor: 'test', + splitAccessors: new Map(), + seriesKeys: ['test'], + key: '', + }; + + beforeEach(() => { + mockAccessor = jest.fn(); + }); + + it('should return undefined if no pointStyleAccessor is passed', () => { + const styleOverrides = getPointStyleOverrides(datum, seriesIdentifier); + + expect(styleOverrides).toBeUndefined(); + }); + + it('should return undefined if pointStyleAccessor returns null', () => { + mockAccessor.mockReturnValue(null); + const styleOverrides = getPointStyleOverrides(datum, seriesIdentifier, mockAccessor); + + expect(styleOverrides).toBeUndefined(); + }); + + it('should call pointStyleAccessor with datum and seriesIdentifier', () => { + getPointStyleOverrides(datum, seriesIdentifier, mockAccessor); + + expect(mockAccessor).toBeCalledWith(datum, seriesIdentifier); + }); + + it('should return seriesStyle with updated stroke color', () => { + const stroke = 'blue'; + mockAccessor.mockReturnValue(stroke); + const styleOverrides = getPointStyleOverrides(datum, seriesIdentifier, mockAccessor); + const expectedStyles: Partial = { + stroke, + }; + expect(styleOverrides).toEqual(expectedStyles); + }); + }); + + describe('getClippedRanges', () => { + const dataSeries = MockDataSeries.fitFunction({ shuffle: false }); + + const xScale = MockScale.default({ + scale: jest.fn().mockImplementation((x) => x), + bandwidth: 0, + range: [dataSeries.data[0].x as number, dataSeries.data[12].x as number], + }); + + it('should return array pairs of non-null x regions with null end values', () => { + const actual = getClippedRanges(dataSeries.data, xScale, 0); + + expect(actual).toEqual([ + [0, 1], + [2, 4], + [4, 6], + [7, 11], + [11, 12], + ]); + }); + + it('should return array pairs of non-null x regions with valid end values', () => { + const data = dataSeries.data.slice(1, -1); + const xScale = MockScale.default({ + scale: jest.fn().mockImplementation((x) => x), + range: [data[0].x as number, data[10].x as number], + }); + const actual = getClippedRanges(data, xScale, 0); + + expect(actual).toEqual([ + [2, 4], + [4, 6], + [7, 11], + ]); + }); + + it('should account for bandwidth', () => { + const bandwidth = 2; + const xScale = MockScale.default({ + scale: jest.fn().mockImplementation((x) => x), + bandwidth, + range: [dataSeries.data[0].x as number, (dataSeries.data[12].x as number) + bandwidth * (2 / 3)], + }); + const actual = getClippedRanges(dataSeries.data, xScale, 0); + + expect(actual).toEqual([ + [0, 2], + [3, 5], + [5, 7], + [8, 12], + ]); + }); + + it('should account for xScaleOffset', () => { + const actual = getClippedRanges(dataSeries.data, xScale, 2); + + expect(actual).toEqual([ + [0, -1], + [0, 2], + [2, 4], + [5, 9], + ]); + }); + + it('should call scale to get x value for each datum', () => { + getClippedRanges(dataSeries.data, xScale, 0); + + expect(xScale.scale).toHaveBeenNthCalledWith(1, dataSeries.data[0].x); + expect(xScale.scale).toHaveBeenCalledTimes(dataSeries.data.length); + expect(xScale.scale).toHaveBeenCalledWith(dataSeries.data[12].x); + }); + }); + describe('#getRadiusFn', () => { + describe('empty data', () => { + const getRadius = getRadiusFn([], 1); + + it('should return a function', () => { + expect(getRadius).toBeFunction(); + }); + + it.each<[number, number]>([ + [0, 0], + [10, 10], + [1000, 1000], + [10000, 10000], + ])('should always return 0 - %#', (...args) => { + expect(getRadius(...args)).toBe(0); + }); + }); + + describe('default markSizeRatio', () => { + const { data } = MockDataSeries.random( + { + count: 20, + mark: { min: 500, max: 1000 }, + }, + true, + ); + const getRadius = getRadiusFn(data, 1); + + it('should return a function', () => { + expect(getRadius).toBeFunction(); + }); + + describe('Dataset validations', () => { + const expectedValues = [ + 15.29, + 40.89, + 13.39, + 36.81, + 44.66, + 44.34, + 51.01, + 6.97, + 34.04, + 49.07, + 45.11, + 25.44, + 8.98, + 9.33, + 50.62, + 48.89, + 44.34, + 1, + 33.09, + 5.94, + ]; + it.each<[number | null, number]>(data.map(({ mark }, i) => [mark, expectedValues[i]]))( + 'should return stepped value from domain - data[%#]', + (mark, expected) => { + expect(getRadius(mark)).toBeCloseTo(expected, 1); + }, + ); + }); + + it('should return default values when mark is null', () => { + expect(getRadius(null, 111)).toBe(111); + }); + }); + + describe('variable markSizeRatio', () => { + const { data } = MockDataSeries.random( + { + count: 5, + mark: { min: 0, max: 100 }, + }, + true, + ); + + describe('markSizeRatio - -100', () => { + // Should be treated as 0 + const getRadius = getRadiusFn(data, 1, -100); + it.each<[number | null]>(data.map(({ mark }) => [mark]))('should return stepped value - data[%#]', (mark) => { + expect(getRadius(mark)).toBe(1); + }); + }); + + describe('markSizeRatio - 0', () => { + const getRadius = getRadiusFn(data, 1, 0); + it.each<[number | null]>(data.map(({ mark }) => [mark]))('should return stepped value - data[%#]', (mark) => { + expect(getRadius(mark)).toBe(1); + }); + }); + + describe('markSizeRatio - 1', () => { + const getRadius = getRadiusFn(data, 1, 1); + const expectedRadii = [2.62, 2.59, 1, 2.73, 2.63]; + it.each<[number | null, number]>(data.map(({ mark }, i) => [mark, expectedRadii[i]]))( + 'should return stepped value - data[%#]', + (mark, expected) => { + expect(getRadius(mark)).toBeCloseTo(expected, 1); + }, + ); + }); + + describe('markSizeRatio - 10', () => { + const getRadius = getRadiusFn(data, 1, 10); + const expectedRadii = [9.09, 8.56, 1, 11.1, 9.38]; + it.each<[number | null, number]>(data.map(({ mark }, i) => [mark, expectedRadii[i]]))( + 'should return stepped value - data[%#]', + (mark, expected) => { + expect(getRadius(mark)).toBeCloseTo(expected, 1); + }, + ); + }); + + describe('markSizeRatio - 100', () => { + const getRadius = getRadiusFn(data, 1, 100); + const expectedRadii = [80.71, 75.37, 1, 101, 83.61]; + it.each<[number | null, number]>(data.map(({ mark }, i) => [mark, expectedRadii[i]]))( + 'should return stepped value - data[%#]', + (mark, expected) => { + expect(getRadius(mark)).toBeCloseTo(expected, 1); + }, + ); + }); + + describe('markSizeRatio - 1000', () => { + // Should be treated as 100 + const getRadius = getRadiusFn(data, 1, 1000); + const expectedRadii = [80.71, 75.37, 1, 101, 83.61]; + it.each<[number | null, number]>(data.map(({ mark }, i) => [mark, expectedRadii[i]]))( + 'should return stepped value - data[%#]', + (mark, expected) => { + expect(getRadius(mark)).toBeCloseTo(expected, 1); + }, + ); + }); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/rendering/utils.ts b/packages/osd-charts/src/chart_types/xy_chart/rendering/utils.ts new file mode 100644 index 000000000000..bcdcb1d0884f --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/rendering/utils.ts @@ -0,0 +1,241 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegendItem } from '../../../common/legend'; +import { Scale } from '../../../scales'; +import { getDomainPolarity } from '../../../scales/scale_continuous'; +import { isLogarithmicScale } from '../../../scales/types'; +import { MarkBuffer } from '../../../specs'; +import { getDistance } from '../../../utils/common'; +import { BarGeometry, ClippedRanges, isPointGeometry, PointGeometry } from '../../../utils/geometry'; +import { GeometryStateStyle, SharedGeometryStateStyle } from '../../../utils/themes/theme'; +import { DataSeriesDatum, FilledValues, XYChartSeriesIdentifier } from '../utils/series'; +import { DEFAULT_HIGHLIGHT_PADDING } from './constants'; + +/** @internal */ +export interface MarkSizeOptions { + enabled: boolean; + ratio?: number; +} + +/** + * Returns value of `y1` or `filled.y1` or null by default. + * Passing a filled key (x, y1, y0) it will return that value or the filled one + * @internal + */ +export function getYDatumValueFn(valueName: keyof Omit = 'y1') { + return (datum: DataSeriesDatum, returnFilled = true): number | null => { + const value = datum[valueName]; + if (value !== null || !returnFilled) { + return value; + } + return datum.filled?.[valueName] ?? null; + }; +} + +/** + * + * @param param0 + * @internal + */ +export function isDatumFilled({ filled, initialY1 }: DataSeriesDatum) { + return filled?.x !== undefined || filled?.y1 !== undefined || initialY1 === null || initialY1 === undefined; +} + +/** + * Gets clipped ranges that have been fitted to values + * @param dataset + * @param xScale + * @param xScaleOffset + * @internal + */ +export function getClippedRanges(dataset: DataSeriesDatum[], xScale: Scale, xScaleOffset: number): ClippedRanges { + let firstNonNullX: number | null = null; + let hasNull = false; + + const completeDatasetIsNull = dataset.every((datum) => isDatumFilled(datum)); + + if (completeDatasetIsNull) return [[xScale.range[0], xScale.range[1]]]; + + return dataset.reduce((acc, data) => { + const xScaled = xScale.scale(data.x); + if (xScaled === null) { + return acc; + } + + const xValue = xScaled - xScaleOffset + xScale.bandwidth / 2; + + if (isDatumFilled(data)) { + const endXValue = xScale.range[1] - xScale.bandwidth * (2 / 3); + if (firstNonNullX !== null && xValue === endXValue) { + acc.push([firstNonNullX, xValue]); + } + hasNull = true; + } else { + if (hasNull) { + if (firstNonNullX !== null) { + acc.push([firstNonNullX, xValue]); + } else { + acc.push([0, xValue]); + } + hasNull = false; + } + + firstNonNullX = xValue; + } + return acc; + }, []); +} + +/** @internal */ +export function getGeometryStateStyle( + seriesIdentifier: XYChartSeriesIdentifier, + sharedGeometryStyle: SharedGeometryStateStyle, + highlightedLegendItem?: LegendItem, + individualHighlight?: { [key: string]: boolean }, +): GeometryStateStyle { + const { default: defaultStyles, highlighted, unhighlighted } = sharedGeometryStyle; + + if (highlightedLegendItem) { + const isPartOfHighlightedSeries = highlightedLegendItem.seriesIdentifiers.some( + ({ key }) => key === seriesIdentifier.key, + ); + + return isPartOfHighlightedSeries ? highlighted : unhighlighted; + } + + if (individualHighlight) { + const { hasHighlight, hasGeometryHover } = individualHighlight; + if (!hasGeometryHover) { + return highlighted; + } + return hasHighlight ? highlighted : unhighlighted; + } + + return defaultStyles; +} + +/** @internal */ +export function isPointOnGeometry( + xCoordinate: number, + yCoordinate: number, + indexedGeometry: BarGeometry | PointGeometry, + buffer: MarkBuffer = DEFAULT_HIGHLIGHT_PADDING, +) { + const { x, y, transform } = indexedGeometry; + if (isPointGeometry(indexedGeometry)) { + const { radius } = indexedGeometry; + const distance = getDistance( + { + x: xCoordinate, + y: yCoordinate, + }, + { + x: x + transform.x, + y: y + transform.y, + }, + ); + + const radiusBuffer = typeof buffer === 'number' ? buffer : buffer(radius); + + if (radiusBuffer === Infinity) { + return distance <= radius + DEFAULT_HIGHLIGHT_PADDING; + } + + return distance <= radius + radiusBuffer; + } + const { width, height } = indexedGeometry; + return yCoordinate >= y && yCoordinate <= y + height && xCoordinate >= x && xCoordinate <= x + width; +} + +/** + * The default zero baseline for area charts. + */ +const DEFAULT_ZERO_BASELINE = 0; + +/** @internal */ +export type YDefinedFn = ( + datum: DataSeriesDatum, + getValueAccessor: (datum: DataSeriesDatum) => number | null, +) => boolean; + +/** @internal */ +export function isYValueDefinedFn(yScale: Scale, xScale: Scale): YDefinedFn { + const isLogScale = isLogarithmicScale(yScale); + const domainPolarity = getDomainPolarity(yScale.domain); + return (datum, getValueAccessor) => { + const yValue = getValueAccessor(datum); + return ( + yValue !== null && + !((isLogScale && domainPolarity >= 0 && yValue <= 0) || (domainPolarity < 0 && yValue >= 0)) && + xScale.isValueInDomain(datum.x) + ); + }; +} + +/** @internal */ +export const CHROME_PINCH_BUG_EPSILON = 0.5; +/** + * Temporary fix for Chromium bug + * Shift a small pixel value when pixel diff is <= 0.5px + * https://github.com/elastic/elastic-charts/issues/1053 + * https://bugs.chromium.org/p/chromium/issues/detail?id=1163912 + */ +function chromeRenderBugBuffer(y1: number, y0: number): number { + const diff = Math.abs(y1 - y0); + return diff <= CHROME_PINCH_BUG_EPSILON ? 0.5 : 0; +} + +/** @internal */ +export function getY1ScaledValueOrThrowFn(yScale: Scale): (datum: DataSeriesDatum) => number { + const datumAccessor = getYDatumValueFn(); + const scaleY0Value = getY0ScaledValueOrThrowFn(yScale); + return (datum) => { + const y1Value = yScale.scaleOrThrow(datumAccessor(datum)); + const y0Value = scaleY0Value(datum); + return y1Value - chromeRenderBugBuffer(y1Value, y0Value); + }; +} + +/** @internal */ +export function getY0ScaledValueOrThrowFn(yScale: Scale): (datum: DataSeriesDatum) => number { + const isLogScale = isLogarithmicScale(yScale); + const domainPolarity = getDomainPolarity(yScale.domain); + const logBaseline = domainPolarity >= 0 ? Math.min(...yScale.domain) : Math.max(...yScale.domain); + + return ({ y0 }) => { + if (y0 === null) { + if (isLogScale) { + // if all positive domain use 1 as baseline, -1 otherwise + return yScale.scaleOrThrow(logBaseline); + } + return yScale.scaleOrThrow(DEFAULT_ZERO_BASELINE); + } + if (isLogScale) { + // wrong y0 polarity + if ((domainPolarity >= 0 && y0 <= 0) || (domainPolarity < 0 && y0 >= 0)) { + // if all positive domain use 1 as baseline, -1 otherwise + return yScale.scaleOrThrow(logBaseline); + } + // if negative value, use -1 as max reference, 1 otherwise + return yScale.scaleOrThrow(y0); + } + return yScale.scaleOrThrow(y0); + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/scales/get_api_scales.ts b/packages/osd-charts/src/chart_types/xy_chart/scales/get_api_scales.ts new file mode 100644 index 000000000000..f43713c2c255 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/scales/get_api_scales.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ScaleContinuousType } from '../../../scales'; +import { BasicSeriesSpec, XScaleType } from '../utils/specs'; +import { X_SCALE_DEFAULT, Y_SCALE_DEFAULT } from './scale_defaults'; + +/** @internal */ +export function getXScaleTypeFromSpec(type?: BasicSeriesSpec['xScaleType']): XScaleType { + return type ?? X_SCALE_DEFAULT.type; +} + +/** @internal */ +export function getXNiceFromSpec(nice?: BasicSeriesSpec['xNice']): boolean { + return nice ?? X_SCALE_DEFAULT.nice; +} + +/** @internal */ +export function getYScaleTypeFromSpec(type?: BasicSeriesSpec['yScaleType']): ScaleContinuousType { + return type ?? Y_SCALE_DEFAULT.type; +} + +/** @internal */ +export function getYNiceFromSpec(nice?: BasicSeriesSpec['yNice']): boolean { + return nice ?? Y_SCALE_DEFAULT.nice; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/scales/scale_defaults.ts b/packages/osd-charts/src/chart_types/xy_chart/scales/scale_defaults.ts new file mode 100644 index 000000000000..7783e69853ed --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/scales/scale_defaults.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ScaleType } from '../../../scales/constants'; + +/** @internal */ +export const X_SCALE_DEFAULT = { + type: ScaleType.Ordinal, + nice: false, + desiredTickCount: 10, +}; + +/** @internal */ +export const Y_SCALE_DEFAULT = { + type: ScaleType.Linear, + nice: false, + desiredTickCount: 10, + constrainDomainPadding: undefined, + domainPixelPadding: 0, + logBase: undefined, + logMinLimit: undefined, +}; diff --git a/packages/osd-charts/src/chart_types/xy_chart/specs/area_series.tsx b/packages/osd-charts/src/chart_types/xy_chart/specs/area_series.tsx new file mode 100644 index 000000000000..6151140da646 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/specs/area_series.tsx @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { ChartType } from '../..'; +import { ScaleType } from '../../../scales/constants'; +import { SpecType } from '../../../specs/constants'; +import { specComponentFactory, getConnect } from '../../../state/spec_factory'; +import { AreaSeriesSpec, HistogramModeAlignments, DEFAULT_GLOBAL_ID, SeriesType } from '../utils/specs'; + +const defaultProps = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + seriesType: SeriesType.Area, + groupId: DEFAULT_GLOBAL_ID, + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + hideInLegend: false, + histogramModeAlignment: HistogramModeAlignments.Center, +}; + +type SpecRequiredProps = Pick; +type SpecOptionalProps = Partial>; + +/** @public */ +export const AreaSeries: React.FunctionComponent = getConnect()( + specComponentFactory< + AreaSeriesSpec, + | 'seriesType' + | 'groupId' + | 'xScaleType' + | 'yScaleType' + | 'xAccessor' + | 'yAccessors' + | 'hideInLegend' + | 'histogramModeAlignment' + >(defaultProps), +); diff --git a/packages/osd-charts/src/chart_types/xy_chart/specs/axis.tsx b/packages/osd-charts/src/chart_types/xy_chart/specs/axis.tsx new file mode 100644 index 000000000000..df76c1170bfa --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/specs/axis.tsx @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { ChartType } from '../..'; +import { SpecType } from '../../../specs/constants'; +import { specComponentFactory, getConnect } from '../../../state/spec_factory'; +import { Position } from '../../../utils/common'; +import { AxisSpec, DEFAULT_GLOBAL_ID } from '../utils/specs'; + +const defaultProps = { + chartType: ChartType.XYAxis, + specType: SpecType.Axis, + groupId: DEFAULT_GLOBAL_ID, + hide: false, + showOverlappingTicks: false, + showOverlappingLabels: false, + position: Position.Left, +}; + +type SpecRequired = Pick; +type SpecOptionals = Partial>; + +/** @public */ +export const Axis: React.FunctionComponent = getConnect()( + specComponentFactory( + defaultProps, + ), +); diff --git a/packages/osd-charts/src/chart_types/xy_chart/specs/bar_series.tsx b/packages/osd-charts/src/chart_types/xy_chart/specs/bar_series.tsx new file mode 100644 index 000000000000..b0862fdd6d47 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/specs/bar_series.tsx @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { ChartType } from '../..'; +import { ScaleType } from '../../../scales/constants'; +import { SpecType } from '../../../specs/constants'; +import { specComponentFactory, getConnect } from '../../../state/spec_factory'; +import { BarSeriesSpec, DEFAULT_GLOBAL_ID, SeriesType } from '../utils/specs'; + +const defaultProps = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + seriesType: SeriesType.Bar, + groupId: DEFAULT_GLOBAL_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + hideInLegend: false, + enableHistogramMode: false, +}; + +type SpecRequiredProps = Pick; +type SpecOptionalProps = Partial>; + +/** @public */ +export const BarSeries: React.FunctionComponent = getConnect()( + specComponentFactory< + BarSeriesSpec, + | 'seriesType' + | 'groupId' + | 'xScaleType' + | 'yScaleType' + | 'xAccessor' + | 'yAccessors' + | 'hideInLegend' + | 'enableHistogramMode' + >(defaultProps), +); diff --git a/packages/osd-charts/src/chart_types/xy_chart/specs/bubble_series.tsx b/packages/osd-charts/src/chart_types/xy_chart/specs/bubble_series.tsx new file mode 100644 index 000000000000..9fc7e2cb44b5 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/specs/bubble_series.tsx @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { ChartType } from '../..'; +import { ScaleType } from '../../../scales/constants'; +import { SpecType } from '../../../specs/constants'; +import { specComponentFactory, getConnect } from '../../../state/spec_factory'; +import { BubbleSeriesSpec, DEFAULT_GLOBAL_ID, SeriesType } from '../utils/specs'; + +const defaultProps = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + seriesType: SeriesType.Bubble, + groupId: DEFAULT_GLOBAL_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + hideInLegend: false, +}; +type SpecRequiredProps = Pick; +type SpecOptionalProps = Partial>; + +/** + * @alpha + * + * This series type uses a spatial index that is incompatible with other series types. This will + * be fixed once an update has been made to the tooltip design. + * + * When used alone with other `BubbleSeries` the spatial index will be used. However when + * mixed with other series types, the linear index will be used. This will affect highlighting + * of points when using the `markSizeAccessor`. + */ +export const BubbleSeries: React.FunctionComponent = getConnect()( + specComponentFactory< + BubbleSeriesSpec, + 'seriesType' | 'groupId' | 'xScaleType' | 'yScaleType' | 'xAccessor' | 'yAccessors' | 'hideInLegend' + >(defaultProps), +); diff --git a/packages/osd-charts/src/chart_types/xy_chart/specs/histogram_bar_series.tsx b/packages/osd-charts/src/chart_types/xy_chart/specs/histogram_bar_series.tsx new file mode 100644 index 000000000000..5724a49c8ffa --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/specs/histogram_bar_series.tsx @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { ChartType } from '../..'; +import { ScaleType } from '../../../scales/constants'; +import { SpecType } from '../../../specs/constants'; +import { specComponentFactory, getConnect } from '../../../state/spec_factory'; +import { HistogramBarSeriesSpec, DEFAULT_GLOBAL_ID, SeriesType } from '../utils/specs'; + +const defaultProps = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + seriesType: SeriesType.Bar, + groupId: DEFAULT_GLOBAL_ID, + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + hideInLegend: false, + enableHistogramMode: true as const, +}; + +type SpecRequiredProps = Pick; +type SpecOptionalProps = Partial>; + +/** @public */ +export const HistogramBarSeries: React.FunctionComponent = getConnect()( + specComponentFactory< + HistogramBarSeriesSpec, + | 'seriesType' + | 'groupId' + | 'xScaleType' + | 'yScaleType' + | 'xAccessor' + | 'yAccessors' + | 'hideInLegend' + | 'enableHistogramMode' + >(defaultProps), +); diff --git a/packages/osd-charts/src/chart_types/xy_chart/specs/index.ts b/packages/osd-charts/src/chart_types/xy_chart/specs/index.ts new file mode 100644 index 000000000000..c47871fcd57d --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/specs/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { AreaSeries } from './area_series'; +export { Axis } from './axis'; +export { BarSeries } from './bar_series'; +export { BubbleSeries } from './bubble_series'; +export { HistogramBarSeries } from './histogram_bar_series'; +export { LineAnnotation } from './line_annotation'; +export { LineSeries } from './line_series'; +export { RectAnnotation } from './rect_annotation'; diff --git a/packages/osd-charts/src/chart_types/xy_chart/specs/line_annotation.test.tsx b/packages/osd-charts/src/chart_types/xy_chart/specs/line_annotation.test.tsx new file mode 100644 index 000000000000..57e4277451a6 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/specs/line_annotation.test.tsx @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { createStore, Store } from 'redux'; + +import { LineAnnotation, AnnotationDomainType } from '../../../specs'; +import { SpecsParser } from '../../../specs/specs_parser'; +import { chartStoreReducer, GlobalChartState } from '../../../state/chart_state'; +import { LineSeries } from './line_series'; + +function LineAnnotationChart(props: { chartStore: Store }) { + return ( + + + + + + + ); +} + +describe('Line annotation', () => { + it('Should always be available on the on every render', () => { + const storeReducer = chartStoreReducer('chart_id'); + const chartStore = createStore(storeReducer); + const wrapper = mount(); + expect(chartStore.getState().specs.threshold).toBeDefined(); + wrapper.setProps({}); + expect(chartStore.getState().specs.threshold).toBeDefined(); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/specs/line_annotation.tsx b/packages/osd-charts/src/chart_types/xy_chart/specs/line_annotation.tsx new file mode 100644 index 000000000000..85e208366627 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/specs/line_annotation.tsx @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { ChartType } from '../..'; +import { SpecType } from '../../../specs/constants'; +import { getConnect, specComponentFactory } from '../../../state/spec_factory'; +import { DEFAULT_ANNOTATION_LINE_STYLE } from '../../../utils/themes/merge_utils'; +import { LineAnnotationSpec, DEFAULT_GLOBAL_ID, AnnotationType } from '../utils/specs'; + +const defaultProps = { + chartType: ChartType.XYAxis, + specType: SpecType.Annotation, + groupId: DEFAULT_GLOBAL_ID, + annotationType: AnnotationType.Line, + style: DEFAULT_ANNOTATION_LINE_STYLE, + hideLines: false, + hideTooltips: false, + hideLinesTooltips: true, + zIndex: 1, +}; + +type SpecRequiredProps = Pick; +type SpecOptionalProps = Partial< + Omit< + LineAnnotationSpec, + 'chartType' | 'specType' | 'seriesType' | 'id' | 'dataValues' | 'domainType' | 'annotationType' + > +>; + +/** @public */ +export const LineAnnotation: React.FunctionComponent = getConnect()( + specComponentFactory(defaultProps), +); diff --git a/packages/osd-charts/src/chart_types/xy_chart/specs/line_series.tsx b/packages/osd-charts/src/chart_types/xy_chart/specs/line_series.tsx new file mode 100644 index 000000000000..950b9ddc032f --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/specs/line_series.tsx @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { ChartType } from '../..'; +import { ScaleType } from '../../../scales/constants'; +import { SpecType } from '../../../specs/constants'; +import { specComponentFactory, getConnect } from '../../../state/spec_factory'; +import { LineSeriesSpec, DEFAULT_GLOBAL_ID, HistogramModeAlignments, SeriesType } from '../utils/specs'; + +const defaultProps = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + seriesType: SeriesType.Line, + groupId: DEFAULT_GLOBAL_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + hideInLegend: false, + histogramModeAlignment: HistogramModeAlignments.Center, +}; +type SpecRequiredProps = Pick; +type SpecOptionalProps = Partial>; + +/** @public */ +export const LineSeries: React.FunctionComponent = getConnect()( + specComponentFactory< + LineSeriesSpec, + | 'seriesType' + | 'groupId' + | 'xScaleType' + | 'yScaleType' + | 'xAccessor' + | 'yAccessors' + | 'hideInLegend' + | 'histogramModeAlignment' + >(defaultProps), +); diff --git a/packages/osd-charts/src/chart_types/xy_chart/specs/rect_annotation.tsx b/packages/osd-charts/src/chart_types/xy_chart/specs/rect_annotation.tsx new file mode 100644 index 000000000000..fc7d1281de4f --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/specs/rect_annotation.tsx @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { ChartType } from '../..'; +import { SpecType } from '../../../specs/constants'; +import { specComponentFactory, getConnect } from '../../../state/spec_factory'; +import { DEFAULT_ANNOTATION_RECT_STYLE } from '../../../utils/themes/merge_utils'; +import { RectAnnotationSpec, DEFAULT_GLOBAL_ID, AnnotationType } from '../utils/specs'; + +const defaultProps = { + chartType: ChartType.XYAxis, + specType: SpecType.Annotation, + groupId: DEFAULT_GLOBAL_ID, + annotationType: AnnotationType.Rectangle, + zIndex: -1, + style: DEFAULT_ANNOTATION_RECT_STYLE, +}; + +/** @public */ +export const RectAnnotation: React.FunctionComponent< + Pick & + Partial< + Omit< + RectAnnotationSpec, + 'chartType' | 'specType' | 'seriesType' | 'id' | 'dataValues' | 'domainType' | 'annotationType' + > + > +> = getConnect()( + specComponentFactory(defaultProps), +); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.accessibility.test.ts b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.accessibility.test.ts new file mode 100644 index 000000000000..29d82bc4dc36 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.accessibility.test.ts @@ -0,0 +1,161 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Store } from 'redux'; + +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store/store'; +import { GlobalChartState } from '../../../state/chart_state'; +import { DEFAULT_A11Y_SETTINGS } from '../../../state/selectors/get_accessibility_config'; +import { getSettingsSpecSelector } from '../../../state/selectors/get_settings_specs'; + +describe('test accessibility prop defaults', () => { + let store: Store; + beforeEach(() => { + store = MockStore.default(); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + data: [ + { x: 1, y: 10 }, + { x: 2, y: 5 }, + ], + }), + MockGlobalSpec.settings(), + ], + store, + ); + }); + it('should test defaults', () => { + const state = store.getState(); + const { + ariaDescription, + ariaUseDefaultSummary, + ariaLabelHeadingLevel, + ariaLabel, + ariaLabelledBy, + } = getSettingsSpecSelector(state); + expect(ariaDescription).toBeUndefined(); + expect(ariaUseDefaultSummary).toBeTrue(); + expect(ariaLabelHeadingLevel).toBe(DEFAULT_A11Y_SETTINGS.labelHeadingLevel); + expect(ariaLabel).toBeUndefined(); + expect(ariaLabelledBy).toBeUndefined(); + }); +}); +describe('custom description for screen readers', () => { + let store: Store; + beforeEach(() => { + store = MockStore.default(); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + data: [ + { x: 1, y: 10 }, + { x: 2, y: 5 }, + ], + }), + MockGlobalSpec.settings(), + ], + store, + ); + }); + it('should allow user to set a custom description for chart', () => { + MockStore.addSpecs( + [ + MockGlobalSpec.settings({ + ariaDescription: 'This is sample Kibana data', + }), + ], + store, + ); + const state = store.getState(); + const { ariaDescription } = getSettingsSpecSelector(state); + expect(ariaDescription).toBe('This is sample Kibana data'); + }); + it('should be able to disable generated descriptions', () => { + MockStore.addSpecs( + [ + MockGlobalSpec.settings({ + ariaUseDefaultSummary: false, + }), + ], + store, + ); + const state = store.getState(); + const { ariaUseDefaultSummary } = getSettingsSpecSelector(state); + expect(ariaUseDefaultSummary).toBe(false); + }); +}); +describe('custom labels for screen readers', () => { + let store: Store; + beforeEach(() => { + store = MockStore.default(); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + data: [ + { x: 1, y: 10 }, + { x: 2, y: 5 }, + ], + }), + MockGlobalSpec.settings(), + ], + store, + ); + }); + it('should allow label set by the user', () => { + MockStore.addSpecs( + [ + MockGlobalSpec.settings({ + ariaLabel: 'Label set by user', + }), + ], + store, + ); + const state = store.getState(); + const { ariaLabel } = getSettingsSpecSelector(state); + expect(ariaLabel).toBe('Label set by user'); + }); + it('should allow labelledBy set by the user', () => { + MockStore.addSpecs( + [ + MockGlobalSpec.settings({ + ariaLabelledBy: 'label-id', + }), + ], + store, + ); + const state = store.getState(); + const { ariaLabelledBy } = getSettingsSpecSelector(state); + expect(ariaLabelledBy).toBe('label-id'); + }); + it('should allow users to specify valid heading levels', () => { + MockStore.addSpecs( + [ + MockGlobalSpec.settings({ + ariaLabelHeadingLevel: 'h5', + }), + ], + store, + ); + const state = store.getState(); + const { ariaLabelHeadingLevel } = getSettingsSpecSelector(state); + expect(ariaLabelHeadingLevel).toBe('h5'); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.interactions.test.ts b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.interactions.test.ts new file mode 100644 index 000000000000..f0b3134c2738 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.interactions.test.ts @@ -0,0 +1,1113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// eslint-disable-next-line eslint-comments/disable-enable-pair +/* eslint-disable jest/no-conditional-expect */ + +import { Store } from 'redux'; + +import { ChartType } from '../..'; +import { Rect } from '../../../geoms/types'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs/specs'; +import { MockStore } from '../../../mocks/store'; +import { ScaleType } from '../../../scales/constants'; +import { SettingsSpec, XScaleType, XYBrushArea } from '../../../specs'; +import { SpecType, TooltipType, BrushAxis } from '../../../specs/constants'; +import { onExternalPointerEvent } from '../../../state/actions/events'; +import { onPointerMove, onMouseDown, onMouseUp } from '../../../state/actions/mouse'; +import { GlobalChartState } from '../../../state/chart_state'; +import { getSettingsSpecSelector } from '../../../state/selectors/get_settings_specs'; +import { Position, RecursivePartial } from '../../../utils/common'; +import { AxisStyle } from '../../../utils/themes/theme'; +import { BarSeriesSpec, BasicSeriesSpec, AxisSpec, SeriesType } from '../utils/specs'; +import { computeSeriesGeometriesSelector } from './selectors/compute_series_geometries'; +import { getCursorBandPositionSelector } from './selectors/get_cursor_band'; +import { getProjectedPointerPositionSelector } from './selectors/get_projected_pointer_position'; +import { + getHighlightedGeomsSelector, + getTooltipInfoAndGeometriesSelector, +} from './selectors/get_tooltip_values_highlighted_geoms'; +import { isTooltipVisibleSelector } from './selectors/is_tooltip_visible'; +import { createOnBrushEndCaller } from './selectors/on_brush_end_caller'; +import { createOnClickCaller } from './selectors/on_click_caller'; +import { createOnElementOutCaller } from './selectors/on_element_out_caller'; +import { createOnElementOverCaller } from './selectors/on_element_over_caller'; +import { createOnPointerMoveCaller } from './selectors/on_pointer_move_caller'; + +const SPEC_ID = 'spec_1'; +const GROUP_ID = 'group_1'; + +const ordinalBarSeries = MockSeriesSpec.bar({ + id: SPEC_ID, + groupId: GROUP_ID, + data: [ + [0, 10], + [1, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + hideInLegend: false, +}); +const linearBarSeries = MockSeriesSpec.bar({ + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: SPEC_ID, + groupId: GROUP_ID, + seriesType: SeriesType.Bar, + data: [ + [0, 10], + [1, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + hideInLegend: false, +}); +const chartTop = 10; +const chartLeft = 10; +const settingSpec = MockGlobalSpec.settings({ + tooltip: { + type: TooltipType.VerticalCursor, + }, + hideDuplicateAxes: false, + theme: { + chartPaddings: { top: 0, left: 0, bottom: 0, right: 0 }, + chartMargins: { top: 10, left: 10, bottom: 0, right: 0 }, + scales: { + barsPadding: 0, + }, + }, +}); + +function initStore(spec: BasicSeriesSpec) { + const store = MockStore.default({ width: 100, height: 100, top: chartTop, left: chartLeft }, 'chartId'); + MockStore.addSpecs([settingSpec, spec], store); + return store; +} + +describe('Chart state pointer interactions', () => { + let store: Store; + const onElementOutCaller = createOnElementOutCaller(); + const onElementOverCaller = createOnElementOverCaller(); + beforeEach(() => { + store = initStore(ordinalBarSeries); + }); + test('check initial geoms', () => { + const { geometries } = computeSeriesGeometriesSelector(store.getState()); + expect(geometries).toBeDefined(); + expect(geometries.bars).toBeDefined(); + expect(geometries.bars[0].value.length).toBe(2); + }); + + test('can convert/limit mouse pointer positions relative to chart projection', () => { + store.dispatch(onPointerMove({ x: 20, y: 20 }, 0)); + let projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); + expect(projectedPointerPosition.x).toBe(10); + expect(projectedPointerPosition.y).toBe(10); + + store.dispatch(onPointerMove({ x: 10, y: 10 }, 1)); + projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); + expect(projectedPointerPosition.x).toBe(0); + expect(projectedPointerPosition.y).toBe(0); + store.dispatch(onPointerMove({ x: 5, y: 5 }, 2)); + projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); + expect(projectedPointerPosition.x).toBe(-1); + expect(projectedPointerPosition.y).toBe(-1); + store.dispatch(onPointerMove({ x: 200, y: 20 }, 3)); + projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); + expect(projectedPointerPosition.x).toBe(-1); + expect(projectedPointerPosition.y).toBe(10); + store.dispatch(onPointerMove({ x: 20, y: 200 }, 4)); + projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); + expect(projectedPointerPosition.x).toBe(10); + expect(projectedPointerPosition.y).toBe(-1); + store.dispatch(onPointerMove({ x: 200, y: 200 }, 5)); + projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); + expect(projectedPointerPosition.x).toBe(-1); + expect(projectedPointerPosition.y).toBe(-1); + store.dispatch(onPointerMove({ x: -20, y: -20 }, 6)); + projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); + expect(projectedPointerPosition.x).toBe(-1); + expect(projectedPointerPosition.y).toBe(-1); + }); + + test('call onElementOut if moving the mouse out from the chart', () => { + const onOutListener = jest.fn((): undefined => undefined); + const settingsWithListeners: SettingsSpec = { + ...settingSpec, + onElementOut: onOutListener, + }; + + MockStore.addSpecs([ordinalBarSeries, settingsWithListeners], store); + // registering the out/over listener caller + store.subscribe(() => { + onElementOutCaller(store.getState()); + onElementOverCaller(store.getState()); + }); + store.dispatch(onPointerMove({ x: 20, y: 20 }, 0)); + expect(onOutListener).toBeCalledTimes(0); + + // no more calls after the first out one outside chart + store.dispatch(onPointerMove({ x: 5, y: 5 }, 1)); + expect(onOutListener).toBeCalledTimes(1); + store.dispatch(onPointerMove({ x: 3, y: 3 }, 2)); + expect(onOutListener).toBeCalledTimes(1); + }); + + test('can respond to tooltip types changes', () => { + let updatedSettings: SettingsSpec = { + ...settingSpec, + tooltip: { + type: TooltipType.None, + }, + }; + MockStore.addSpecs([ordinalBarSeries, updatedSettings], store); + store.dispatch(onPointerMove({ x: 10, y: 10 + 70 }, 0)); + const tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); + // no tooltip values exist if we have a TooltipType === None + expect(tooltipInfo.tooltip.values.length).toBe(0); + let isTooltipVisible = isTooltipVisibleSelector(store.getState()); + expect(isTooltipVisible.visible).toBe(false); + + updatedSettings = { + ...settingSpec, + tooltip: { + type: TooltipType.Follow, + }, + }; + MockStore.addSpecs([ordinalBarSeries, updatedSettings], store); + store.dispatch(onPointerMove({ x: 10, y: 10 + 70 }, 1)); + const { geometriesIndex } = computeSeriesGeometriesSelector(store.getState()); + expect(geometriesIndex.size).toBe(2); + const highlightedGeometries = getHighlightedGeomsSelector(store.getState()); + expect(highlightedGeometries.length).toBe(1); + isTooltipVisible = isTooltipVisibleSelector(store.getState()); + expect(isTooltipVisible.visible).toBe(true); + }); + + describe('mouse over with Ordinal scale', () => { + mouseOverTestSuite(ScaleType.Ordinal); + }); + describe('mouse over with Linear scale', () => { + mouseOverTestSuite(ScaleType.Linear); + }); + + it.todo('add test for point series'); + it.todo('add test for mixed series'); + it.todo('add test for clicks'); +}); + +function mouseOverTestSuite(scaleType: XScaleType) { + let store: Store; + let onOverListener: jest.Mock; + let onOutListener: jest.Mock; + let onPointerUpdateListener: jest.Mock; + const spec = scaleType === ScaleType.Ordinal ? ordinalBarSeries : linearBarSeries; + beforeEach(() => { + store = initStore(spec); + onOverListener = jest.fn((): undefined => undefined); + onOutListener = jest.fn((): undefined => undefined); + onPointerUpdateListener = jest.fn((): undefined => undefined); + const settingsWithListeners: SettingsSpec = { + ...settingSpec, + onElementOver: onOverListener, + onElementOut: onOutListener, + onPointerUpdate: onPointerUpdateListener, + }; + MockStore.addSpecs([spec, settingsWithListeners], store); + const onElementOutCaller = createOnElementOutCaller(); + const onElementOverCaller = createOnElementOverCaller(); + const onPointerMoveCaller = createOnPointerMoveCaller(); + store.subscribe(() => { + const state = store.getState(); + onElementOutCaller(state); + onElementOverCaller(state); + onPointerMoveCaller(state); + }); + const tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(tooltipInfo.tooltip.values).toEqual([]); + }); + + test('store is correctly configured', () => { + // checking this to avoid broken tests due to nested describe and before + const seriesGeoms = computeSeriesGeometriesSelector(store.getState()); + expect(seriesGeoms.scales.xScale).not.toBeUndefined(); + expect(seriesGeoms.scales.yScales).not.toBeUndefined(); + }); + + test('avoid call pointer update listener if moving over the same element', () => { + store.dispatch(onPointerMove({ x: chartLeft + 10, y: chartTop + 10 }, 0)); + expect(onPointerUpdateListener).toBeCalledTimes(1); + + const tooltipInfo1 = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(tooltipInfo1.tooltip.values.length).toBe(1); + // avoid calls + store.dispatch(onPointerMove({ x: chartLeft + 12, y: chartTop + 12 }, 1)); + expect(onPointerUpdateListener).toBeCalledTimes(1); + + const tooltipInfo2 = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(tooltipInfo2.tooltip.values.length).toBe(1); + expect(tooltipInfo1).toEqual(tooltipInfo2); + }); + + test('call pointer update listener on move', () => { + store.dispatch(onPointerMove({ x: chartLeft + 10, y: chartTop + 10 }, 0)); + expect(onPointerUpdateListener).toBeCalledTimes(1); + expect(onPointerUpdateListener.mock.calls[0][0]).toEqual({ + chartId: 'chartId', + scale: scaleType, + type: 'Over', + unit: undefined, + value: 0, + }); + + // avoid multiple calls for the same value + store.dispatch(onPointerMove({ x: chartLeft + 50, y: chartTop + 10 }, 1)); + expect(onPointerUpdateListener).toBeCalledTimes(2); + expect(onPointerUpdateListener.mock.calls[1][0]).toEqual({ + chartId: 'chartId', + scale: scaleType, + type: 'Over', + unit: undefined, + value: 1, + }); + + store.dispatch(onPointerMove({ x: chartLeft + 200, y: chartTop + 10 }, 1)); + expect(onPointerUpdateListener).toBeCalledTimes(3); + expect(onPointerUpdateListener.mock.calls[2][0]).toEqual({ + chartId: 'chartId', + type: 'Out', + }); + }); + + test('handle only external pointer update', () => { + store.dispatch( + onExternalPointerEvent({ + chartId: 'chartId', + scale: scaleType, + type: 'Over', + unit: undefined, + value: 0, + }), + ); + let cursorBandPosition = getCursorBandPositionSelector(store.getState()); + expect(cursorBandPosition).toBeUndefined(); + + store.dispatch( + onExternalPointerEvent({ + chartId: 'differentChart', + scale: scaleType, + type: 'Over', + unit: undefined, + value: 0, + }), + ); + cursorBandPosition = getCursorBandPositionSelector(store.getState()); + expect(cursorBandPosition).toBeDefined(); + }); + + test.skip('can determine which tooltip to display if chart & annotation tooltips possible', () => { + // const annotationDimensions = [{ rect: { x: 49, y: -1, width: 3, height: 99 } }]; + // const rectAnnotationSpec: RectAnnotationSpec = { + // id: 'rect', + // groupId: GROUP_ID, + // annotationType: 'rectangle', + // dataValues: [{ coordinates: { x0: 1, x1: 1.5, y0: 0.5, y1: 10 } }], + // }; + // store.annotationSpecs.set(rectAnnotationSpec.annotationId, rectAnnotationSpec); + // store.annotationDimensions.set(rectAnnotationSpec.annotationId, annotationDimensions); + // debugger; + // // isHighlighted false, chart tooltip true; should show annotationTooltip only + // store.setCursorPosition(chartLeft + 51, chartTop + 1); + // expect(store.isTooltipVisible.get()).toBe(false); + }); + + test('can hover top-left corner of the first bar', () => { + let tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(tooltipInfo.tooltip.values).toEqual([]); + store.dispatch(onPointerMove({ x: chartLeft + 0, y: chartTop + 0 }, 0)); + let projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); + expect(projectedPointerPosition).toMatchObject({ x: 0, y: 0 }); + const cursorBandPosition = getCursorBandPositionSelector(store.getState()); + expect(cursorBandPosition).toBeDefined(); + expect((cursorBandPosition as Rect).x).toBe(chartLeft + 0); + expect((cursorBandPosition as Rect).width).toBe(45); + let isTooltipVisible = isTooltipVisibleSelector(store.getState()); + expect(isTooltipVisible.visible).toBe(true); + tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(tooltipInfo.tooltip.values.length).toBe(1); + expect(tooltipInfo.highlightedGeometries.length).toBe(1); + expect(onOverListener).toBeCalledTimes(1); + expect(onOutListener).toBeCalledTimes(0); + expect(onOverListener.mock.calls[0][0]).toEqual([ + [ + { + x: 0, + y: 10, + accessor: 'y1', + mark: null, + datum: [0, 10], + }, + { + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', + seriesKeys: [1], + specId: 'spec_1', + splitAccessors: new Map(), + yAccessor: 1, + }, + ], + ]); + + store.dispatch(onPointerMove({ x: chartLeft - 1, y: chartTop - 1 }, 1)); + projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); + expect(projectedPointerPosition).toMatchObject({ x: -1, y: -1 }); + isTooltipVisible = isTooltipVisibleSelector(store.getState()); + expect(isTooltipVisible.visible).toBe(false); + tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(tooltipInfo.tooltip.values.length).toBe(0); + expect(tooltipInfo.highlightedGeometries.length).toBe(0); + expect(onOverListener).toBeCalledTimes(1); + expect(onOutListener).toBeCalledTimes(1); + }); + + test('can hover bottom-left corner of the first bar', () => { + store.dispatch(onPointerMove({ x: chartLeft + 0, y: chartTop + 89 }, 0)); + let projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); + expect(projectedPointerPosition).toMatchObject({ x: 0, y: 89 }); + const cursorBandPosition = getCursorBandPositionSelector(store.getState()); + expect(cursorBandPosition).toBeDefined(); + expect((cursorBandPosition as Rect).x).toBe(chartLeft + 0); + expect((cursorBandPosition as Rect).width).toBe(45); + let isTooltipVisible = isTooltipVisibleSelector(store.getState()); + expect(isTooltipVisible.visible).toBe(true); + let tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(tooltipInfo.highlightedGeometries.length).toBe(1); + expect(tooltipInfo.tooltip.values.length).toBe(1); + expect(onOverListener).toBeCalledTimes(1); + expect(onOutListener).toBeCalledTimes(0); + expect(onOverListener.mock.calls[0][0]).toEqual([ + [ + { + x: 0, + y: 10, + accessor: 'y1', + mark: null, + datum: [0, 10], + }, + { + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', + seriesKeys: [1], + specId: 'spec_1', + splitAccessors: new Map(), + yAccessor: 1, + }, + ], + ]); + store.dispatch(onPointerMove({ x: chartLeft - 1, y: chartTop + 89 }, 1)); + projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); + expect(projectedPointerPosition).toMatchObject({ x: -1, y: 89 }); + isTooltipVisible = isTooltipVisibleSelector(store.getState()); + expect(isTooltipVisible.visible).toBe(false); + tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(tooltipInfo.tooltip.values.length).toBe(0); + expect(tooltipInfo.highlightedGeometries.length).toBe(0); + expect(onOverListener).toBeCalledTimes(1); + expect(onOutListener).toBeCalledTimes(1); + }); + + test('can hover top-right corner of the first bar', () => { + let scaleOffset = 0; + if (scaleType !== ScaleType.Ordinal) { + scaleOffset = 1; + } + store.dispatch(onPointerMove({ x: chartLeft + 44 + scaleOffset, y: chartTop + 0 }, 0)); + let projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); + expect(projectedPointerPosition).toMatchObject({ x: 44 + scaleOffset, y: 0 }); + let cursorBandPosition = getCursorBandPositionSelector(store.getState()); + expect(cursorBandPosition).toBeDefined(); + expect((cursorBandPosition as Rect).x).toBe(chartLeft + 0); + expect((cursorBandPosition as Rect).width).toBe(45); + let isTooltipVisible = isTooltipVisibleSelector(store.getState()); + expect(isTooltipVisible.visible).toBe(true); + let tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(tooltipInfo.highlightedGeometries.length).toBe(1); + expect(tooltipInfo.tooltip.values.length).toBe(1); + expect(onOverListener).toBeCalledTimes(1); + expect(onOutListener).toBeCalledTimes(0); + expect(onOverListener.mock.calls[0][0]).toEqual([ + [ + { + x: 0, + y: 10, + accessor: 'y1', + mark: null, + datum: [0, 10], + }, + { + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', + seriesKeys: [1], + specId: 'spec_1', + splitAccessors: new Map(), + yAccessor: 1, + }, + ], + ]); + + store.dispatch(onPointerMove({ x: chartLeft + 45 + scaleOffset, y: chartTop + 0 }, 1)); + projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); + expect(projectedPointerPosition).toMatchObject({ x: 45 + scaleOffset, y: 0 }); + cursorBandPosition = getCursorBandPositionSelector(store.getState()); + expect(cursorBandPosition).toBeDefined(); + expect((cursorBandPosition as Rect).x).toBe(chartLeft + 45); + expect((cursorBandPosition as Rect).width).toBe(45); + isTooltipVisible = isTooltipVisibleSelector(store.getState()); + expect(isTooltipVisible.visible).toBe(true); + tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(tooltipInfo.tooltip.values.length).toBe(1); + expect(tooltipInfo.highlightedGeometries.length).toBe(0); + expect(onOverListener).toBeCalledTimes(1); + expect(onOutListener).toBeCalledTimes(1); + }); + + test('can hover bottom-right corner of the first bar', () => { + let scaleOffset = 0; + if (scaleType !== ScaleType.Ordinal) { + scaleOffset = 1; + } + store.dispatch(onPointerMove({ x: chartLeft + 44 + scaleOffset, y: chartTop + 89 }, 0)); + let projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); + expect(projectedPointerPosition).toMatchObject({ x: 44 + scaleOffset, y: 89 }); + let cursorBandPosition = getCursorBandPositionSelector(store.getState()); + expect(cursorBandPosition).toBeDefined(); + expect((cursorBandPosition as Rect).x).toBe(chartLeft + 0); + expect((cursorBandPosition as Rect).width).toBe(45); + let isTooltipVisible = isTooltipVisibleSelector(store.getState()); + expect(isTooltipVisible.visible).toBe(true); + let tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(tooltipInfo.highlightedGeometries.length).toBe(1); + expect(tooltipInfo.tooltip.values.length).toBe(1); + expect(onOverListener).toBeCalledTimes(1); + expect(onOutListener).toBeCalledTimes(0); + expect(onOverListener.mock.calls[0][0]).toEqual([ + [ + { + x: (spec.data[0] as Array)[0], + y: (spec.data[0] as Array)[1], + accessor: 'y1', + mark: null, + datum: [(spec.data[0] as Array)[0], (spec.data[0] as Array)[1]], + }, + { + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', + seriesKeys: [1], + specId: 'spec_1', + splitAccessors: new Map(), + yAccessor: 1, + }, + ], + ]); + + store.dispatch(onPointerMove({ x: chartLeft + 45 + scaleOffset, y: chartTop + 89 }, 1)); + projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); + expect(projectedPointerPosition).toMatchObject({ x: 45 + scaleOffset, y: 89 }); + cursorBandPosition = getCursorBandPositionSelector(store.getState()); + expect(cursorBandPosition).toBeDefined(); + expect((cursorBandPosition as Rect).x).toBe(chartLeft + 45); + expect((cursorBandPosition as Rect).width).toBe(45); + isTooltipVisible = isTooltipVisibleSelector(store.getState()); + expect(isTooltipVisible.visible).toBe(true); + tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(tooltipInfo.tooltip.values.length).toBe(1); + // we are over the second bar here + expect(tooltipInfo.highlightedGeometries.length).toBe(1); + expect(onOverListener).toBeCalledTimes(2); + expect(onOverListener.mock.calls[1][0]).toEqual([ + [ + { + x: (spec.data[1] as Array)[0], + y: (spec.data[1] as Array)[1], + accessor: 'y1', + mark: null, + datum: [(spec.data[1] as Array)[0], (spec.data[1] as Array)[1]], + }, + { + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', + seriesKeys: [1], + specId: 'spec_1', + splitAccessors: new Map(), + yAccessor: 1, + }, + ], + ]); + + expect(onOutListener).toBeCalledTimes(0); + + store.dispatch(onPointerMove({ x: chartLeft + 47 + scaleOffset, y: chartTop + 89 }, 2)); + }); + + test('can hover top-right corner of the chart', () => { + expect(onOverListener).toBeCalledTimes(0); + expect(onOutListener).toBeCalledTimes(0); + let tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(tooltipInfo.highlightedGeometries.length).toBe(0); + expect(tooltipInfo.tooltip.values.length).toBe(0); + + store.dispatch(onPointerMove({ x: chartLeft + 89, y: chartTop + 0 }, 0)); + const projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); + expect(projectedPointerPosition).toMatchObject({ x: 89, y: 0 }); + const cursorBandPosition = getCursorBandPositionSelector(store.getState()); + expect(cursorBandPosition).toBeDefined(); + expect((cursorBandPosition as Rect).x).toBe(chartLeft + 45); + expect((cursorBandPosition as Rect).width).toBe(45); + + const isTooltipVisible = isTooltipVisibleSelector(store.getState()); + expect(isTooltipVisible.visible).toBe(true); + tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(tooltipInfo.highlightedGeometries.length).toBe(0); + expect(tooltipInfo.tooltip.values.length).toBe(1); + expect(onOverListener).toBeCalledTimes(0); + expect(onOutListener).toBeCalledTimes(0); + }); + + test('will call only one time the listener with the same values', () => { + expect(onOverListener).toBeCalledTimes(0); + expect(onOutListener).toBeCalledTimes(0); + let halfWidth = 45; + if (scaleType !== ScaleType.Ordinal) { + halfWidth = 46; + } + let timeCounter = 0; + for (let i = 0; i < halfWidth; i++) { + store.dispatch(onPointerMove({ x: chartLeft + i, y: chartTop + 89 }, timeCounter)); + expect(onOverListener).toBeCalledTimes(1); + expect(onOutListener).toBeCalledTimes(0); + timeCounter++; + } + for (let i = halfWidth; i < 90; i++) { + store.dispatch(onPointerMove({ x: chartLeft + i, y: chartTop + 89 }, timeCounter)); + expect(onOverListener).toBeCalledTimes(2); + expect(onOutListener).toBeCalledTimes(0); + timeCounter++; + } + for (let i = 0; i < halfWidth; i++) { + store.dispatch(onPointerMove({ x: chartLeft + i, y: chartTop + 0 }, timeCounter)); + expect(onOverListener).toBeCalledTimes(3); + expect(onOutListener).toBeCalledTimes(0); + timeCounter++; + } + for (let i = halfWidth; i < 90; i++) { + store.dispatch(onPointerMove({ x: chartLeft + i, y: chartTop + 0 }, timeCounter)); + expect(onOverListener).toBeCalledTimes(3); + expect(onOutListener).toBeCalledTimes(1); + timeCounter++; + } + }); + + test('can hover bottom-right corner of the chart', () => { + store.dispatch(onPointerMove({ x: chartLeft + 89, y: chartTop + 89 }, 0)); + const projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); + // store.setCursorPosition(chartLeft + 99, chartTop + 99); + expect(projectedPointerPosition).toMatchObject({ x: 89, y: 89 }); + const cursorBandPosition = getCursorBandPositionSelector(store.getState()); + expect(cursorBandPosition).toBeDefined(); + expect((cursorBandPosition as Rect).x).toBe(chartLeft + 45); + expect((cursorBandPosition as Rect).width).toBe(45); + const isTooltipVisible = isTooltipVisibleSelector(store.getState()); + expect(isTooltipVisible.visible).toBe(true); + const tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(tooltipInfo.highlightedGeometries.length).toBe(1); + expect(tooltipInfo.tooltip.values.length).toBe(1); + expect(onOverListener).toBeCalledTimes(1); + expect(onOverListener.mock.calls[0][0]).toEqual([ + [ + { + x: 1, + y: 5, + accessor: 'y1', + mark: null, + datum: [1, 5], + }, + { + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', + seriesKeys: [1], + specId: 'spec_1', + splitAccessors: new Map(), + yAccessor: 1, + }, + ], + ]); + expect(onOutListener).toBeCalledTimes(0); + }); + + describe.skip('can position tooltip within chart when xScale is a single value scale', () => { + beforeEach(() => { + // const singleValueScale = + // store.xScale!.type === ScaleType.Ordinal + // ? new ScaleBand(['a'], [0, 0]) + // : new ScaleContinuous({ type: ScaleType.Linear, domain: [1, 1], range: [0, 0] }); + // store.xScale = singleValueScale; + }); + test.skip('horizontal chart rotation', () => { + // store.setCursorPosition(chartLeft + 99, chartTop + 99); + // const expectedTransform = `translateX(${chartLeft}px) translateX(-0%) translateY(109px) translateY(-100%)`; + // expect(store.tooltipPosition.transform).toBe(expectedTransform); + }); + + test.skip('vertical chart rotation', () => { + // store.chartRotation = 90; + // store.setCursorPosition(chartLeft + 99, chartTop + 99); + // const expectedTransform = `translateX(109px) translateX(-100%) translateY(${chartTop}px) translateY(-0%)`; + // expect(store.tooltipPosition.transform).toBe(expectedTransform); + }); + }); + describe('can format tooltip values on rotated chart', () => { + let leftAxis: AxisSpec; + let bottomAxis: AxisSpec; + let currentSettingSpec: SettingsSpec; + const style: RecursivePartial = { + tickLine: { + size: 0, + padding: 0, + }, + }; + beforeEach(() => { + leftAxis = { + chartType: ChartType.XYAxis, + specType: SpecType.Axis, + hide: true, + id: 'yaxis', + groupId: GROUP_ID, + position: Position.Left, + tickFormat: (value) => `left ${Number(value)}`, + showOverlappingLabels: false, + showOverlappingTicks: false, + style, + }; + bottomAxis = { + chartType: ChartType.XYAxis, + specType: SpecType.Axis, + hide: true, + id: 'xaxis', + groupId: GROUP_ID, + position: Position.Bottom, + tickFormat: (value) => `bottom ${Number(value)}`, + showOverlappingLabels: false, + showOverlappingTicks: false, + style, + }; + currentSettingSpec = getSettingsSpecSelector(store.getState()); + }); + + test('chart 0 rotation', () => { + MockStore.addSpecs([spec, leftAxis, bottomAxis, currentSettingSpec], store); + store.dispatch(onPointerMove({ x: chartLeft + 0, y: chartTop + 89 }, 0)); + const tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(tooltipInfo.tooltip.header?.value).toBe(0); + expect(tooltipInfo.tooltip.header?.formattedValue).toBe('bottom 0'); + expect(tooltipInfo.tooltip.values[0].value).toBe(10); + expect(tooltipInfo.tooltip.values[0].formattedValue).toBe('left 10'); + }); + + test('chart 90 deg rotated', () => { + const updatedSettings: SettingsSpec = { + ...currentSettingSpec, + rotation: 90, + }; + MockStore.addSpecs([spec, leftAxis, bottomAxis, updatedSettings], store); + + store.dispatch(onPointerMove({ x: chartLeft + 0, y: chartTop + 89 }, 0)); + const tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(tooltipInfo.tooltip.header?.value).toBe(1); + expect(tooltipInfo.tooltip.header?.formattedValue).toBe('left 1'); + expect(tooltipInfo.tooltip.values[0].value).toBe(5); + expect(tooltipInfo.tooltip.values[0].formattedValue).toBe('bottom 5'); + }); + }); + describe('brush', () => { + test('can respond to a brush end event', () => { + const brushEndListener = jest.fn((): void => undefined); + const onBrushCaller = createOnBrushEndCaller(); + store.subscribe(() => { + onBrushCaller(store.getState()); + }); + const settings = getSettingsSpecSelector(store.getState()); + const updatedSettings: SettingsSpec = { + ...settings, + theme: { + ...settings.theme, + chartMargins: { + left: 0, + right: 0, + top: 0, + bottom: 0, + }, + }, + onBrushEnd: brushEndListener, + }; + MockStore.addSpecs( + [ + { + ...spec, + data: [ + [0, 1], + [1, 1], + [2, 2], + [3, 3], + ], + } as BarSeriesSpec, + updatedSettings, + ], + store, + ); + + const start1 = { x: 0, y: 0 }; + const end1 = { x: 75, y: 0 }; + + store.dispatch(onMouseDown(start1, 0)); + store.dispatch(onPointerMove(end1, 200)); + store.dispatch(onMouseUp(end1, 300)); + if (scaleType === ScaleType.Ordinal) { + expect(brushEndListener).not.toBeCalled(); + } else { + expect(brushEndListener).toBeCalled(); + expect(brushEndListener.mock.calls[0][0]).toEqual({ x: [0, 2.5] }); + } + const start2 = { x: 75, y: 0 }; + const end2 = { x: 100, y: 0 }; + + store.dispatch(onMouseDown(start2, 400)); + store.dispatch(onPointerMove(end2, 500)); + store.dispatch(onMouseUp(end2, 600)); + if (scaleType === ScaleType.Ordinal) { + expect(brushEndListener).not.toBeCalled(); + } else { + expect(brushEndListener).toBeCalled(); + expect(brushEndListener.mock.calls[1][0]).toEqual({ x: [2.5, 3] }); + } + + const start3 = { x: 75, y: 0 }; + const end3 = { x: 250, y: 0 }; + store.dispatch(onMouseDown(start3, 700)); + store.dispatch(onPointerMove(end3, 800)); + store.dispatch(onMouseUp(end3, 900)); + if (scaleType === ScaleType.Ordinal) { + expect(brushEndListener).not.toBeCalled(); + } else { + expect(brushEndListener).toBeCalled(); + expect(brushEndListener.mock.calls[2][0]).toEqual({ x: [2.5, 3] }); + } + + const start4 = { x: 25, y: 0 }; + const end4 = { x: -20, y: 0 }; + store.dispatch(onMouseDown(start4, 1000)); + store.dispatch(onPointerMove(end4, 1100)); + store.dispatch(onMouseUp(end4, 1200)); + if (scaleType === ScaleType.Ordinal) { + expect(brushEndListener).not.toBeCalled(); + } else { + expect(brushEndListener).toBeCalled(); + expect(brushEndListener.mock.calls[3][0]).toEqual({ x: [0, 0.5] }); + } + + store.dispatch(onMouseDown({ x: 25, y: 0 }, 1300)); + store.dispatch(onPointerMove({ x: 28, y: 0 }, 1390)); + store.dispatch(onMouseUp({ x: 28, y: 0 }, 1400)); + if (scaleType === ScaleType.Ordinal) { + expect(brushEndListener).not.toBeCalled(); + } else { + expect(brushEndListener.mock.calls[4]).toBeUndefined(); + } + }); + test('can respond to a brush end event on rotated chart', () => { + const brushEndListener = jest.fn((): void => undefined); + const onBrushCaller = createOnBrushEndCaller(); + store.subscribe(() => { + onBrushCaller(store.getState()); + }); + const settings = getSettingsSpecSelector(store.getState()); + const updatedSettings: SettingsSpec = { + ...settings, + rotation: 90, + theme: { + ...settings.theme, + chartMargins: { + left: 0, + right: 0, + top: 0, + bottom: 0, + }, + }, + onBrushEnd: brushEndListener, + }; + MockStore.addSpecs([spec, updatedSettings], store); + + const start1 = { x: 0, y: 25 }; + const end1 = { x: 0, y: 75 }; + + store.dispatch(onMouseDown(start1, 0)); + store.dispatch(onPointerMove(end1, 100)); + store.dispatch(onMouseUp(end1, 200)); + if (scaleType === ScaleType.Ordinal) { + expect(brushEndListener).not.toBeCalled(); + } else { + expect(brushEndListener).toBeCalled(); + expect(brushEndListener.mock.calls[0][0]).toEqual({ x: [0, 1] }); + } + const start2 = { x: 0, y: 75 }; + const end2 = { x: 0, y: 100 }; + + store.dispatch(onMouseDown(start2, 400)); + store.dispatch(onPointerMove(end2, 500)); + store.dispatch(onMouseUp(end2, 600)); + if (scaleType === ScaleType.Ordinal) { + expect(brushEndListener).not.toBeCalled(); + } else { + expect(brushEndListener).toBeCalled(); + expect(brushEndListener.mock.calls[1][0]).toEqual({ x: [1, 1] }); + } + + const start3 = { x: 0, y: 75 }; + const end3 = { x: 0, y: 200 }; + store.dispatch(onMouseDown(start3, 700)); + store.dispatch(onPointerMove(end3, 800)); + store.dispatch(onMouseUp(end3, 900)); + if (scaleType === ScaleType.Ordinal) { + expect(brushEndListener).not.toBeCalled(); + } else { + expect(brushEndListener).toBeCalled(); + expect(brushEndListener.mock.calls[2][0]).toEqual({ x: [1, 1] }); // max of chart + } + + const start4 = { x: 0, y: 25 }; + const end4 = { x: 0, y: -20 }; + store.dispatch(onMouseDown(start4, 1000)); + store.dispatch(onPointerMove(end4, 1100)); + store.dispatch(onMouseUp(end4, 1200)); + if (scaleType === ScaleType.Ordinal) { + expect(brushEndListener).not.toBeCalled(); + } else { + expect(brushEndListener).toBeCalled(); + expect(brushEndListener.mock.calls[3][0]).toEqual({ x: [0, 0] }); + } + }); + test('can respond to a Y brush', () => { + const brushEndListener = jest.fn((): void => undefined); + const onBrushCaller = createOnBrushEndCaller(); + store.subscribe(() => { + onBrushCaller(store.getState()); + }); + const settings = getSettingsSpecSelector(store.getState()); + const updatedSettings: SettingsSpec = { + ...settings, + brushAxis: BrushAxis.Y, + theme: { + ...settings.theme, + chartMargins: { + left: 0, + right: 0, + top: 0, + bottom: 0, + }, + }, + onBrushEnd: brushEndListener, + }; + MockStore.addSpecs( + [ + { + ...spec, + data: [ + [0, 1], + [1, 1], + [2, 2], + [3, 3], + ], + } as BarSeriesSpec, + updatedSettings, + ], + store, + ); + + const start1 = { x: 0, y: 0 }; + const end1 = { x: 0, y: 75 }; + + store.dispatch(onMouseDown(start1, 0)); + store.dispatch(onPointerMove(end1, 100)); + store.dispatch(onMouseUp(end1, 200)); + if (scaleType === ScaleType.Ordinal) { + expect(brushEndListener).not.toBeCalled(); + } else { + expect(brushEndListener).toBeCalled(); + expect(brushEndListener.mock.calls[0][0]).toEqual({ + y: [ + { + groupId: spec.groupId, + extent: [0.75, 3], + }, + ], + }); + } + const start2 = { x: 0, y: 75 }; + const end2 = { x: 0, y: 100 }; + + store.dispatch(onMouseDown(start2, 400)); + store.dispatch(onPointerMove(end2, 500)); + store.dispatch(onMouseUp(end2, 600)); + if (scaleType === ScaleType.Ordinal) { + expect(brushEndListener).not.toBeCalled(); + } else { + expect(brushEndListener).toBeCalled(); + expect(brushEndListener.mock.calls[1][0]).toEqual({ + y: [ + { + groupId: spec.groupId, + extent: [0, 0.75], + }, + ], + }); + } + }); + test('can respond to rectangular brush', () => { + const brushEndListener = jest.fn((): void => undefined); + const onBrushCaller = createOnBrushEndCaller(); + store.subscribe(() => { + onBrushCaller(store.getState()); + }); + const settings = getSettingsSpecSelector(store.getState()); + const updatedSettings: SettingsSpec = { + ...settings, + brushAxis: BrushAxis.Both, + theme: { + ...settings.theme, + chartMargins: { + left: 0, + right: 0, + top: 0, + bottom: 0, + }, + }, + onBrushEnd: brushEndListener, + }; + MockStore.addSpecs( + [ + { + ...spec, + data: [ + [0, 1], + [1, 1], + [2, 2], + [3, 3], + ], + } as BarSeriesSpec, + updatedSettings, + ], + store, + ); + + const start1 = { x: 0, y: 0 }; + const end1 = { x: 75, y: 75 }; + + store.dispatch(onMouseDown(start1, 0)); + store.dispatch(onPointerMove(end1, 100)); + store.dispatch(onMouseUp(end1, 300)); + if (scaleType === ScaleType.Ordinal) { + expect(brushEndListener).not.toBeCalled(); + } else { + expect(brushEndListener).toBeCalled(); + expect(brushEndListener.mock.calls[0][0]).toEqual({ + x: [0, 2.5], + y: [ + { + groupId: spec.groupId, + extent: [0.75, 3], + }, + ], + }); + } + const start2 = { x: 75, y: 75 }; + const end2 = { x: 100, y: 100 }; + + store.dispatch(onMouseDown(start2, 400)); + store.dispatch(onPointerMove(end2, 500)); + store.dispatch(onMouseUp(end2, 600)); + if (scaleType === ScaleType.Ordinal) { + expect(brushEndListener).not.toBeCalled(); + } else { + expect(brushEndListener).toBeCalled(); + expect(brushEndListener.mock.calls[1][0]).toEqual({ + x: [2.5, 3], + y: [ + { + groupId: spec.groupId, + extent: [0, 0.75], + }, + ], + }); + } + }); + }); +} + +describe('Negative bars click and hover', () => { + let store: Store; + let onElementClick: jest.Mock; + beforeEach(() => { + store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }, 'chartId'); + onElementClick = jest.fn((): void => undefined); + const onElementClickCaller = createOnClickCaller(); + store.subscribe(() => { + onElementClickCaller(store.getState()); + }); + MockStore.addSpecs( + [ + MockGlobalSpec.settingsNoMargins({ + onElementClick, + }), + MockSeriesSpec.bar({ + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 10], + [1, -10], + [2, 10], + ], + }), + ], + store, + ); + }); + + test('highlight negative bars', () => { + store.dispatch(onPointerMove({ x: 50, y: 75 }, 0)); + const highlightedGeoms = getHighlightedGeomsSelector(store.getState()); + expect(highlightedGeoms.length).toBe(1); + expect(highlightedGeoms[0].value.datum).toEqual([1, -10]); + }); + test('click negative bars', () => { + store.dispatch(onPointerMove({ x: 50, y: 75 }, 0)); + store.dispatch(onMouseDown({ x: 50, y: 75 }, 100)); + store.dispatch(onMouseUp({ x: 50, y: 75 }, 200)); + + expect(onElementClick).toBeCalled(); + const callArgs = onElementClick.mock.calls[0][0]; + expect(callArgs[0][0].datum).toEqual([1, -10]); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.specs.test.ts b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.specs.test.ts new file mode 100644 index 000000000000..af73c61911d8 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.specs.test.ts @@ -0,0 +1,157 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Store } from 'redux'; + +import { MockSeriesSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store'; +import { removeSpec, specParsed, upsertSpec } from '../../../state/actions/specs'; +import { GlobalChartState } from '../../../state/chart_state'; +import { getInternalIsInitializedSelector, InitStatus } from '../../../state/selectors/get_internal_is_intialized'; +import { getLegendItemsSelector } from '../../../state/selectors/get_legend_items'; + +const data = [ + { x: 0, y: 10 }, + { x: 1, y: 10 }, +]; + +describe('XYChart - specs ordering', () => { + let store: Store; + beforeEach(() => { + store = MockStore.default({ width: 100, height: 100, left: 0, top: 0 }); + }); + + it('the legend respect the insert [A, B, C] order', () => { + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ id: 'A', data }), + MockSeriesSpec.bar({ id: 'B', data }), + MockSeriesSpec.bar({ id: 'C', data }), + ], + store, + ); + + const legendItems = getLegendItemsSelector(store.getState()); + const names = [...legendItems.values()].map((item) => item.label); + expect(names).toEqual(['A', 'B', 'C']); + }); + it('the legend respect the insert order [B, A, C]', () => { + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ id: 'B', data }), + MockSeriesSpec.bar({ id: 'A', data }), + MockSeriesSpec.bar({ id: 'C', data }), + ], + store, + ); + const legendItems = getLegendItemsSelector(store.getState()); + const names = [...legendItems.values()].map((item) => item.label); + expect(names).toEqual(['B', 'A', 'C']); + }); + it('the legend respect the order when changing properties of existing specs', () => { + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ id: 'A', data }), + MockSeriesSpec.bar({ id: 'B', data }), + MockSeriesSpec.bar({ id: 'C', data }), + ], + store, + ); + + let legendItems = getLegendItemsSelector(store.getState()); + let names = [...legendItems.values()].map((item) => item.label); + expect(names).toEqual(['A', 'B', 'C']); + + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ id: 'A', data }), + MockSeriesSpec.bar({ id: 'B', name: 'B updated', data }), + MockSeriesSpec.bar({ id: 'C', data }), + ], + store, + ); + + legendItems = getLegendItemsSelector(store.getState()); + names = [...legendItems.values()].map((item) => item.label); + expect(names).toEqual(['A', 'B updated', 'C']); + }); + it('the legend respect the order when changing the order of the specs', () => { + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ id: 'A', data }), + MockSeriesSpec.bar({ id: 'B', data }), + MockSeriesSpec.bar({ id: 'C', data }), + ], + store, + ); + let legendItems = getLegendItemsSelector(store.getState()); + let names = [...legendItems.values()].map((item) => item.label); + expect(names).toEqual(['A', 'B', 'C']); + + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ id: 'B', data }), + MockSeriesSpec.bar({ id: 'A', data }), + MockSeriesSpec.bar({ id: 'C', data }), + ], + store, + ); + + legendItems = getLegendItemsSelector(store.getState()); + names = [...legendItems.values()].map((item) => item.label); + expect(names).toEqual(['B', 'A', 'C']); + }); + it('The status should switch to not initialized removing a spec', () => { + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ id: 'A', data }), + MockSeriesSpec.bar({ id: 'B', data }), + MockSeriesSpec.bar({ id: 'C', data }), + ], + store, + ); + expect(getInternalIsInitializedSelector(store.getState())).toBe(InitStatus.Initialized); + // check on remove + store.dispatch(removeSpec('A')); + expect(getInternalIsInitializedSelector(store.getState())).not.toBe(InitStatus.Initialized); + + // initialized again after specParsed action + store.dispatch(specParsed()); + expect(getInternalIsInitializedSelector(store.getState())).toBe(InitStatus.Initialized); + }); + it('The status should switch to not initialized when upserting a spec', () => { + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ id: 'A', data }), + MockSeriesSpec.bar({ id: 'B', data }), + MockSeriesSpec.bar({ id: 'C', data }), + ], + store, + ); + expect(getInternalIsInitializedSelector(store.getState())).toBe(InitStatus.Initialized); + + // check on upsert + store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'D', data }))); + expect(getInternalIsInitializedSelector(store.getState())).not.toBe(InitStatus.Initialized); + + // initialized again after specParsed action + store.dispatch(specParsed()); + expect(getInternalIsInitializedSelector(store.getState())).toBe(InitStatus.Initialized); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.test.ts b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.test.ts new file mode 100644 index 000000000000..54536c3edaad --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.test.ts @@ -0,0 +1,176 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartType } from '../..'; +import { SpecType } from '../../../specs/constants'; +import { Position, RecursivePartial } from '../../../utils/common'; +import { AxisId } from '../../../utils/ids'; +import { AxisStyle } from '../../../utils/themes/theme'; +import { AxisTicksDimensions, isDuplicateAxis } from '../utils/axis_utils'; +import { AxisSpec } from '../utils/specs'; + +const style: RecursivePartial = { + tickLine: { + size: 30, + padding: 10, + }, +}; +describe('isDuplicateAxis', () => { + const AXIS_1_ID = 'spec_1'; + const AXIS_2_ID = 'spec_1'; + const axis1: AxisSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Axis, + id: AXIS_1_ID, + groupId: 'group_1', + hide: false, + showOverlappingTicks: false, + showOverlappingLabels: false, + position: Position.Left, + style, + tickFormat: (value: any) => `${value}%`, + }; + const axis2: AxisSpec = { + ...axis1, + id: AXIS_2_ID, + groupId: 'group_2', + }; + const axisTicksDimensions: AxisTicksDimensions = { + tickValues: [], + tickLabels: ['10', '20', '30'], + maxLabelBboxWidth: 1, + maxLabelBboxHeight: 1, + maxLabelTextWidth: 1, + maxLabelTextHeight: 1, + isHidden: false, + }; + let tickMap: Map; + let specMap: AxisSpec[]; + + beforeEach(() => { + tickMap = new Map(); + specMap = []; + }); + + it('should return true if axisSpecs and ticks match', () => { + tickMap.set(AXIS_2_ID, axisTicksDimensions); + specMap.push(axis2); + const result = isDuplicateAxis(axis1, axisTicksDimensions, tickMap, specMap); + + expect(result).toBe(true); + }); + + it('should return false if axisSpecs, ticks AND title match', () => { + tickMap.set(AXIS_2_ID, axisTicksDimensions); + specMap.push({ + ...axis2, + title: 'TESTING', + }); + const result = isDuplicateAxis( + { + ...axis1, + title: 'TESTING', + }, + axisTicksDimensions, + tickMap, + specMap, + ); + + expect(result).toBe(true); + }); + + it('should return true with single tick', () => { + const newAxisTicksDimensions = { + ...axisTicksDimensions, + tickLabels: ['10'], + }; + tickMap.set(AXIS_2_ID, newAxisTicksDimensions); + specMap.push(axis2); + + const result = isDuplicateAxis(axis1, newAxisTicksDimensions, tickMap, specMap); + + expect(result).toBe(true); + }); + + it('should return false if axisSpecs and ticks match but title is different', () => { + tickMap.set(AXIS_2_ID, axisTicksDimensions); + specMap.push({ + ...axis2, + title: 'TESTING', + }); + const result = isDuplicateAxis( + { + ...axis1, + title: 'NOT TESTING', + }, + axisTicksDimensions, + tickMap, + specMap, + ); + + expect(result).toBe(false); + }); + + it('should return false if axisSpecs and ticks match but position is different', () => { + tickMap.set(AXIS_2_ID, axisTicksDimensions); + specMap.push(axis2); + const result = isDuplicateAxis( + { + ...axis1, + position: Position.Top, + }, + axisTicksDimensions, + tickMap, + specMap, + ); + + expect(result).toBe(false); + }); + + it('should return false if tickFormat is different', () => { + tickMap.set(AXIS_2_ID, { + ...axisTicksDimensions, + tickLabels: ['10%', '20%', '30%'], + }); + specMap.push(axis2); + + const result = isDuplicateAxis(axis1, axisTicksDimensions, tickMap, specMap); + + expect(result).toBe(false); + }); + + it('should return false if tick label count is different', () => { + tickMap.set(AXIS_2_ID, { + ...axisTicksDimensions, + tickLabels: ['10', '20', '25', '30'], + }); + specMap.push(axis2); + + const result = isDuplicateAxis(axis1, axisTicksDimensions, tickMap, specMap); + + expect(result).toBe(false); + }); + + it("should return false if can't find spec", () => { + tickMap.set(AXIS_2_ID, axisTicksDimensions); + const result = isDuplicateAxis(axis1, axisTicksDimensions, tickMap, specMap); + + expect(result).toBe(false); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.timescales.test.ts b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.timescales.test.ts new file mode 100644 index 000000000000..a3681a176134 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.timescales.test.ts @@ -0,0 +1,280 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DateTime } from 'luxon'; +import { createStore, Store } from 'redux'; + +import { ChartType } from '../..'; +import { ScaleType } from '../../../scales/constants'; +import { SettingsSpec } from '../../../specs'; +import { SpecType, DEFAULT_SETTINGS_SPEC } from '../../../specs/constants'; +import { updateParentDimensions } from '../../../state/actions/chart_settings'; +import { onPointerMove } from '../../../state/actions/mouse'; +import { upsertSpec, specParsed } from '../../../state/actions/specs'; +import { chartStoreReducer, GlobalChartState } from '../../../state/chart_state'; +import { LIGHT_THEME } from '../../../utils/themes/light_theme'; +import { mergeWithDefaultTheme } from '../../../utils/themes/merge_utils'; +import { LineSeriesSpec, SeriesType } from '../utils/specs'; +import { computeSeriesGeometriesSelector } from './selectors/compute_series_geometries'; +import { getComputedScalesSelector } from './selectors/get_computed_scales'; +import { getTooltipInfoSelector } from './selectors/get_tooltip_values_highlighted_geoms'; + +describe('Render chart', () => { + describe('line, utc-time, day interval', () => { + let store: Store; + const day1 = 1546300800000; // 2019-01-01T00:00:00.000Z + const day2 = day1 + 1000 * 60 * 60 * 24; + const day3 = day2 + 1000 * 60 * 60 * 24; + beforeEach(() => { + const storeReducer = chartStoreReducer('chartId'); + store = createStore(storeReducer); + + const lineSeries: LineSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'lines', + groupId: 'line', + seriesType: SeriesType.Line, + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [day1, 10], + [day2, 22], + [day3, 6], + ], + }; + store.dispatch(upsertSpec(lineSeries)); + + const settingSpec: SettingsSpec = { + ...DEFAULT_SETTINGS_SPEC, + theme: mergeWithDefaultTheme( + { + chartPaddings: { top: 0, left: 0, bottom: 0, right: 0 }, + chartMargins: { top: 0, left: 0, bottom: 0, right: 0 }, + }, + LIGHT_THEME, + ), + }; + store.dispatch(upsertSpec(settingSpec)); + store.dispatch(specParsed()); + store.dispatch(updateParentDimensions({ width: 100, height: 100, top: 0, left: 0 })); + const state = store.getState(); + expect(state.specs.lines).toBeDefined(); + expect(state.chartType).toBe(ChartType.XYAxis); + }); + test('check rendered geometries', () => { + const { geometries } = computeSeriesGeometriesSelector(store.getState()); + expect(geometries).toBeDefined(); + expect(geometries.lines).toBeDefined(); + expect(geometries.lines.length).toBe(1); + expect(geometries.lines[0].value.points.length).toBe(3); + }); + test('check mouse position correctly return inverted value', () => { + store.dispatch(onPointerMove({ x: 15, y: 10 }, 0)); // check first valid tooltip + let tooltip = getTooltipInfoSelector(store.getState()); + expect(tooltip.values.length).toBe(1); + expect(tooltip.header?.value).toBe(day1); + expect(tooltip.header?.formattedValue).toBe(`${day1}`); + expect(tooltip.values[0].value).toBe(10); + expect(tooltip.values[0].formattedValue).toBe(`${10}`); + store.dispatch(onPointerMove({ x: 35, y: 10 }, 1)); // check second valid tooltip + tooltip = getTooltipInfoSelector(store.getState()); + expect(tooltip.values.length).toBe(1); + expect(tooltip.header?.value).toBe(day2); + expect(tooltip.header?.formattedValue).toBe(`${day2}`); + expect(tooltip.values[0].value).toBe(22); + expect(tooltip.values[0].formattedValue).toBe(`${22}`); + store.dispatch(onPointerMove({ x: 76, y: 10 }, 2)); // check third valid tooltip + tooltip = getTooltipInfoSelector(store.getState()); + expect(tooltip.values.length).toBe(1); + expect(tooltip.header?.value).toBe(day3); + expect(tooltip.header?.formattedValue).toBe(`${day3}`); + expect(tooltip.values[0].value).toBe(6); + expect(tooltip.values[0].formattedValue).toBe(`${6}`); + }); + }); + describe('line, utc-time, 5m interval', () => { + let store: Store; + const date1 = 1546300800000; // 2019-01-01T00:00:00.000Z + const date2 = date1 + 1000 * 60 * 5; + const date3 = date2 + 1000 * 60 * 5; + beforeEach(() => { + const storeReducer = chartStoreReducer('chartId'); + store = createStore(storeReducer); + + const lineSeries: LineSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'lines', + groupId: 'line', + seriesType: SeriesType.Line, + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [date1, 10], + [date2, 22], + [date3, 6], + ], + }; + store.dispatch(upsertSpec(lineSeries)); + const settingSpec: SettingsSpec = { + ...DEFAULT_SETTINGS_SPEC, + theme: mergeWithDefaultTheme( + { + chartPaddings: { top: 0, left: 0, bottom: 0, right: 0 }, + chartMargins: { top: 0, left: 0, bottom: 0, right: 0 }, + }, + LIGHT_THEME, + ), + }; + store.dispatch(upsertSpec(settingSpec)); + store.dispatch(specParsed()); + store.dispatch(updateParentDimensions({ width: 100, height: 100, top: 0, left: 0 })); + const state = store.getState(); + expect(state.specs.lines).toBeDefined(); + expect(state.chartType).toBe(ChartType.XYAxis); + }); + test('check rendered geometries', () => { + const { geometries } = computeSeriesGeometriesSelector(store.getState()); + expect(geometries).toBeDefined(); + expect(geometries.lines).toBeDefined(); + expect(geometries.lines.length).toBe(1); + expect(geometries.lines[0].value.points.length).toBe(3); + }); + test('check mouse position correctly return inverted value', () => { + store.dispatch(onPointerMove({ x: 15, y: 10 }, 0)); // check first valid tooltip + let tooltip = getTooltipInfoSelector(store.getState()); + expect(tooltip.values.length).toBe(1); + expect(tooltip.header?.value).toBe(date1); + expect(tooltip.header?.formattedValue).toBe(`${date1}`); + expect(tooltip.values[0].value).toBe(10); + expect(tooltip.values[0].formattedValue).toBe(`${10}`); + store.dispatch(onPointerMove({ x: 35, y: 10 }, 1)); // check second valid tooltip + tooltip = getTooltipInfoSelector(store.getState()); + expect(tooltip.values.length).toBe(1); + expect(tooltip.header?.value).toBe(date2); + expect(tooltip.header?.formattedValue).toBe(`${date2}`); + expect(tooltip.values[0].value).toBe(22); + expect(tooltip.values[0].formattedValue).toBe(`${22}`); + store.dispatch(onPointerMove({ x: 76, y: 10 }, 2)); // check third valid tooltip + tooltip = getTooltipInfoSelector(store.getState()); + expect(tooltip.values.length).toBe(1); + expect(tooltip.header?.value).toBe(date3); + expect(tooltip.header?.formattedValue).toBe(`${date3}`); + expect(tooltip.values[0].value).toBe(6); + expect(tooltip.values[0].formattedValue).toBe(`${6}`); + }); + }); + describe('line, non utc-time, 5m + 1s interval', () => { + let store: Store; + const date1 = DateTime.fromISO('2019-01-01T00:00:01.000-0300', { setZone: true }).toMillis(); + const date2 = date1 + 1000 * 60 * 5; + const date3 = date2 + 1000 * 60 * 5; + beforeEach(() => { + const storeReducer = chartStoreReducer('chartId'); + store = createStore(storeReducer); + const lineSeries: LineSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'lines', + groupId: 'line', + seriesType: SeriesType.Line, + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [date1, 10], + [date2, 22], + [date3, 6], + ], + }; + store.dispatch(upsertSpec(lineSeries)); + const settingSpec: SettingsSpec = { + ...DEFAULT_SETTINGS_SPEC, + theme: mergeWithDefaultTheme( + { + chartPaddings: { top: 0, left: 0, bottom: 0, right: 0 }, + chartMargins: { top: 0, left: 0, bottom: 0, right: 0 }, + }, + LIGHT_THEME, + ), + }; + store.dispatch(upsertSpec(settingSpec)); + store.dispatch(specParsed()); + store.dispatch(updateParentDimensions({ width: 100, height: 100, top: 0, left: 0 })); + const state = store.getState(); + expect(state.specs.lines).toBeDefined(); + expect(state.chartType).toBe(ChartType.XYAxis); + }); + test('check rendered geometries', () => { + const { geometries } = computeSeriesGeometriesSelector(store.getState()); + expect(geometries).toBeDefined(); + expect(geometries.lines).toBeDefined(); + expect(geometries.lines.length).toBe(1); + expect(geometries.lines[0].value.points.length).toBe(3); + }); + test('check scale values', () => { + const xValues = [date1, date2, date3]; + const state = store.getState(); + const { xScale } = getComputedScalesSelector(state); + + expect(xScale.minInterval).toBe(1000 * 60 * 5); + expect(xScale.domain).toEqual([date1, date3]); + expect(xScale.range).toEqual([0, 100]); + expect(xScale.invert(0)).toBe(date1); + expect(xScale.invert(50)).toBe(date2); + expect(xScale.invert(100)).toBe(date3); + expect(xScale.invertWithStep(5, xValues)).toEqual({ value: date1, withinBandwidth: true }); + expect(xScale.invertWithStep(20, xValues)).toEqual({ value: date1, withinBandwidth: true }); + expect(xScale.invertWithStep(30, xValues)).toEqual({ value: date2, withinBandwidth: true }); + expect(xScale.invertWithStep(50, xValues)).toEqual({ value: date2, withinBandwidth: true }); + expect(xScale.invertWithStep(70, xValues)).toEqual({ value: date2, withinBandwidth: true }); + expect(xScale.invertWithStep(80, xValues)).toEqual({ value: date3, withinBandwidth: true }); + expect(xScale.invertWithStep(100, xValues)).toEqual({ value: date3, withinBandwidth: true }); + }); + test('check mouse position correctly return inverted value', () => { + store.dispatch(onPointerMove({ x: 15, y: 10 }, 0)); // check first valid tooltip + let tooltip = getTooltipInfoSelector(store.getState()); + expect(tooltip.values.length).toBe(1); + expect(tooltip.header?.value).toBe(date1); + expect(tooltip.header?.formattedValue).toBe(`${date1}`); + expect(tooltip.values[0].value).toBe(10); + expect(tooltip.values[0].formattedValue).toBe(`${10}`); + store.dispatch(onPointerMove({ x: 35, y: 10 }, 1)); // check second valid tooltip + tooltip = getTooltipInfoSelector(store.getState()); + expect(tooltip.values.length).toBe(1); + expect(tooltip.header?.value).toBe(date2); + expect(tooltip.header?.formattedValue).toBe(`${date2}`); + expect(tooltip.values[0].value).toBe(22); + expect(tooltip.values[0].formattedValue).toBe(`${22}`); + store.dispatch(onPointerMove({ x: 76, y: 10 }, 2)); // check third valid tooltip + tooltip = getTooltipInfoSelector(store.getState()); + expect(tooltip.values.length).toBe(1); + expect(tooltip.header?.value).toBe(date3); + expect(tooltip.header?.formattedValue).toBe(`${date3}`); + expect(tooltip.values[0].value).toBe(6); + expect(tooltip.values[0].formattedValue).toBe(`${6}`); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.tooltip.test.ts b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.tooltip.test.ts new file mode 100644 index 000000000000..1322a91f3189 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.tooltip.test.ts @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createStore, Store } from 'redux'; + +import { MockSeriesSpec, MockGlobalSpec } from '../../../mocks/specs'; +import { TooltipType } from '../../../specs/constants'; +import { updateParentDimensions } from '../../../state/actions/chart_settings'; +import { onPointerMove } from '../../../state/actions/mouse'; +import { upsertSpec, specParsed } from '../../../state/actions/specs'; +import { GlobalChartState, chartStoreReducer } from '../../../state/chart_state'; +import { getTooltipInfoAndGeometriesSelector } from './selectors/get_tooltip_values_highlighted_geoms'; + +describe('XYChart - State tooltips', () => { + let store: Store; + beforeEach(() => { + const storeReducer = chartStoreReducer('chartId'); + store = createStore(storeReducer); + store.dispatch( + upsertSpec( + MockSeriesSpec.bar({ + data: [ + { x: 1, y: 10 }, + { x: 2, y: 5 }, + ], + }), + ), + ); + store.dispatch(upsertSpec(MockGlobalSpec.settings())); + store.dispatch(specParsed()); + store.dispatch(updateParentDimensions({ width: 100, height: 100, top: 0, left: 0 })); + }); + + describe('should compute tooltip values depending on tooltip type', () => { + it.each<[TooltipType, number, boolean, number]>([ + [TooltipType.None, 0, true, 0], + [TooltipType.Follow, 1, false, 1], + [TooltipType.VerticalCursor, 1, false, 1], + [TooltipType.Crosshairs, 1, false, 1], + ])('tooltip type %s', (tooltipType, expectedHgeomsLength, expectHeader, expectedTooltipValuesLength) => { + store.dispatch(onPointerMove({ x: 25, y: 50 }, 0)); + store.dispatch( + upsertSpec( + MockGlobalSpec.settings({ + tooltip: { + type: tooltipType, + }, + }), + ), + ); + store.dispatch( + upsertSpec( + MockSeriesSpec.bar({ + data: [ + { x: 1, y: 10 }, + { x: 2, y: 5 }, + ], + }), + ), + ); + store.dispatch(specParsed()); + const state = store.getState(); + const tooltipValues = getTooltipInfoAndGeometriesSelector(state); + expect(tooltipValues.tooltip.values).toHaveLength(expectedTooltipValuesLength); + expect(tooltipValues.tooltip.header === null).toBe(expectHeader); + expect(tooltipValues.highlightedGeometries).toHaveLength(expectedHgeomsLength); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.tsx b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.tsx new file mode 100644 index 000000000000..a8080fc93b2c --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.tsx @@ -0,0 +1,167 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { RefObject } from 'react'; + +import { ChartType } from '../..'; +import { LegendItemExtraValues } from '../../../common/legend'; +import { SeriesKey } from '../../../common/series_id'; +import { BrushTool } from '../../../components/brush/brush'; +import { Tooltip } from '../../../components/tooltip'; +import { InternalChartState, GlobalChartState, BackwardRef } from '../../../state/chart_state'; +import { getChartContainerDimensionsSelector } from '../../../state/selectors/get_chart_container_dimensions'; +import { InitStatus } from '../../../state/selectors/get_internal_is_intialized'; +import { htmlIdGenerator } from '../../../utils/common'; +import { XYChart } from '../renderer/canvas/xy_chart'; +import { Annotations } from '../renderer/dom/annotations'; +import { Crosshair } from '../renderer/dom/crosshair'; +import { Highlighter } from '../renderer/dom/highlighter'; +import { computeChartDimensionsSelector } from './selectors/compute_chart_dimensions'; +import { computeLegendSelector } from './selectors/compute_legend'; +import { getBrushAreaSelector } from './selectors/get_brush_area'; +import { getChartTypeDescriptionSelector } from './selectors/get_chart_type_description'; +import { getPointerCursorSelector } from './selectors/get_cursor_pointer'; +import { getDebugStateSelector } from './selectors/get_debug_state'; +import { getHighlightedValuesSelector } from './selectors/get_highlighted_values'; +import { getLegendItemsLabelsSelector } from './selectors/get_legend_items_labels'; +import { getSeriesSpecsSelector } from './selectors/get_specs'; +import { getTooltipAnchorPositionSelector } from './selectors/get_tooltip_position'; +import { getTooltipInfoSelector } from './selectors/get_tooltip_values_highlighted_geoms'; +import { isBrushAvailableSelector } from './selectors/is_brush_available'; +import { isBrushingSelector } from './selectors/is_brushing'; +import { isChartEmptySelector } from './selectors/is_chart_empty'; +import { isTooltipVisibleSelector } from './selectors/is_tooltip_visible'; +import { createOnBrushEndCaller } from './selectors/on_brush_end_caller'; +import { createOnClickCaller } from './selectors/on_click_caller'; +import { createOnElementOutCaller } from './selectors/on_element_out_caller'; +import { createOnElementOverCaller } from './selectors/on_element_over_caller'; +import { createOnPointerMoveCaller } from './selectors/on_pointer_move_caller'; + +/** @internal */ +export class XYAxisChartState implements InternalChartState { + chartType: ChartType; + + legendId: string; + + onClickCaller: (state: GlobalChartState) => void; + + onElementOverCaller: (state: GlobalChartState) => void; + + onElementOutCaller: (state: GlobalChartState) => void; + + onBrushEndCaller: (state: GlobalChartState) => void; + + onPointerMoveCaller: (state: GlobalChartState) => void; + + constructor() { + this.onClickCaller = createOnClickCaller(); + this.onElementOverCaller = createOnElementOverCaller(); + this.onElementOutCaller = createOnElementOutCaller(); + this.onBrushEndCaller = createOnBrushEndCaller(); + this.onPointerMoveCaller = createOnPointerMoveCaller(); + + this.chartType = ChartType.XYAxis; + this.legendId = htmlIdGenerator()('legend'); + } + + isInitialized(globalState: GlobalChartState) { + return getSeriesSpecsSelector(globalState).length > 0 ? InitStatus.Initialized : InitStatus.SpecNotInitialized; + } + + isBrushAvailable(globalState: GlobalChartState) { + return isBrushAvailableSelector(globalState); + } + + isBrushing(globalState: GlobalChartState) { + return isBrushingSelector(globalState); + } + + isChartEmpty(globalState: GlobalChartState) { + return isChartEmptySelector(globalState); + } + + getMainProjectionArea(globalState: GlobalChartState) { + return computeChartDimensionsSelector(globalState).chartDimensions; + } + + getProjectionContainerArea(globalState: GlobalChartState) { + return getChartContainerDimensionsSelector(globalState); + } + + getBrushArea(globalState: GlobalChartState) { + return getBrushAreaSelector(globalState); + } + + getLegendItemsLabels(globalState: GlobalChartState) { + return getLegendItemsLabelsSelector(globalState); + } + + getLegendItems(globalState: GlobalChartState) { + return computeLegendSelector(globalState); + } + + getLegendExtraValues(globalState: GlobalChartState): Map { + return getHighlightedValuesSelector(globalState); + } + + chartRenderer(containerRef: BackwardRef, forwardCanvasRef: RefObject) { + return ( + <> + + + + + + + + ); + } + + getPointerCursor(globalState: GlobalChartState) { + return getPointerCursorSelector(globalState); + } + + isTooltipVisible(globalState: GlobalChartState) { + return isTooltipVisibleSelector(globalState); + } + + getTooltipInfo(globalState: GlobalChartState) { + return getTooltipInfoSelector(globalState); + } + + getTooltipAnchor(globalState: GlobalChartState) { + return getTooltipAnchorPositionSelector(globalState); + } + + eventCallbacks(globalState: GlobalChartState) { + this.onElementOverCaller(globalState); + this.onElementOutCaller(globalState); + this.onClickCaller(globalState); + this.onBrushEndCaller(globalState); + this.onPointerMoveCaller(globalState); + } + + getDebugState(globalState: GlobalChartState) { + return getDebugStateSelector(globalState); + } + + getChartTypeDescription(globalState: GlobalChartState) { + return getChartTypeDescriptionSelector(globalState); + } +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_annotations.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_annotations.ts new file mode 100644 index 000000000000..b151d68aa998 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_annotations.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { AnnotationId } from '../../../../utils/ids'; +import { AnnotationDimensions } from '../../annotations/types'; +import { computeAnnotationDimensions } from '../../annotations/utils'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { computeSeriesGeometriesSelector } from './compute_series_geometries'; +import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; +import { getAxisSpecsSelector, getAnnotationSpecsSelector } from './get_specs'; +import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; + +/** @internal */ +export const computeAnnotationDimensionsSelector = createCachedSelector( + [ + getAnnotationSpecsSelector, + computeChartDimensionsSelector, + getSettingsSpecSelector, + computeSeriesGeometriesSelector, + getAxisSpecsSelector, + isHistogramModeEnabledSelector, + computeSmallMultipleScalesSelector, + ], + ( + annotationSpecs, + chartDimensions, + settingsSpec, + { scales: { yScales, xScale } }, + axesSpecs, + isHistogramMode, + smallMultipleScales, + ): Map => + computeAnnotationDimensions( + annotationSpecs, + chartDimensions.chartDimensions, + settingsSpec.rotation, + yScales, + xScale, + axesSpecs, + isHistogramMode, + smallMultipleScales, + ), +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_axes_geometries.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_axes_geometries.ts new file mode 100644 index 000000000000..7c68a34da98c --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_axes_geometries.ts @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getAxesGeometries, AxisGeometry, defaultTickFormatter } from '../../utils/axis_utils'; +import { computeAxisTicksDimensionsSelector } from './compute_axis_ticks_dimensions'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { computeSeriesDomainsSelector } from './compute_series_domains'; +import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; +import { countBarsInClusterSelector } from './count_bars_in_cluster'; +import { getAxesStylesSelector } from './get_axis_styles'; +import { getBarPaddingsSelector } from './get_bar_paddings'; +import { getAxisSpecsSelector, getSeriesSpecsSelector } from './get_specs'; +import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; + +/** @internal */ +export const computeAxesGeometriesSelector = createCachedSelector( + [ + computeChartDimensionsSelector, + getChartThemeSelector, + getSettingsSpecSelector, + getAxisSpecsSelector, + computeAxisTicksDimensionsSelector, + getAxesStylesSelector, + computeSeriesDomainsSelector, + countBarsInClusterSelector, + isHistogramModeEnabledSelector, + getBarPaddingsSelector, + getSeriesSpecsSelector, + computeSmallMultipleScalesSelector, + ], + ( + chartDimensions, + chartTheme, + settingsSpec, + axesSpecs, + axesTicksDimensions, + axesStyles, + seriesDomainsAndData, + totalBarsInCluster, + isHistogramMode, + barsPadding, + seriesSpecs, + smScales, + ): AxisGeometry[] => { + const fallBackTickFormatter = seriesSpecs.find(({ tickFormat }) => tickFormat)?.tickFormat ?? defaultTickFormatter; + const { xDomain, yDomains } = seriesDomainsAndData; + + return getAxesGeometries( + chartDimensions, + chartTheme, + settingsSpec.rotation, + axesSpecs, + axesTicksDimensions, + axesStyles, + xDomain, + yDomains, + smScales, + totalBarsInCluster, + isHistogramMode, + fallBackTickFormatter, + barsPadding, + ); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_axis_ticks_dimensions.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_axis_ticks_dimensions.ts new file mode 100644 index 000000000000..03a1c9a686d1 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_axis_ticks_dimensions.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { CanvasTextBBoxCalculator } from '../../../../utils/bbox/canvas_text_bbox_calculator'; +import { AxisId } from '../../../../utils/ids'; +import { + computeAxisTicksDimensions, + AxisTicksDimensions, + isDuplicateAxis, + defaultTickFormatter, +} from '../../utils/axis_utils'; +import { computeSeriesDomainsSelector } from './compute_series_domains'; +import { countBarsInClusterSelector } from './count_bars_in_cluster'; +import { getAxesStylesSelector } from './get_axis_styles'; +import { getBarPaddingsSelector } from './get_bar_paddings'; +import { getAxisSpecsSelector, getSeriesSpecsSelector } from './get_specs'; +import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; + +/** @internal */ +export const computeAxisTicksDimensionsSelector = createCachedSelector( + [ + getBarPaddingsSelector, + isHistogramModeEnabledSelector, + getAxisSpecsSelector, + getChartThemeSelector, + getSettingsSpecSelector, + computeSeriesDomainsSelector, + countBarsInClusterSelector, + getSeriesSpecsSelector, + getAxesStylesSelector, + ], + ( + barsPadding, + isHistogramMode, + axesSpecs, + chartTheme, + settingsSpec, + seriesDomainsAndData, + totalBarsInCluster, + seriesSpecs, + axesStyles, + ): Map => { + const { xDomain, yDomains } = seriesDomainsAndData; + const fallBackTickFormatter = seriesSpecs.find(({ tickFormat }) => tickFormat)?.tickFormat ?? defaultTickFormatter; + const bboxCalculator = new CanvasTextBBoxCalculator(); + const axesTicksDimensions: Map = new Map(); + axesSpecs.forEach((axisSpec) => { + const { id } = axisSpec; + const axisStyle = axesStyles.get(id) ?? chartTheme.axes; + const dimensions = computeAxisTicksDimensions( + axisSpec, + xDomain, + yDomains, + totalBarsInCluster, + bboxCalculator, + settingsSpec.rotation, + axisStyle, + fallBackTickFormatter, + barsPadding, + isHistogramMode, + ); + if ( + dimensions && + (!settingsSpec.hideDuplicateAxes || !isDuplicateAxis(axisSpec, dimensions, axesTicksDimensions, axesSpecs)) + ) { + axesTicksDimensions.set(id, dimensions); + } + }); + bboxCalculator.destroy(); + return axesTicksDimensions; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_chart_dimensions.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_chart_dimensions.ts new file mode 100644 index 000000000000..df69be257e1e --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_chart_dimensions.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { getSmallMultiplesSpec } from '../../../../state/selectors/get_small_multiples_spec'; +import { computeChartDimensions, ChartDimensions } from '../../utils/dimensions'; +import { computeAxisTicksDimensionsSelector } from './compute_axis_ticks_dimensions'; +import { getAxesStylesSelector } from './get_axis_styles'; +import { getAxisSpecsSelector } from './get_specs'; + +/** @internal */ +export const computeChartDimensionsSelector = createCachedSelector( + [ + getChartContainerDimensionsSelector, + getChartThemeSelector, + computeAxisTicksDimensionsSelector, + getAxisSpecsSelector, + getAxesStylesSelector, + getSmallMultiplesSpec, + ], + (chartContainerDimensions, chartTheme, axesTicksDimensions, axesSpecs, axesStyles, smSpec): ChartDimensions => + computeChartDimensions( + chartContainerDimensions, + chartTheme, + axesTicksDimensions, + axesStyles, + axesSpecs, + smSpec && smSpec[0], + ), +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_chart_transform.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_chart_transform.ts new file mode 100644 index 000000000000..b935c57f96ce --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_chart_transform.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { Transform } from '../utils/types'; +import { computeChartTransform } from '../utils/utils'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; + +/** @internal */ +export const computeChartTransformSelector = createCachedSelector( + [computeChartDimensionsSelector, getSettingsSpecSelector], + (chartDimensions, settingsSpecs): Transform => + computeChartTransform(chartDimensions.chartDimensions, settingsSpecs.rotation), +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_grid_lines.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_grid_lines.ts new file mode 100644 index 000000000000..646bdf2747cf --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_grid_lines.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { getGridLines, LinesGrid } from '../../utils/grid_lines'; +import { computeAxesGeometriesSelector } from './compute_axes_geometries'; +import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; +import { getAxisSpecsSelector } from './get_specs'; + +/** @internal */ +export const computePerPanelGridLinesSelector = createCachedSelector( + [getAxisSpecsSelector, getChartThemeSelector, computeAxesGeometriesSelector, computeSmallMultipleScalesSelector], + (axesSpecs, chartTheme, axesGeoms, scales): Array => { + return getGridLines(axesSpecs, axesGeoms, chartTheme.axes, scales); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_legend.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_legend.ts new file mode 100644 index 000000000000..1f6fcaed5d52 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_legend.ts @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { LegendItem } from '../../../../common/legend'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { getDeselectedSeriesSelector } from '../../../../state/selectors/get_deselected_data_series'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { computeLegend } from '../../legend/legend'; +import { DataSeries } from '../../utils/series'; +import { getLastValues } from '../utils/get_last_value'; +import { computeSeriesDomainsSelector } from './compute_series_domains'; +import { getSeriesColorsSelector } from './get_series_color_map'; +import { getSiDataSeriesMapSelector } from './get_si_dataseries_map'; +import { getSeriesSpecsSelector, getAxisSpecsSelector } from './get_specs'; + +/** @internal */ +export const computeLegendSelector = createCachedSelector( + [ + getSeriesSpecsSelector, + computeSeriesDomainsSelector, + getChartThemeSelector, + getSeriesColorsSelector, + getAxisSpecsSelector, + getDeselectedSeriesSelector, + getSettingsSpecSelector, + getSiDataSeriesMapSelector, + ], + ( + seriesSpecs, + { formattedDataSeries, xDomain }, + chartTheme, + seriesColors, + axesSpecs, + deselectedDataSeries, + settings, + siDataSeriesMap: Record, + ): LegendItem[] => { + const lastValues = getLastValues(formattedDataSeries, xDomain); + return computeLegend( + formattedDataSeries, + lastValues, + seriesColors, + seriesSpecs, + chartTheme.colors.defaultVizColor, + axesSpecs, + settings.showLegendExtra, + siDataSeriesMap, + deselectedDataSeries, + // @ts-ignore + settings.sortSeriesBy, + ); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_panels.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_panels.ts new file mode 100644 index 000000000000..13263523a7b2 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_panels.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { Size } from '../../../../utils/dimensions'; +import { getPanelSize } from '../../utils/panel'; +import { PerPanelMap, getPerPanelMap } from '../../utils/panel_utils'; +import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; + +/** @internal */ +export type PanelGeoms = Array; + +/** @internal */ +export const computePanelsSelectors = createCachedSelector( + [computeSmallMultipleScalesSelector], + (scales): PanelGeoms => { + const panelSize = getPanelSize(scales); + return getPerPanelMap(scales, () => panelSize); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_per_panel_axes_geoms.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_per_panel_axes_geoms.ts new file mode 100644 index 000000000000..38034b6c5770 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_per_panel_axes_geoms.ts @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { Position, safeFormat } from '../../../../utils/common'; +import { isHorizontalAxis, isVerticalAxis } from '../../utils/axis_type_utils'; +import { AxisGeometry } from '../../utils/axis_utils'; +import { hasSMDomain } from '../../utils/panel'; +import { PerPanelMap, getPerPanelMap } from '../../utils/panel_utils'; +import { computeAxesGeometriesSelector } from './compute_axes_geometries'; +import { computeSmallMultipleScalesSelector, SmallMultipleScales } from './compute_small_multiple_scales'; +import { getSmallMultiplesIndexOrderSelector, SmallMultiplesGroupBy } from './get_specs'; + +/** @internal */ +export type PerPanelAxisGeoms = { + axesGeoms: AxisGeometry[]; +} & PerPanelMap; + +const getPanelTitle = ( + isVertical: boolean, + verticalValue: any, + horizontalValue: any, + groupBy?: SmallMultiplesGroupBy, +): string => { + const formatter = isVertical ? groupBy?.vertical?.format : groupBy?.horizontal?.format; + const value = isVertical ? `${verticalValue}` : `${horizontalValue}`; + + return safeFormat(value, formatter); +}; + +const isPrimaryColumnFn = ({ horizontal: { domain } }: SmallMultipleScales) => ( + position: Position, + horizontalValue: any, +) => isVerticalAxis(position) && domain[0] === horizontalValue; + +const isPrimaryRowFn = ({ vertical: { domain } }: SmallMultipleScales) => (position: Position, verticalValue: any) => + isHorizontalAxis(position) && domain[0] === verticalValue; + +/** @internal */ +export const computePerPanelAxesGeomsSelector = createCachedSelector( + [computeAxesGeometriesSelector, computeSmallMultipleScalesSelector, getSmallMultiplesIndexOrderSelector], + (axesGeoms, scales, groupBySpec): Array => { + const { horizontal, vertical } = scales; + const isPrimaryColumn = isPrimaryColumnFn(scales); + const isPrimaryRow = isPrimaryRowFn(scales); + + return getPerPanelMap(scales, (_, h, v) => ({ + axesGeoms: axesGeoms.map((geom) => { + const { + axis: { position }, + } = geom; + const isVertical = isVerticalAxis(position); + const usePanelTitle = isVertical ? hasSMDomain(vertical) : hasSMDomain(horizontal); + const panelTitle = usePanelTitle ? getPanelTitle(isVertical, v, h, groupBySpec) : undefined; + const secondary = !isPrimaryColumn(position, h) && !isPrimaryRow(position, v); + + return { + ...geom, + axis: { + ...geom.axis, + panelTitle, + secondary, + }, + }; + }), + })); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts new file mode 100644 index 000000000000..f9ba585acf66 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { SeriesDomainsAndData } from '../utils/types'; +import { computeSeriesDomains } from '../utils/utils'; +import { getScaleConfigsFromSpecsSelector } from './get_api_scale_configs'; +import { getSeriesSpecsSelector, getSmallMultiplesIndexOrderSelector } from './get_specs'; + +const getDeselectedSeriesSelector = (state: GlobalChartState) => state.interactions.deselectedDataSeries; + +/** @internal */ +export const computeSeriesDomainsSelector = createCachedSelector( + [ + getSeriesSpecsSelector, + getDeselectedSeriesSelector, + getSettingsSpecSelector, + getSmallMultiplesIndexOrderSelector, + getScaleConfigsFromSpecsSelector, + ], + (seriesSpecs, deselectedDataSeries, settingsSpec, smallMultiples, scaleConfigs): SeriesDomainsAndData => { + return computeSeriesDomains( + seriesSpecs, + scaleConfigs, + deselectedDataSeries, + settingsSpec.orderOrdinalBinsBy, + smallMultiples, + // @ts-ignore + settingsSpec.sortSeriesBy, + ); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts new file mode 100644 index 000000000000..689444acb53f --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { ComputedGeometries } from '../utils/types'; +import { computeSeriesGeometries } from '../utils/utils'; +import { computeSeriesDomainsSelector } from './compute_series_domains'; +import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; +import { getSeriesColorsSelector } from './get_series_color_map'; +import { getSeriesSpecsSelector, getAxisSpecsSelector } from './get_specs'; +import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; + +/** @internal */ +export const computeSeriesGeometriesSelector = createCachedSelector( + [ + getSettingsSpecSelector, + getSeriesSpecsSelector, + computeSeriesDomainsSelector, + getSeriesColorsSelector, + getChartThemeSelector, + getAxisSpecsSelector, + computeSmallMultipleScalesSelector, + isHistogramModeEnabledSelector, + ], + ( + settingsSpec, + seriesSpecs, + seriesDomainsAndData, + seriesColors, + chartTheme, + axesSpecs, + smallMultiplesScales, + isHistogramMode, + ): ComputedGeometries => { + return computeSeriesGeometries( + seriesSpecs, + seriesDomainsAndData, + seriesColors, + chartTheme, + settingsSpec.rotation, + axesSpecs, + smallMultiplesScales, + isHistogramMode, + ); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_small_multiple_scales.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_small_multiple_scales.ts new file mode 100644 index 000000000000..dc3f2a15c09a --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_small_multiple_scales.ts @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { ScaleBand } from '../../../../scales'; +import { DEFAULT_SM_PANEL_PADDING, RelativeBandsPadding } from '../../../../specs/small_multiples'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSmallMultiplesSpec } from '../../../../state/selectors/get_small_multiples_spec'; +import { OrdinalDomain } from '../../../../utils/domain'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { computeSeriesDomainsSelector } from './compute_series_domains'; + +/** @internal */ +export interface SmallMultipleScales { + horizontal: ScaleBand; + vertical: ScaleBand; +} + +/** + * Return the small multiple scales for horizontal and vertical grids + * @internal + */ +export const computeSmallMultipleScalesSelector = createCachedSelector( + [computeSeriesDomainsSelector, computeChartDimensionsSelector, getSmallMultiplesSpec], + ({ smHDomain, smVDomain }, { chartDimensions: { width, height } }, smSpec): SmallMultipleScales => { + return { + horizontal: getScale(smHDomain, width, smSpec && smSpec[0].style?.horizontalPanelPadding), + vertical: getScale(smVDomain, height, smSpec && smSpec[0].style?.verticalPanelPadding), + }; + }, +)(getChartIdSelector); + +/** + * @internal + */ +export function getScale( + domain: OrdinalDomain, + maxRange: number, + padding: RelativeBandsPadding = DEFAULT_SM_PANEL_PADDING, +) { + const singlePanelSmallMultiple = domain.length <= 1; + const defaultDomain = domain.length === 0 ? [undefined] : domain; + return new ScaleBand(defaultDomain, [0, maxRange], undefined, singlePanelSmallMultiple ? 0 : padding); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/count_bars_in_cluster.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/count_bars_in_cluster.ts new file mode 100644 index 000000000000..bd01192d6b2a --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/count_bars_in_cluster.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { SeriesType } from '../../../../specs'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { groupBy } from '../../utils/group_data_series'; +import { SeriesDomainsAndData } from '../utils/types'; +import { getBarIndexKey } from '../utils/utils'; +import { computeSeriesDomainsSelector } from './compute_series_domains'; +import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; + +/** @internal */ +export const countBarsInClusterSelector = createCachedSelector( + [computeSeriesDomainsSelector, isHistogramModeEnabledSelector], + countBarsInCluster, +)(getChartIdSelector); + +/** @internal */ +export function countBarsInCluster({ formattedDataSeries }: SeriesDomainsAndData, isHistogramEnabled: boolean): number { + const barDataSeries = formattedDataSeries.filter(({ seriesType }) => seriesType === SeriesType.Bar); + + const dataSeriesGroupedByPanel = groupBy( + barDataSeries, + ['smVerticalAccessorValue', 'smHorizontalAccessorValue'], + false, + ); + + const barIndexByPanel = Object.keys(dataSeriesGroupedByPanel).reduce>((acc, panelKey) => { + const panelBars = dataSeriesGroupedByPanel[panelKey]; + const barDataSeriesByBarIndex = groupBy( + panelBars, + (d) => { + return getBarIndexKey(d, isHistogramEnabled); + }, + false, + ); + + acc[panelKey] = Object.keys(barDataSeriesByBarIndex); + return acc; + }, {}); + + return Object.values(barIndexByPanel).reduce((acc, curr) => { + return Math.max(acc, curr.length); + }, 0); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts new file mode 100644 index 000000000000..0cc5218bb571 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts @@ -0,0 +1,173 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { TooltipPortalSettings } from '../../../../components/portal/types'; +import { TooltipInfo } from '../../../../components/tooltip/types'; +import { DOMElement } from '../../../../state/actions/dom_element'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation'; +import { Rotation } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { AnnotationId } from '../../../../utils/ids'; +import { Point } from '../../../../utils/point'; +import { AnnotationLineProps } from '../../annotations/line/types'; +import { AnnotationRectProps } from '../../annotations/rect/types'; +import { computeRectAnnotationTooltipState } from '../../annotations/tooltip'; +import { AnnotationTooltipState, AnnotationDimensions } from '../../annotations/types'; +import { AxisSpec, AnnotationSpec, AnnotationType } from '../../utils/specs'; +import { ComputedGeometries } from '../utils/types'; +import { computeAnnotationDimensionsSelector } from './compute_annotations'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { computeSeriesGeometriesSelector } from './compute_series_geometries'; +import { getAxisSpecsSelector, getAnnotationSpecsSelector } from './get_specs'; +import { getTooltipInfoSelector } from './get_tooltip_values_highlighted_geoms'; + +const getCurrentPointerPosition = (state: GlobalChartState) => state.interactions.pointer.current.position; +const getHoveredDOMElement = (state: GlobalChartState) => state.interactions.hoveredDOMElement; + +/** @internal */ +export const getAnnotationTooltipStateSelector = createCachedSelector( + [ + getCurrentPointerPosition, + computeChartDimensionsSelector, + computeSeriesGeometriesSelector, + getChartRotationSelector, + getAnnotationSpecsSelector, + getAxisSpecsSelector, + computeAnnotationDimensionsSelector, + getTooltipInfoSelector, + getHoveredDOMElement, + ], + getAnnotationTooltipState, +)(getChartIdSelector); + +function getAnnotationTooltipState( + cursorPosition: Point, + { + chartDimensions, + }: { + chartDimensions: Dimensions; + }, + geometries: ComputedGeometries, + chartRotation: Rotation, + annotationSpecs: AnnotationSpec[], + axesSpecs: AxisSpec[], + annotationDimensions: Map, + tooltip: TooltipInfo, + hoveredDOMElement: DOMElement | null, +): AnnotationTooltipState | null { + const hoveredTooltip = getTooltipStateForDOMElements( + chartDimensions, + annotationSpecs, + annotationDimensions, + hoveredDOMElement, + ); + if (hoveredTooltip) { + return hoveredTooltip; + } + // get positions relative to chart + if (cursorPosition.x < 0 || cursorPosition.y < 0) { + return null; + } + const { xScale, yScales } = geometries.scales; + // only if we have a valid cursor position and the necessary scale + if (!xScale || !yScales) { + return null; + } + const tooltipState = computeRectAnnotationTooltipState( + cursorPosition, + annotationDimensions, + annotationSpecs, + chartRotation, + axesSpecs, + chartDimensions, + ); + + // If there's a highlighted chart element tooltip value, don't show annotation tooltip + const isChartTooltipDisplayed = tooltip.values.some(({ isHighlighted }) => isHighlighted); + if ( + tooltipState && + tooltipState.isVisible && + tooltipState.annotationType === AnnotationType.Rectangle && + isChartTooltipDisplayed + ) { + return null; + } + + return tooltipState; +} + +function getTooltipStateForDOMElements( + chartDimensions: Dimensions, + annotationSpecs: AnnotationSpec[], + annotationDimensions: Map, + hoveredDOMElement: DOMElement | null, +): AnnotationTooltipState | null { + if (!hoveredDOMElement) { + return null; + } + // current type for hoveredDOMElement is only used for line annotation markers + // and we can safety cast the union types to the respective Line types + const spec = annotationSpecs.find(({ id }) => id === hoveredDOMElement.createdBySpecId); + if (!spec || spec.hideTooltips) { + return null; + } + const dimension = (annotationDimensions.get(hoveredDOMElement.createdBySpecId) ?? []) + .filter(isAnnotationLineProps) + .find(({ id }) => id === hoveredDOMElement.id); + + if (!dimension) { + return null; + } + + return { + isVisible: true, + annotationType: AnnotationType.Line, + datum: dimension.datum, + anchor: { + y: (dimension.markers[0]?.position.top ?? 0) + dimension.panel.top + chartDimensions.top, + x: (dimension.markers[0]?.position.left ?? 0) + dimension.panel.left + chartDimensions.left, + width: 0, + height: 0, + }, + customTooltipDetails: spec.customTooltipDetails, + customTooltip: spec.customTooltip, + tooltipSettings: getTooltipSettings(spec), + }; +} +function isAnnotationLineProps(prop: AnnotationLineProps | AnnotationRectProps): prop is AnnotationLineProps { + return 'linePathPoints' in prop; +} + +function getTooltipSettings({ + placement, + fallbackPlacements, + boundary, + offset, +}: AnnotationSpec): TooltipPortalSettings<'chart'> { + return { + placement, + fallbackPlacements, + boundary, + offset, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_api_scale_configs.test.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_api_scale_configs.test.ts new file mode 100644 index 000000000000..60c532d8aeb1 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_api_scale_configs.test.ts @@ -0,0 +1,94 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs/specs'; +import { MockStore } from '../../../../mocks/store/store'; +import { DEFAULT_GLOBAL_ID } from '../../utils/specs'; +import { computeSeriesGeometriesSelector } from './compute_series_geometries'; +import { getScaleConfigsFromSpecsSelector } from './get_api_scale_configs'; + +describe('GroupIds and useDefaultGroupId', () => { + it('use the specified useDefaultGroupId to compute scale configs', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + groupId: 'other', + useDefaultGroupDomain: 'a different one', + }), + ], + store, + ); + const scaleConfigs = getScaleConfigsFromSpecsSelector(store.getState()); + expect(scaleConfigs.y['a different one']).toBeDefined(); + }); + + it('have 2 different y domains with 2 groups', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ id: 'one' }), + MockSeriesSpec.bar({ + id: 'two', + groupId: 'other', + useDefaultGroupDomain: 'a different one', + }), + ], + store, + ); + const scaleConfigs = getScaleConfigsFromSpecsSelector(store.getState()); + expect(Object.keys(scaleConfigs.y)).toHaveLength(2); + expect(scaleConfigs.y['a different one']).toBeDefined(); + expect(scaleConfigs.y[DEFAULT_GLOBAL_ID]).toBeDefined(); + }); + + it('have 2 different y domains with 3 groups', () => { + const store = MockStore.default({ width: 120, height: 100, left: 0, top: 0 }); + MockStore.addSpecs( + [ + MockGlobalSpec.settingsNoMargins(), + MockSeriesSpec.bar({ id: 'one', data: [{ x: 1, y: 10 }] }), + MockSeriesSpec.bar({ + id: 'two', + groupId: 'other', + useDefaultGroupDomain: 'a different one', + data: [{ x: 1, y: 10 }], + }), + MockSeriesSpec.bar({ + id: 'three', + groupId: 'another again', + useDefaultGroupDomain: 'a different one', + data: [{ x: 1, y: 10 }], + }), + ], + store, + ); + const scaleConfigs = getScaleConfigsFromSpecsSelector(store.getState()); + expect(Object.keys(scaleConfigs.y)).toHaveLength(2); + expect(scaleConfigs.y['a different one']).toBeDefined(); + expect(scaleConfigs.y[DEFAULT_GLOBAL_ID]).toBeDefined(); + + const geoms = computeSeriesGeometriesSelector(store.getState()); + const { bars } = geoms.geometries; + expect(bars).toHaveLength(3); + expect(bars[0].value[0].width).toBe(40); + expect(bars[1].value[0].width).toBe(40); + expect(bars[2].value[0].width).toBe(40); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_api_scale_configs.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_api_scale_configs.ts new file mode 100644 index 000000000000..ecec96bd0e82 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_api_scale_configs.ts @@ -0,0 +1,118 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { ScaleContinuousType } from '../../../../scales'; +import { ScaleType } from '../../../../scales/constants'; +import { SettingsSpec } from '../../../../specs/settings'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { GroupId } from '../../../../utils/ids'; +import { convertXScaleTypes } from '../../domains/x_domain'; +import { coerceYScaleTypes } from '../../domains/y_domain'; +import { getYNiceFromSpec, getYScaleTypeFromSpec } from '../../scales/get_api_scales'; +import { X_SCALE_DEFAULT, Y_SCALE_DEFAULT } from '../../scales/scale_defaults'; +import { isHorizontalAxis, isVerticalAxis } from '../../utils/axis_type_utils'; +import { groupBy } from '../../utils/group_data_series'; +import { AxisSpec, BasicSeriesSpec, CustomXDomain, XScaleType, YDomainRange } from '../../utils/specs'; +import { isHorizontalRotation } from '../utils/common'; +import { getSpecDomainGroupId } from '../utils/spec'; +import { getAxisSpecsSelector, getSeriesSpecsSelector } from './get_specs'; +import { mergeYCustomDomainsByGroupId } from './merge_y_custom_domains'; + +/** @internal */ +export type ScaleConfigBase = { + type: T; + nice: boolean; + desiredTickCount: number; + customDomain?: D; +}; +type XScaleConfigBase = ScaleConfigBase; +type YScaleConfigBase = ScaleConfigBase; + +/** @internal */ +export interface ScaleConfigs { + x: XScaleConfigBase & { + isBandScale: boolean; + timeZone?: string; + }; + y: Record; +} + +/** @internal */ +export const getScaleConfigsFromSpecsSelector = createCachedSelector( + [getAxisSpecsSelector, getSeriesSpecsSelector, getSettingsSpecSelector], + getScaleConfigsFromSpecs, +)(getChartIdSelector); + +/** @internal */ +export function getScaleConfigsFromSpecs( + axisSpecs: AxisSpec[], + seriesSpecs: BasicSeriesSpec[], + settingsSpec: SettingsSpec, +): ScaleConfigs { + const isHorizontalChart = isHorizontalRotation(settingsSpec.rotation); + + // x axis + const xAxes = axisSpecs.filter((d) => isHorizontalChart === isHorizontalAxis(d.position)); + const xTicks = xAxes.reduce((acc, { ticks = X_SCALE_DEFAULT.desiredTickCount }) => { + return Math.max(acc, ticks); + }, X_SCALE_DEFAULT.desiredTickCount); + + const xScaleConfig = convertXScaleTypes(seriesSpecs); + const x: ScaleConfigs['x'] = { + customDomain: settingsSpec.xDomain, + ...xScaleConfig, + desiredTickCount: xTicks, + }; + + // y axes + const scaleConfigsByGroupId = groupBy(seriesSpecs, getSpecDomainGroupId, true).reduce< + Record + >((acc, series) => { + const yScaleTypes = series.map(({ yScaleType, yNice }) => ({ + nice: getYNiceFromSpec(yNice), + type: getYScaleTypeFromSpec(yScaleType), + })); + const groupId = getSpecDomainGroupId(series[0]); + acc[groupId] = coerceYScaleTypes(yScaleTypes); + return acc; + }, {}); + + const customDomainByGroupId = mergeYCustomDomainsByGroupId(axisSpecs, settingsSpec.rotation); + + const yAxes = axisSpecs.filter((d) => isHorizontalChart === isVerticalAxis(d.position)); + const y = Object.keys(scaleConfigsByGroupId).reduce((acc, groupId) => { + const axis = yAxes.find((yAxis) => yAxis.groupId === groupId); + const desiredTickCount = axis?.ticks ?? Y_SCALE_DEFAULT.desiredTickCount; + const scaleConfig = scaleConfigsByGroupId[groupId]; + const customDomain = customDomainByGroupId.get(groupId); + if (!acc[groupId]) { + acc[groupId] = { + customDomain, + ...scaleConfig, + desiredTickCount, + }; + } + acc[groupId].desiredTickCount = Math.max(acc[groupId].desiredTickCount, desiredTickCount); + return acc; + }, {}); + return { x, y }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_axis_styles.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_axis_styles.ts new file mode 100644 index 000000000000..2209924fd078 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_axis_styles.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { mergePartial, RecursivePartial } from '../../../../utils/common'; +import { AxisId } from '../../../../utils/ids'; +import { AxisStyle } from '../../../../utils/themes/theme'; +import { isVerticalAxis } from '../../utils/axis_type_utils'; +import { getAxisSpecsSelector } from './get_specs'; + +/** + * Get merged axis styles. **Only** include axes with styles overrides. + * + * @internal + */ +export const getAxesStylesSelector = createCachedSelector( + [getAxisSpecsSelector, getChartThemeSelector], + (axesSpecs, { axes: sharedAxesStyle }): Map => { + const axesStyles = new Map(); + axesSpecs.forEach(({ id, style, gridLine, position }) => { + const isVertical = isVerticalAxis(position); + const axisStyleMerge: RecursivePartial = { + ...style, + }; + if (gridLine) { + axisStyleMerge.gridLine = { [isVertical ? 'vertical' : 'horizontal']: gridLine }; + } + const newStyle = style + ? mergePartial(sharedAxesStyle, axisStyleMerge, { + mergeOptionalPartialValues: true, + }) + : null; + axesStyles.set(id, newStyle); + }); + return axesStyles; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_bar_paddings.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_bar_paddings.ts new file mode 100644 index 000000000000..a536fe2113d4 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_bar_paddings.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; + +/** @internal */ +export const getBarPaddingsSelector = createCachedSelector( + [isHistogramModeEnabledSelector, getChartThemeSelector], + (isHistogramMode, chartTheme): number => + isHistogramMode ? chartTheme.scales.histogramPadding : chartTheme.scales.barsPadding, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_brush_area.test.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_brush_area.test.ts new file mode 100644 index 000000000000..c4b8ad9e52e9 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_brush_area.test.ts @@ -0,0 +1,317 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs/specs'; +import { MockStore } from '../../../../mocks/store/store'; +import { ScaleType } from '../../../../scales/constants'; +import { BrushAxis } from '../../../../specs/constants'; +import { onMouseDown, onMouseUp, onPointerMove } from '../../../../state/actions/mouse'; +import { getBrushAreaSelector } from './get_brush_area'; + +describe('getBrushArea selector', () => { + describe('compute brush', () => { + it('along the X axis', () => { + const store = MockStore.default({ left: 0, top: 0, width: 100, height: 100 }); + const onBrushEnd = jest.fn(); + MockStore.addSpecs( + [ + MockGlobalSpec.settingsNoMargins({ onBrushEnd }), + MockSeriesSpec.line({ + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + data: [ + [0, 10], + [0.5, 5], + [1, 3], + ], + }), + ], + store, + ); + store.dispatch(onMouseDown({ x: 10, y: 10 }, 0)); + store.dispatch(onPointerMove({ x: 30, y: 30 }, 1000)); + const xBrushArea = getBrushAreaSelector(store.getState()); + store.dispatch(onMouseUp({ x: 30, y: 30 }, 1100)); + store.getState().internalChartState?.eventCallbacks(store.getState()); + + expect(onBrushEnd).toHaveBeenCalled(); + expect(onBrushEnd.mock.calls[0][0].x[0]).toBeCloseTo(0.1); + expect(onBrushEnd.mock.calls[0][0].x[1]).toBeCloseTo(0.3); + + expect(xBrushArea).toEqual({ + top: 0, + left: 10, + width: 20, + height: 100, + }); + }); + + it('along the Y axis', () => { + const store = MockStore.default({ left: 0, top: 0, width: 100, height: 100 }); + const onBrushEnd = jest.fn(); + MockStore.addSpecs( + [ + MockGlobalSpec.settingsNoMargins({ onBrushEnd, brushAxis: BrushAxis.Y }), + MockSeriesSpec.line({ + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + data: [ + [0, 10], + [0.5, 5], + [1, 3], + ], + }), + ], + store, + ); + store.dispatch(onMouseDown({ x: 10, y: 10 }, 0)); + store.dispatch(onPointerMove({ x: 30, y: 30 }, 1000)); + const yBrushArea = getBrushAreaSelector(store.getState()); + store.dispatch(onMouseUp({ x: 30, y: 30 }, 1100)); + store.getState().internalChartState?.eventCallbacks(store.getState()); + + expect(onBrushEnd).toHaveBeenCalled(); + const brushData = onBrushEnd.mock.calls[0][0].y; + + expect(brushData[0].extent[0]).toBeCloseTo(7); + expect(brushData[0].extent[1]).toBeCloseTo(9); + + expect(yBrushArea).toEqual({ + top: 10, + left: 0, + width: 100, + height: 20, + }); + }); + + it('along both axis', () => { + const store = MockStore.default({ left: 0, top: 0, width: 100, height: 100 }); + const onBrushEnd = jest.fn(); + MockStore.addSpecs( + [ + MockGlobalSpec.settingsNoMargins({ onBrushEnd, brushAxis: BrushAxis.Both }), + MockSeriesSpec.line({ + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + data: [ + [0, 10], + [0.5, 5], + [1, 3], + ], + }), + ], + store, + ); + store.dispatch(onMouseDown({ x: 10, y: 10 }, 0)); + store.dispatch(onPointerMove({ x: 30, y: 30 }, 1000)); + const bothBrushArea = getBrushAreaSelector(store.getState()); + store.dispatch(onMouseUp({ x: 30, y: 30 }, 1100)); + store.getState().internalChartState?.eventCallbacks(store.getState()); + + expect(onBrushEnd).toHaveBeenCalled(); + + expect(onBrushEnd.mock.calls[0][0].x[0]).toBeCloseTo(0.1); + expect(onBrushEnd.mock.calls[0][0].x[1]).toBeCloseTo(0.3); + expect(onBrushEnd.mock.calls[0][0].y[0].extent[0]).toBeCloseTo(7); + expect(onBrushEnd.mock.calls[0][0].y[0].extent[1]).toBeCloseTo(9); + + expect(bothBrushArea).toEqual({ + top: 10, + left: 10, + width: 20, + height: 20, + }); + }); + }); + + describe('compute brush on a rotated chart', () => { + it('along the X axis', () => { + const store = MockStore.default({ left: 0, top: 0, width: 100, height: 100 }); + const onBrushEnd = jest.fn(); + MockStore.addSpecs( + [ + MockGlobalSpec.settingsNoMargins({ onBrushEnd, rotation: 90 }), + MockSeriesSpec.line({ + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + data: [ + [0, 10], + [0.5, 5], + [1, 3], + ], + }), + ], + store, + ); + store.dispatch(onMouseDown({ x: 10, y: 10 }, 0)); + store.dispatch(onPointerMove({ x: 30, y: 30 }, 1000)); + const xBrushArea = getBrushAreaSelector(store.getState()); + store.dispatch(onMouseUp({ x: 30, y: 30 }, 1100)); + store.getState().internalChartState?.eventCallbacks(store.getState()); + + expect(onBrushEnd).toHaveBeenCalled(); + expect(onBrushEnd.mock.calls[0][0].x[0]).toBeCloseTo(0.1); + expect(onBrushEnd.mock.calls[0][0].x[1]).toBeCloseTo(0.3); + + expect(xBrushArea).toEqual({ + top: 10, + left: 0, + width: 100, + height: 20, + }); + }); + + it('along the Y axis', () => { + const store = MockStore.default({ left: 0, top: 0, width: 100, height: 100 }); + const onBrushEnd = jest.fn(); + MockStore.addSpecs( + [ + MockGlobalSpec.settingsNoMargins({ onBrushEnd, brushAxis: BrushAxis.Y, rotation: 90 }), + MockSeriesSpec.line({ + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + data: [ + [0, 10], + [0.5, 5], + [1, 3], + ], + }), + ], + store, + ); + store.dispatch(onMouseDown({ x: 10, y: 10 }, 0)); + store.dispatch(onPointerMove({ x: 30, y: 30 }, 1000)); + const yBrushArea = getBrushAreaSelector(store.getState()); + store.dispatch(onMouseUp({ x: 30, y: 30 }, 1100)); + store.getState().internalChartState?.eventCallbacks(store.getState()); + + expect(onBrushEnd).toHaveBeenCalled(); + const brushData = onBrushEnd.mock.calls[0][0].y; + + expect(brushData[0].extent[0]).toBeCloseTo(1); + expect(brushData[0].extent[1]).toBeCloseTo(3); + + expect(yBrushArea).toEqual({ + top: 0, + left: 10, + width: 20, + height: 100, + }); + }); + + it('along both axis', () => { + const store = MockStore.default({ left: 0, top: 0, width: 100, height: 100 }); + const onBrushEnd = jest.fn(); + MockStore.addSpecs( + [ + MockGlobalSpec.settingsNoMargins({ onBrushEnd, brushAxis: BrushAxis.Both, rotation: 90 }), + MockSeriesSpec.line({ + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + data: [ + [0, 10], + [0.5, 5], + [1, 3], + ], + }), + ], + store, + ); + store.dispatch(onMouseDown({ x: 10, y: 10 }, 0)); + store.dispatch(onPointerMove({ x: 30, y: 30 }, 1000)); + const bothBrushArea = getBrushAreaSelector(store.getState()); + store.dispatch(onMouseUp({ x: 30, y: 30 }, 1100)); + store.getState().internalChartState?.eventCallbacks(store.getState()); + + expect(onBrushEnd).toHaveBeenCalled(); + + expect(onBrushEnd.mock.calls[0][0].x[0]).toBeCloseTo(0.1); + expect(onBrushEnd.mock.calls[0][0].x[1]).toBeCloseTo(0.3); + expect(onBrushEnd.mock.calls[0][0].y[0].extent[0]).toBeCloseTo(1); + expect(onBrushEnd.mock.calls[0][0].y[0].extent[1]).toBeCloseTo(3); + + expect(bothBrushArea).toEqual({ + top: 10, + left: 10, + width: 20, + height: 20, + }); + }); + }); + + describe('limit brush to single panel w/small multiples', () => { + const store = MockStore.default({ left: 0, top: 0, width: 200, height: 200 }); + const onBrushEnd = jest.fn(); + MockStore.addSpecs( + [ + MockGlobalSpec.settingsNoMargins({ onBrushEnd }), + MockGlobalSpec.groupBy({ id: 'hSplit' }), + MockGlobalSpec.groupBy({ id: 'vSplit' }), + MockGlobalSpec.smallMultiple({ splitHorizontally: 'hSplit', splitVertically: 'vSplit' }), + MockSeriesSpec.line({ + id: '1', + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + data: [ + [0, 10], + [0.5, 5], + [1, 3], + ], + }), + MockSeriesSpec.line({ + id: '2', + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + data: [ + [0, 5], + [0.5, 2], + [1, 6], + ], + }), + ], + store, + ); + + store.dispatch(onMouseDown({ x: 150, y: 10 }, 0)); + store.dispatch(onPointerMove({ x: 10, y: 150 }, 1000)); + const bothBrushArea = getBrushAreaSelector(store.getState()); + expect(bothBrushArea).toEqual({ + top: 0, + left: 150, + width: -50, + height: 100, + }); + + store.dispatch(onMouseUp({ x: 10, y: 150 }, 1100)); + store.getState().internalChartState?.eventCallbacks(store.getState()); + + expect(onBrushEnd).toHaveBeenCalled(); + + expect(onBrushEnd.mock.calls[0][0].x[0]).toBeCloseTo(0); + expect(onBrushEnd.mock.calls[0][0].x[1]).toBeCloseTo(0.5); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_brush_area.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_brush_area.ts new file mode 100644 index 000000000000..3de1d554df90 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_brush_area.ts @@ -0,0 +1,167 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { BrushAxis } from '../../../../specs/constants'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { clamp, Rotation } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { Point } from '../../../../utils/point'; +import { isVerticalRotation } from '../utils/common'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { computeSmallMultipleScalesSelector, SmallMultipleScales } from './compute_small_multiple_scales'; + +const MIN_AREA_SIZE = 1; + +const getMouseDownPosition = (state: GlobalChartState) => state.interactions.pointer.down?.position; +const getCurrentPointerPosition = (state: GlobalChartState) => state.interactions.pointer.current.position; + +/** @internal */ +export const getBrushAreaSelector = createCachedSelector( + [ + getMouseDownPosition, + getCurrentPointerPosition, + getChartRotationSelector, + computeChartDimensionsSelector, + getSettingsSpecSelector, + computeSmallMultipleScalesSelector, + ], + (start, end, chartRotation, { chartDimensions }, { brushAxis }, smallMultipleScales): Dimensions | null => { + if (!start) { + return null; + } + const plotStartPointPx = getPlotAreaRestrictedPoint(start, chartDimensions); + const plotEndPointPx = getPlotAreaRestrictedPoint(end, chartDimensions); + const panelPoints = getPointsConstraintToSinglePanel(plotStartPointPx, plotEndPointPx, smallMultipleScales); + + switch (brushAxis) { + case BrushAxis.Y: + return getBrushForYAxis(chartRotation, panelPoints); + case BrushAxis.Both: + return getBrushForBothAxis(panelPoints); + case BrushAxis.X: + default: + return getBrushForXAxis(chartRotation, panelPoints); + } + }, +)(getChartIdSelector); + +/** @internal */ +export type PanelPoints = { + start: Point; + end: Point; + hPanelStart: number; + hPanelWidth: number; + vPanelStart: number; + vPanelHeight: number; +}; + +/** @internal */ +export function getPointsConstraintToSinglePanel( + startPlotPoint: Point, + endPlotPoint: Point, + { horizontal, vertical }: SmallMultipleScales, +): PanelPoints { + const hPanel = horizontal.invert(startPlotPoint.x); + const vPanel = vertical.invert(startPlotPoint.y); + + const hPanelStart = horizontal.scale(hPanel) ?? 0; + const hPanelEnd = hPanelStart + horizontal.bandwidth; + + const vPanelStart = vertical.scale(vPanel) ?? 0; + const vPanelEnd = vPanelStart + vertical.bandwidth; + + const start = { + x: clamp(startPlotPoint.x, hPanelStart, hPanelEnd), + y: clamp(startPlotPoint.y, vPanelStart, vPanelEnd), + }; + const end = { + x: clamp(endPlotPoint.x, hPanelStart, hPanelEnd), + y: clamp(endPlotPoint.y, vPanelStart, vPanelEnd), + }; + + return { + start, + end, + hPanelStart, + hPanelWidth: horizontal.bandwidth, + vPanelStart, + vPanelHeight: vertical.bandwidth, + }; +} + +/** @internal */ +export function getPlotAreaRestrictedPoint({ x, y }: Point, { left, top }: Dimensions) { + return { + x: x - left, + y: y - top, + }; +} + +/** @internal */ +export function getBrushForXAxis( + chartRotation: Rotation, + { hPanelStart, vPanelStart, hPanelWidth, vPanelHeight, start, end }: PanelPoints, +) { + const rotated = isVerticalRotation(chartRotation); + + return { + left: rotated ? hPanelStart : start.x, + top: rotated ? start.y : vPanelStart, + height: rotated ? getMinSize(start.y, end.y) : vPanelHeight, + width: rotated ? hPanelWidth : getMinSize(start.x, end.x), + }; +} + +/** @internal */ +export function getBrushForYAxis( + chartRotation: Rotation, + { hPanelStart, vPanelStart, hPanelWidth, vPanelHeight, start, end }: PanelPoints, +) { + const rotated = isVerticalRotation(chartRotation); + + return { + left: rotated ? start.x : hPanelStart, + top: rotated ? vPanelStart : start.y, + height: rotated ? vPanelHeight : getMinSize(start.y, end.y), + width: rotated ? getMinSize(start.x, end.x) : hPanelWidth, + }; +} + +/** @internal */ +export function getBrushForBothAxis({ start, end }: PanelPoints) { + return { + left: start.x, + top: start.y, + height: getMinSize(start.y, end.y), + width: getMinSize(start.x, end.x), + }; +} + +function getMinSize(a: number, b: number, minSize = MIN_AREA_SIZE) { + const size = b - a; + if (Math.abs(size) < minSize) { + return minSize; + } + return size; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_chart_type_description.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_chart_type_description.ts new file mode 100644 index 000000000000..b491bb48d5b8 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_chart_type_description.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { SeriesType } from '../../utils/specs'; +import { getSeriesSpecsSelector } from './get_specs'; + +/** @internal */ +export const getChartTypeDescriptionSelector = createCachedSelector([getSeriesSpecsSelector], (specs): string => { + const seriesTypes = new Set(); + specs.forEach((value) => seriesTypes.add(value.seriesType)); + const chartSeriesTypes = + seriesTypes.size > 1 ? `Mixed chart: ${[...seriesTypes].join(' and ')} chart` : `${[...seriesTypes]} chart`; + return chartSeriesTypes; +})(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_computed_scales.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_computed_scales.ts new file mode 100644 index 000000000000..fa3244c4e38d --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_computed_scales.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { ComputedScales } from '../utils/types'; +import { computeSeriesGeometriesSelector } from './compute_series_geometries'; + +/** @internal */ +export const getComputedScalesSelector = createCachedSelector( + [computeSeriesGeometriesSelector], + ({ scales }): ComputedScales => scales, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts new file mode 100644 index 000000000000..94aa15b19888 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts @@ -0,0 +1,158 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { Rect } from '../../../../geoms/types'; +import { Scale } from '../../../../scales'; +import { SettingsSpec, PointerEvent } from '../../../../specs/settings'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { Dimensions } from '../../../../utils/dimensions'; +import { isValidPointerOverEvent } from '../../../../utils/events'; +import { getCursorBandPosition } from '../../crosshair/crosshair_utils'; +import { BasicSeriesSpec } from '../../utils/specs'; +import { isLineAreaOnlyChart } from '../utils/common'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { computeSeriesGeometriesSelector } from './compute_series_geometries'; +import { computeSmallMultipleScalesSelector, SmallMultipleScales } from './compute_small_multiple_scales'; +import { countBarsInClusterSelector } from './count_bars_in_cluster'; +import { getGeometriesIndexKeysSelector } from './get_geometries_index_keys'; +import { getOrientedProjectedPointerPositionSelector } from './get_oriented_projected_pointer_position'; +import { PointerPosition } from './get_projected_pointer_position'; +import { getSeriesSpecsSelector } from './get_specs'; +import { isTooltipSnapEnableSelector } from './is_tooltip_snap_enabled'; + +const getExternalPointerEventStateSelector = (state: GlobalChartState) => state.externalEvents.pointer; + +/** @internal */ +export const getCursorBandPositionSelector = createCachedSelector( + [ + getOrientedProjectedPointerPositionSelector, + getExternalPointerEventStateSelector, + computeChartDimensionsSelector, + getSettingsSpecSelector, + computeSeriesGeometriesSelector, + getSeriesSpecsSelector, + countBarsInClusterSelector, + isTooltipSnapEnableSelector, + getGeometriesIndexKeysSelector, + computeSmallMultipleScalesSelector, + ], + ( + orientedProjectedPointerPosition, + externalPointerEvent, + chartDimensions, + settingsSpec, + seriesGeometries, + seriesSpec, + totalBarsInCluster, + isTooltipSnapEnabled, + geometriesIndexKeys, + smallMultipleScales, + ) => + getCursorBand( + orientedProjectedPointerPosition, + externalPointerEvent, + chartDimensions.chartDimensions, + settingsSpec, + seriesGeometries.scales.xScale, + seriesSpec, + totalBarsInCluster, + isTooltipSnapEnabled, + geometriesIndexKeys, + smallMultipleScales, + ), +)(getChartIdSelector); + +function getCursorBand( + orientedProjectedPointerPosition: PointerPosition, + externalPointerEvent: PointerEvent | null, + chartDimensions: Dimensions, + settingsSpec: SettingsSpec, + xScale: Scale | undefined, + seriesSpecs: BasicSeriesSpec[], + totalBarsInCluster: number, + isTooltipSnapEnabled: boolean, + geometriesIndexKeys: (string | number)[], + smallMultipleScales: SmallMultipleScales, +): (Rect & { fromExternalEvent: boolean }) | undefined { + if (!xScale) { + return; + } + // update che cursorBandPosition based on chart configuration + const isLineAreaOnly = isLineAreaOnlyChart(seriesSpecs); + + let pointerPosition = { ...orientedProjectedPointerPosition }; + + let xValue; + let fromExternalEvent = false; + // external pointer events takes precedence over the current mouse pointer + if (isValidPointerOverEvent(xScale, externalPointerEvent)) { + fromExternalEvent = true; + const x = xScale.pureScale(externalPointerEvent.value); + if (x == null || x > chartDimensions.width || x < 0) { + return; + } + pointerPosition = { + x, + y: 0, + verticalPanelValue: null, + horizontalPanelValue: null, + }; + xValue = { + value: externalPointerEvent.value, + withinBandwidth: true, + }; + } else { + xValue = xScale.invertWithStep(orientedProjectedPointerPosition.x, geometriesIndexKeys); + if (!xValue) { + return; + } + } + const { horizontal, vertical } = smallMultipleScales; + const topPos = vertical.scale(pointerPosition.verticalPanelValue) || 0; + const leftPos = horizontal.scale(pointerPosition.horizontalPanelValue) || 0; + + const panel = { + width: horizontal.bandwidth, + height: vertical.bandwidth, + top: chartDimensions.top + topPos, + left: chartDimensions.left + leftPos, + }; + const cursorBand = getCursorBandPosition( + settingsSpec.rotation, + panel, + pointerPosition, + { + value: xValue.value, + withinBandwidth: true, + }, + isTooltipSnapEnabled, + xScale, + isLineAreaOnly ? 0 : totalBarsInCluster, + ); + return ( + cursorBand && { + ...cursorBand, + fromExternalEvent, + } + ); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_cursor_line.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_cursor_line.ts new file mode 100644 index 000000000000..f928e6b8635a --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_cursor_line.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { Line } from '../../../../geoms/types'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getCursorLinePosition } from '../../crosshair/crosshair_utils'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { getProjectedPointerPositionSelector } from './get_projected_pointer_position'; + +/** @internal */ +export const getCursorLinePositionSelector = createCachedSelector( + [computeChartDimensionsSelector, getSettingsSpecSelector, getProjectedPointerPositionSelector], + (chartDimensions, settingsSpec, projectedPointerPosition): Line | undefined => + getCursorLinePosition(settingsSpec.rotation, chartDimensions.chartDimensions, projectedPointerPosition), +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_cursor_pointer.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_cursor_pointer.ts new file mode 100644 index 000000000000..21ed49dfc79f --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_cursor_pointer.ts @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { DEFAULT_CSS_CURSOR } from '../../../../common/constants'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { getProjectedScaledValues } from './get_projected_scaled_values'; +import { getHighlightedGeomsSelector } from './get_tooltip_values_highlighted_geoms'; +import { isBrushAvailableSelector } from './is_brush_available'; + +const getCurrentPointerPositionSelector = (state: GlobalChartState) => state.interactions.pointer.current.position; + +/** @internal */ +export const getPointerCursorSelector = createCachedSelector( + [ + getHighlightedGeomsSelector, + getSettingsSpecSelector, + getCurrentPointerPositionSelector, + getProjectedScaledValues, + computeChartDimensionsSelector, + isBrushAvailableSelector, + ], + ( + highlightedGeometries, + settingsSpec, + currentPointerPosition, + projectedValues, + { chartDimensions }, + isBrushAvailable, + ): string => { + const { x, y } = currentPointerPosition; + // get positions relative to chart + const xPos = x - chartDimensions.left; + const yPos = y - chartDimensions.top; + + // limit cursorPosition to chartDimensions + if (xPos < 0 || xPos >= chartDimensions.width || yPos < 0 || yPos >= chartDimensions.height) { + return DEFAULT_CSS_CURSOR; + } + if (highlightedGeometries.length > 0 && (settingsSpec.onElementClick || settingsSpec.onElementOver)) { + return 'pointer'; + } + if (projectedValues !== null && settingsSpec.onProjectionClick) { + return 'pointer'; + } + return isBrushAvailable ? 'crosshair' : DEFAULT_CSS_CURSOR; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_debug_state.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_debug_state.ts new file mode 100644 index 000000000000..c9268bab1b16 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_debug_state.ts @@ -0,0 +1,297 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { LegendItem } from '../../../../common/legend'; +import { Line } from '../../../../geoms/types'; +import { AxisSpec } from '../../../../specs'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { + DebugState, + DebugStateValue, + DebugStateAxes, + DebugStateArea, + DebugStateLine, + DebugStateBar, + DebugStateLegend, +} from '../../../../state/types'; +import { AreaGeometry, BandedAccessorType, LineGeometry, BarGeometry, PerPanel } from '../../../../utils/geometry'; +import { FillStyle, Visible, StrokeStyle, Opacity } from '../../../../utils/themes/theme'; +import { isVerticalAxis } from '../../utils/axis_type_utils'; +import { AxisGeometry } from '../../utils/axis_utils'; +import { LinesGrid } from '../../utils/grid_lines'; +import { computeAxesGeometriesSelector } from './compute_axes_geometries'; +import { computeLegendSelector } from './compute_legend'; +import { computeSeriesGeometriesSelector } from './compute_series_geometries'; +import { computeGridLinesSelector } from './get_grid_lines'; +import { getAxisSpecsSelector } from './get_specs'; + +/** + * Returns a stringified version of the `debugState` + * @internal + */ +export const getDebugStateSelector = createCachedSelector( + [ + computeSeriesGeometriesSelector, + computeLegendSelector, + computeAxesGeometriesSelector, + computeGridLinesSelector, + getAxisSpecsSelector, + ], + ({ geometries }, legend, axes, gridLines, axesSpecs): DebugState => { + const seriesNameMap = getSeriesNameMap(legend); + + return { + legend: getLegendState(legend), + axes: getAxes(axes, axesSpecs, gridLines), + areas: geometries.areas.map(getAreaState(seriesNameMap)), + lines: geometries.lines.map(getLineState(seriesNameMap)), + bars: getBarsState(seriesNameMap, geometries.bars), + }; + }, +)(getChartIdSelector); + +function getAxes(axesGeoms: AxisGeometry[], axesSpecs: AxisSpec[], gridLines: LinesGrid[]): DebugStateAxes | undefined { + if (axesSpecs.length === 0) { + return; + } + + return axesSpecs.reduce( + (acc, { position, title, id }) => { + const geom = axesGeoms.find(({ axis }) => axis.id === id); + if (!geom) { + return acc; + } + + const visibleTicks = geom.visibleTicks.filter(({ label }) => label !== ''); + const labels = visibleTicks.map(({ label }) => label); + const values = visibleTicks.map(({ value }) => value); + + const gridlines = gridLines + .reduce((accLines, { lineGroups }) => { + const groupLines = lineGroups.find(({ axisId }) => { + return axisId === geom.axis.id; + }); + if (!groupLines) { + return accLines; + } + return [...accLines, ...groupLines.lines]; + }, []) + .map(({ x1, y1 }) => ({ x: x1, y: y1 })); + + if (isVerticalAxis(position)) { + acc.y.push({ + id, + title, + position, + // reverse for bottom/up coordinates + labels: labels.reverse(), + values: values.reverse(), + gridlines: gridlines.reverse(), + }); + } else { + acc.x.push({ + id, + title, + position, + labels, + values, + gridlines, + }); + } + + return acc; + }, + { + y: [], + x: [], + }, + ); +} + +function getBarsState( + seriesNameMap: Map, + barGeometries: Array>, +): DebugStateBar[] { + const buckets = new Map(); + const bars = barGeometries.reduce((acc, bars) => { + return [...acc, ...bars.value]; + }, []); + bars.forEach( + ({ + color, + seriesIdentifier: { key }, + seriesStyle: { rect, rectBorder }, + value: { x, y, mark }, + displayValue, + }: BarGeometry) => { + const label = displayValue?.text; + const name = seriesNameMap.get(key) ?? ''; + const bucket: DebugStateBar = buckets.get(key) ?? { + key, + name, + color, + bars: [], + labels: [], + visible: hasVisibleStyle(rect) || hasVisibleStyle(rectBorder), + }; + + bucket.bars.push({ x, y, mark }); + + if (label) { + bucket.labels.push(label); + } + + buckets.set(key, bucket); + + return buckets; + }, + ); + + return [...buckets.values()]; +} + +function getLineState(seriesNameMap: Map) { + return ({ + value: { + line: path, + points, + color, + seriesIdentifier: { key }, + seriesLineStyle, + seriesPointStyle, + }, + }: PerPanel): DebugStateLine => { + const name = seriesNameMap.get(key) ?? ''; + + return { + path, + color, + key, + name, + visible: hasVisibleStyle(seriesLineStyle), + visiblePoints: hasVisibleStyle(seriesPointStyle), + points: points.map(({ value: { x, y, mark } }) => ({ x, y, mark })), + }; + }; +} + +function getAreaState(seriesNameMap: Map) { + return ({ + value: { + area: path, + lines, + points, + color, + seriesIdentifier: { key }, + seriesAreaStyle, + seriesPointStyle, + seriesAreaLineStyle, + }, + }: PerPanel): DebugStateArea => { + const [y1Path, y0Path] = lines; + const linePoints = points.reduce<{ + y0: DebugStateValue[]; + y1: DebugStateValue[]; + }>( + (acc, { value: { accessor, ...value } }) => { + if (accessor === BandedAccessorType.Y0) { + acc.y0.push(value); + } else { + acc.y1.push(value); + } + + return acc; + }, + { + y0: [], + y1: [], + }, + ); + const lineVisible = hasVisibleStyle(seriesAreaLineStyle); + const visiblePoints = hasVisibleStyle(seriesPointStyle); + const name = seriesNameMap.get(key) ?? ''; + + return { + path, + color, + key, + name, + visible: hasVisibleStyle(seriesAreaStyle), + lines: { + y0: y0Path + ? { + visible: lineVisible, + path: y0Path, + points: linePoints.y0, + visiblePoints, + } + : undefined, + y1: { + visible: lineVisible, + path: y1Path, + points: linePoints.y1, + visiblePoints, + }, + }, + }; + }; +} + +/** + * returns series key to name mapping + */ +function getSeriesNameMap(legendItems: LegendItem[]): Map { + return legendItems.reduce((acc, { label: name, seriesIdentifiers }) => { + seriesIdentifiers.forEach(({ key }) => { + acc.set(key, name); + }); + return acc; + }, new Map()); +} + +function getLegendState(legendItems: LegendItem[]): DebugStateLegend { + const items = legendItems + .filter(({ isSeriesHidden }) => !isSeriesHidden) + .map(({ label: name, color, seriesIdentifiers }) => { + return seriesIdentifiers.map(({ key }) => ({ + key, + name, + color, + })); + }) + .flat(); + + return { items }; +} + +/** + * Returns true for styles if they are visible + * Serves as a catchall for multiple style types + */ +function hasVisibleStyle({ + visible = true, + fill = '#fff', + stroke = '#fff', + strokeWidth = 1, + opacity = 1, +}: Partial): boolean { + return Boolean(visible && opacity > 0 && strokeWidth > 0 && fill && stroke); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_elements_at_cursor_pos.test.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_elements_at_cursor_pos.test.ts new file mode 100644 index 000000000000..6847e8a1dcd2 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_elements_at_cursor_pos.test.ts @@ -0,0 +1,98 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Store } from 'redux'; + +import { MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs/specs'; +import { MockStore } from '../../../../mocks/store/store'; +import { ScaleType } from '../../../../scales/constants'; +import { onPointerMove } from '../../../../state/actions/mouse'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getElementAtCursorPositionSelector } from './get_elements_at_cursor_pos'; + +const data = [ + { x: 0, y: 2 }, + { x: 0, y: 2.2 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, +]; + +describe('getElementAtCursorPositionSelector', () => { + let store: Store; + + describe('Area', () => { + beforeEach(() => { + store = MockStore.default({ width: 300, height: 300, top: 0, left: 0 }, 'chartId'); + MockStore.addSpecs( + [ + MockGlobalSpec.settingsNoMargins(), + MockSeriesSpec.area({ + data, + xScaleType: ScaleType.Ordinal, + }), + ], + store, + ); + }); + + it('should correctly sort matched points near y = 2', () => { + store.dispatch(onPointerMove({ x: 0, y: 100 }, 0)); + const elements = getElementAtCursorPositionSelector(store.getState()); + expect(elements).toHaveLength(2); + expect(elements.map(({ value }) => value.datum.y)).toEqual([2, 2.2]); + }); + + it('should correctly sort matched points near y = 2.2', () => { + store.dispatch(onPointerMove({ x: 0, y: 80 }, 0)); + const elements = getElementAtCursorPositionSelector(store.getState()); + expect(elements).toHaveLength(2); + expect(elements.map(({ value }) => value.datum.y)).toEqual([2.2, 2]); + }); + }); + + describe('Bubble', () => { + beforeEach(() => { + store = MockStore.default({ width: 300, height: 300, top: 0, left: 0 }, 'chartId'); + MockStore.addSpecs( + [ + MockGlobalSpec.settingsNoMargins(), + MockSeriesSpec.bubble({ + data, + xScaleType: ScaleType.Ordinal, + }), + ], + store, + ); + }); + + it('should correctly sort matched points near y = 2', () => { + store.dispatch(onPointerMove({ x: 0, y: 100 }, 0)); + const elements = getElementAtCursorPositionSelector(store.getState()); + expect(elements).toHaveLength(3); + expect(elements.map(({ value }) => value.datum.y)).toEqual([2, 2.2, 2]); + }); + + it('should correctly sort matched points near y = 2.2', () => { + store.dispatch(onPointerMove({ x: 0, y: 80 }, 0)); + const elements = getElementAtCursorPositionSelector(store.getState()); + expect(elements).toHaveLength(4); + expect(elements.map(({ value }) => value.datum.y)).toEqual([2.2, 2, 2, 3]); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_elements_at_cursor_pos.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_elements_at_cursor_pos.ts new file mode 100644 index 000000000000..e2ad8e77a80d --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_elements_at_cursor_pos.ts @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { PointerEvent } from '../../../../specs'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { isValidPointerOverEvent } from '../../../../utils/events'; +import { IndexedGeometry } from '../../../../utils/geometry'; +import { ChartDimensions } from '../../utils/dimensions'; +import { IndexedGeometryMap } from '../../utils/indexed_geometry_map'; +import { sortClosestToPoint } from '../utils/common'; +import { ComputedScales } from '../utils/types'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { getComputedScalesSelector } from './get_computed_scales'; +import { getGeometriesIndexSelector } from './get_geometries_index'; +import { getGeometriesIndexKeysSelector } from './get_geometries_index_keys'; +import { getOrientedProjectedPointerPositionSelector } from './get_oriented_projected_pointer_position'; +import { PointerPosition } from './get_projected_pointer_position'; + +const getExternalPointerEventStateSelector = (state: GlobalChartState) => state.externalEvents.pointer; + +/** @internal */ +export const getElementAtCursorPositionSelector = createCachedSelector( + [ + getOrientedProjectedPointerPositionSelector, + getComputedScalesSelector, + getGeometriesIndexKeysSelector, + getGeometriesIndexSelector, + getExternalPointerEventStateSelector, + computeChartDimensionsSelector, + ], + getElementAtCursorPosition, +)(getChartIdSelector); + +function getElementAtCursorPosition( + orientedProjectedPointerPosition: PointerPosition, + scales: ComputedScales, + geometriesIndexKeys: (string | number)[], + geometriesIndex: IndexedGeometryMap, + externalPointerEvent: PointerEvent | null, + { chartDimensions }: ChartDimensions, +): IndexedGeometry[] { + if (isValidPointerOverEvent(scales.xScale, externalPointerEvent)) { + const x = scales.xScale.pureScale(externalPointerEvent.value); + + if (x == null || x > chartDimensions.width + chartDimensions.left || x < 0) { + return []; + } + // TODO: Handle external event with spatial points + return geometriesIndex.find(externalPointerEvent.value, { x: -1, y: -1 }); + } + const xValue = scales.xScale.invertWithStep(orientedProjectedPointerPosition.x, geometriesIndexKeys); + if (!xValue) { + return []; + } + // get the elements at cursor position + return geometriesIndex + .find( + xValue?.value, + orientedProjectedPointerPosition, + orientedProjectedPointerPosition.horizontalPanelValue, + orientedProjectedPointerPosition.verticalPanelValue, + ) + .sort(sortClosestToPoint(orientedProjectedPointerPosition)); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_geometries_index.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_geometries_index.ts new file mode 100644 index 000000000000..321b13932ea5 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_geometries_index.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { IndexedGeometryMap } from '../../utils/indexed_geometry_map'; +import { computeSeriesGeometriesSelector } from './compute_series_geometries'; + +/** @internal */ +export const getGeometriesIndexSelector = createCachedSelector( + [computeSeriesGeometriesSelector], + ({ geometriesIndex }): IndexedGeometryMap => geometriesIndex, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_geometries_index_keys.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_geometries_index_keys.ts new file mode 100644 index 000000000000..a8fa7e1814a1 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_geometries_index_keys.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { compareByValueAsc } from '../../../../utils/common'; +import { computeSeriesGeometriesSelector } from './compute_series_geometries'; + +/** @internal */ +export const getGeometriesIndexKeysSelector = createCachedSelector( + [computeSeriesGeometriesSelector], + (seriesGeometries): (number | string)[] => seriesGeometries.geometriesIndex.keys().sort(compareByValueAsc), +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_grid_lines.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_grid_lines.ts new file mode 100644 index 000000000000..23efeab5844f --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_grid_lines.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { getGridLines, LinesGrid } from '../../utils/grid_lines'; +import { computeAxesGeometriesSelector } from './compute_axes_geometries'; +import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; +import { getAxisSpecsSelector } from './get_specs'; + +/** @internal */ +export const computeGridLinesSelector = createCachedSelector( + [getChartThemeSelector, getAxisSpecsSelector, computeAxesGeometriesSelector, computeSmallMultipleScalesSelector], + (chartTheme, axesSpecs, axesGeoms, scales): LinesGrid[] => { + return getGridLines(axesSpecs, axesGeoms, chartTheme.axes, scales); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_highlighted_series.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_highlighted_series.ts new file mode 100644 index 000000000000..241a0a53f1ba --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_highlighted_series.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { LegendItem } from '../../../../common/legend'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { computeLegendSelector } from './compute_legend'; + +const getHighlightedLegendPath = (state: GlobalChartState) => state.interactions.highlightedLegendPath; + +/** @internal */ +export const getHighlightedSeriesSelector = createCachedSelector( + [getHighlightedLegendPath, computeLegendSelector], + (highlightedLegendPaths, legendItems): LegendItem | undefined => { + if (highlightedLegendPaths.length === 0) { + return; + } + const highlightedSeriesKeys = highlightedLegendPaths.map(({ value }) => value); + return legendItems.find(({ seriesIdentifiers }) => + seriesIdentifiers.some(({ key }) => highlightedSeriesKeys.some((hKey) => hKey === key)), + ); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_highlighted_values.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_highlighted_values.ts new file mode 100644 index 000000000000..921c18f4e359 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_highlighted_values.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { LegendItemExtraValues } from '../../../../common/legend'; +import { SeriesKey } from '../../../../common/series_id'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getHighligthedValues } from '../../tooltip/tooltip'; +import { getTooltipInfoSelector } from './get_tooltip_values_highlighted_geoms'; + +/** @internal */ +export const getHighlightedValuesSelector = createCachedSelector( + [getTooltipInfoSelector], + ({ values }): Map => getHighligthedValues(values), +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts new file mode 100644 index 000000000000..100f8362e3f7 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { LegendItemLabel } from '../../../../state/selectors/get_legend_items_labels'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { computeLegendSelector } from './compute_legend'; + +/** @internal */ +export const getLegendItemsLabelsSelector = createCachedSelector( + [computeLegendSelector, getSettingsSpecSelector], + (legendItems, { showLegendExtra }): LegendItemLabel[] => + legendItems.map(({ label, defaultExtra }) => { + if (defaultExtra?.legendSizingLabel != null) { + return { label: `${label}${showLegendExtra ? defaultExtra.legendSizingLabel : ''}`, depth: 0 }; + } + return { label, depth: 0 }; + }), +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_oriented_projected_pointer_position.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_oriented_projected_pointer_position.ts new file mode 100644 index 000000000000..40cc23c896bc --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_oriented_projected_pointer_position.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { SettingsSpec } from '../../../../specs/settings'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getOrientedXPosition, getOrientedYPosition } from '../../utils/interactions'; +import { getPanelSize } from '../../utils/panel'; +import { computeSmallMultipleScalesSelector, SmallMultipleScales } from './compute_small_multiple_scales'; +import { getProjectedPointerPositionSelector, PointerPosition } from './get_projected_pointer_position'; + +/** @internal */ +export const getOrientedProjectedPointerPositionSelector = createCachedSelector( + [getProjectedPointerPositionSelector, getSettingsSpecSelector, computeSmallMultipleScalesSelector], + getOrientedProjectedPointerPosition, +)(getChartIdSelector); + +function getOrientedProjectedPointerPosition( + { x, y, horizontalPanelValue, verticalPanelValue }: PointerPosition, + settingsSpec: SettingsSpec, + scales: SmallMultipleScales, +): PointerPosition { + // get the oriented projected pointer position + const panel = getPanelSize(scales); + return { + x: getOrientedXPosition(x, y, settingsSpec.rotation, panel), + y: getOrientedYPosition(x, y, settingsSpec.rotation, panel), + horizontalPanelValue, + verticalPanelValue, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_projected_pointer_position.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_projected_pointer_position.ts new file mode 100644 index 000000000000..38de8f3a0e58 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_projected_pointer_position.ts @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { ScaleBand } from '../../../../scales/scale_band'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { Dimensions } from '../../../../utils/dimensions'; +import { Point } from '../../../../utils/point'; +import { PrimitiveValue } from '../../../partition_chart/layout/utils/group_by_rollup'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { computeSmallMultipleScalesSelector, SmallMultipleScales } from './compute_small_multiple_scales'; + +const getCurrentPointerPosition = (state: GlobalChartState) => state.interactions.pointer.current.position; + +/** @internal */ +export type PointerPosition = Point & { horizontalPanelValue: PrimitiveValue; verticalPanelValue: PrimitiveValue }; +/** + * Get the x and y pointer position relative to the chart projection area + * @internal + */ +export const getProjectedPointerPositionSelector = createCachedSelector( + [getCurrentPointerPosition, computeChartDimensionsSelector, computeSmallMultipleScalesSelector], + (currentPointerPosition, { chartDimensions }, smallMultipleScales): PointerPosition => + getProjectedPointerPosition(currentPointerPosition, chartDimensions, smallMultipleScales), +)(getChartIdSelector); + +/** + * Get the x and y pointer position relative to the chart projection area + * @param chartAreaPointerPosition the pointer position relative to the chart area + * @param horizontal SmallMultipleScales horizontal panel scale + * @param vertical SmallMultipleScales vertical panel scale + * @param chartAreaDimensions the chart dimensions + */ +function getProjectedPointerPosition( + chartAreaPointerPosition: Point, + { left, top, width, height }: Dimensions, + { horizontal, vertical }: SmallMultipleScales, +): PointerPosition { + const { x, y } = chartAreaPointerPosition; + // get positions relative to chart + let xPos = x - left; + let yPos = y - top; + + // limit cursorPosition to the chart area + if (xPos < 0 || xPos >= width) { + xPos = -1; + } + if (yPos < 0 || yPos >= height) { + yPos = -1; + } + const h = getPosRelativeToPanel(horizontal, xPos); + const v = getPosRelativeToPanel(vertical, yPos); + + return { + x: h.pos, + y: v.pos, + horizontalPanelValue: h.value, + verticalPanelValue: v.value, + }; +} + +function getPosRelativeToPanel(panelScale: ScaleBand, pos: number): { pos: number; value: PrimitiveValue } { + const outerPadding = panelScale.outerPadding * panelScale.step; + const innerPadding = panelScale.innerPadding * panelScale.step; + const numOfDomainSteps = panelScale.domain.length; + const rangeWithoutOuterPaddings = numOfDomainSteps * panelScale.bandwidth + (numOfDomainSteps - 1) * innerPadding; + + if (pos < outerPadding || pos > outerPadding + rangeWithoutOuterPaddings) { + return { pos: -1, value: null }; + } + const posWOInitialOuterPadding = pos - outerPadding; + const minEqualSteps = (numOfDomainSteps - 1) * panelScale.step; + if (posWOInitialOuterPadding <= minEqualSteps) { + const relativePosIndex = Math.floor(posWOInitialOuterPadding / panelScale.step); + const relativePos = posWOInitialOuterPadding - panelScale.step * relativePosIndex; + if (relativePos > panelScale.bandwidth) { + return { pos: -1, value: null }; + } + return { pos: relativePos, value: panelScale.domain[relativePosIndex] }; + } + return { + pos: posWOInitialOuterPadding - panelScale.step * (numOfDomainSteps - 1), + value: panelScale.domain[numOfDomainSteps - 1], + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_projected_scaled_values.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_projected_scaled_values.ts new file mode 100644 index 000000000000..072e4a7d3344 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_projected_scaled_values.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { ProjectedValues } from '../../../../specs/settings'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { computeSeriesGeometriesSelector } from './compute_series_geometries'; +import { getGeometriesIndexKeysSelector } from './get_geometries_index_keys'; +import { getOrientedProjectedPointerPositionSelector } from './get_oriented_projected_pointer_position'; + +/** @internal */ +export const getProjectedScaledValues = createCachedSelector( + [getOrientedProjectedPointerPositionSelector, computeSeriesGeometriesSelector, getGeometriesIndexKeysSelector], + ( + { x, y, verticalPanelValue, horizontalPanelValue }, + { scales: { xScale, yScales } }, + geometriesIndexKeys, + ): ProjectedValues | undefined => { + if (!xScale) { + return; + } + + const xValue = xScale.invertWithStep(x, geometriesIndexKeys); + if (!xValue) { + return; + } + + return { + x: xValue.value, + y: [...yScales.entries()].map(([groupId, yScale]) => { + return { value: yScale.invert(y), groupId }; + }), + smVerticalValue: verticalPanelValue, + smHorizontalValue: horizontalPanelValue, + }; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts new file mode 100644 index 000000000000..4a9436f2028a --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { SeriesKey } from '../../../../common/series_id'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { Color } from '../../../../utils/common'; +import { getSeriesColors } from '../../utils/series'; +import { getCustomSeriesColors } from '../utils/utils'; +import { computeSeriesDomainsSelector } from './compute_series_domains'; + +function getColorOverrides({ colors }: GlobalChartState) { + return colors; +} + +/** @internal */ +export const getSeriesColorsSelector = createCachedSelector( + [computeSeriesDomainsSelector, getChartThemeSelector, getColorOverrides], + (seriesDomainsAndData, chartTheme, colorOverrides): Map => { + const updatedCustomSeriesColors = getCustomSeriesColors(seriesDomainsAndData.formattedDataSeries); + + return getSeriesColors( + seriesDomainsAndData.formattedDataSeries, + chartTheme.colors, + updatedCustomSeriesColors, + colorOverrides, + ); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_si_dataseries_map.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_si_dataseries_map.ts new file mode 100644 index 000000000000..cc94572f4bde --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_si_dataseries_map.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { DataSeries, getSeriesKey } from '../../utils/series'; +import { computeSeriesDomainsSelector } from './compute_series_domains'; + +/** @internal */ +export const getSiDataSeriesMapSelector = createCachedSelector( + [computeSeriesDomainsSelector], + ({ formattedDataSeries }) => { + return formattedDataSeries.reduce>((acc, dataSeries) => { + const seriesKey = getSeriesKey(dataSeries, dataSeries.groupId); + acc[seriesKey] = dataSeries; + return acc; + }, {}); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_specs.test.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_specs.test.ts new file mode 100644 index 000000000000..85421cada501 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_specs.test.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MockSeriesSpec } from '../../../../mocks/specs'; +import { getInitialState } from '../../../../state/chart_state'; +import { getSeriesSpecsSelector } from './get_specs'; + +describe('selector - get_specs', () => { + const state = getInitialState('chartId1'); + beforeEach(() => { + state.specs.bars1 = MockSeriesSpec.bar({ id: 'bars1' }); + state.specs.bars2 = MockSeriesSpec.bar({ id: 'bars2' }); + }); + it('shall return the same ref objects', () => { + const series = getSeriesSpecsSelector(state); + expect(series.length).toBe(2); + const seriesSecondCall = getSeriesSpecsSelector({ ...state, specsInitialized: true }); + expect(series).toBe(seriesSecondCall); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_specs.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_specs.ts new file mode 100644 index 000000000000..624457418772 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_specs.ts @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { ChartType } from '../../..'; +import { GroupBySpec, SmallMultiplesSpec } from '../../../../specs'; +import { SpecType } from '../../../../specs/constants'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSpecs } from '../../../../state/selectors/get_settings_specs'; +import { getSpecsFromStore } from '../../../../state/utils'; +import { AnnotationSpec, AxisSpec, BasicSeriesSpec } from '../../utils/specs'; + +/** @internal */ +export interface SmallMultiplesGroupBy { + vertical?: GroupBySpec; + horizontal?: GroupBySpec; +} + +/** @internal */ +export const getAxisSpecsSelector = createCachedSelector([getSpecs], (specs): AxisSpec[] => + getSpecsFromStore(specs, ChartType.XYAxis, SpecType.Axis), +)(getChartIdSelector); + +/** @internal */ +export const getSeriesSpecsSelector = createCachedSelector([getSpecs], (specs) => { + return getSpecsFromStore(specs, ChartType.XYAxis, SpecType.Series); +})(getChartIdSelector); + +/** @internal */ +export const getAnnotationSpecsSelector = createCachedSelector([getSpecs], (specs) => + getSpecsFromStore(specs, ChartType.XYAxis, SpecType.Annotation), +)(getChartIdSelector); + +/** @internal */ +export const getSmallMultiplesIndexOrderSelector = createCachedSelector([getSpecs], (specs): + | SmallMultiplesGroupBy + | undefined => { + const [smallMultiples] = getSpecsFromStore(specs, ChartType.Global, SpecType.SmallMultiples); + const groupBySpecs = getSpecsFromStore(specs, ChartType.Global, SpecType.IndexOrder); + return { + horizontal: groupBySpecs.find((s) => s.id === smallMultiples?.splitHorizontally), + vertical: groupBySpecs.find((s) => s.id === smallMultiples?.splitVertically), + }; +})(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_position.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_position.ts new file mode 100644 index 000000000000..57bf5d569f1b --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_position.ts @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { AnchorPosition } from '../../../../components/portal/types'; +import { isTooltipType } from '../../../../specs/settings'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getTooltipAnchorPosition } from '../../crosshair/crosshair_utils'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; +import { getCursorBandPositionSelector } from './get_cursor_band'; +import { getProjectedPointerPositionSelector } from './get_projected_pointer_position'; + +/** @internal */ +export const getTooltipAnchorPositionSelector = createCachedSelector( + [ + computeChartDimensionsSelector, + getSettingsSpecSelector, + getCursorBandPositionSelector, + getProjectedPointerPositionSelector, + computeSmallMultipleScalesSelector, + ], + ( + chartDimensions, + settings, + cursorBandPosition, + projectedPointerPosition, + { horizontal, vertical }, + ): AnchorPosition | null => { + if (!cursorBandPosition) { + return null; + } + + const topPos = vertical.scale(projectedPointerPosition.verticalPanelValue) ?? 0; + const leftPos = horizontal.scale(projectedPointerPosition.horizontalPanelValue) ?? 0; + + const panel = { + width: horizontal.bandwidth, + height: vertical.bandwidth, + top: chartDimensions.chartDimensions.top + topPos, + left: chartDimensions.chartDimensions.left + leftPos, + }; + + return getTooltipAnchorPosition( + settings.rotation, + cursorBandPosition, + projectedPointerPosition, + panel, + isTooltipType(settings.tooltip) ? undefined : settings.tooltip.stickTo, + ); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_snap.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_snap.ts new file mode 100644 index 000000000000..cb7748a47d66 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_snap.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { DEFAULT_TOOLTIP_SNAP } from '../../../../specs/constants'; +import { SettingsSpec, isTooltipProps } from '../../../../specs/settings'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; + +/** @internal */ +export const getTooltipSnapSelector = createCachedSelector( + [getSettingsSpecSelector], + getTooltipSnap, +)(getChartIdSelector); + +function getTooltipSnap(settings: SettingsSpec): boolean { + const { tooltip } = settings; + if (tooltip && isTooltipProps(tooltip)) { + return tooltip.snap || false; + } + return DEFAULT_TOOLTIP_SNAP; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_type.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_type.ts new file mode 100644 index 000000000000..37c5c0f1cb27 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_type.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getTooltipType } from '../../../../specs/settings'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; + +/** @internal */ +export const getTooltipTypeSelector = createCachedSelector( + [getSettingsSpecSelector], + getTooltipType, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.test.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.test.ts new file mode 100644 index 000000000000..42e7b0932243 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.test.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Store } from 'redux'; + +import { MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs/specs'; +import { MockStore } from '../../../../mocks/store/store'; +import { ScaleType } from '../../../../scales/constants'; +import { onPointerMove } from '../../../../state/actions/mouse'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getTooltipInfoAndGeometriesSelector } from './get_tooltip_values_highlighted_geoms'; + +describe('Highlight points', () => { + describe('On Ordinal area chart', () => { + let store: Store; + beforeEach(() => { + store = MockStore.default({ width: 300, height: 300, top: 0, left: 0 }, 'chartId'); + MockStore.addSpecs( + [ + MockGlobalSpec.settingsNoMargins(), + MockSeriesSpec.area({ + data: [ + { x: 0, y: 2 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + xScaleType: ScaleType.Ordinal, + }), + ], + store, + ); + }); + it('On ordinal area chart, it should correctly highlight points', () => { + store.dispatch(onPointerMove({ x: 50, y: 100 }, 0)); + const { highlightedGeometries } = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(highlightedGeometries).toHaveLength(1); + }); + it('On ordinal area chart, it should not highlight points if not within the buffer', () => { + store.dispatch(onPointerMove({ x: 5, y: 100 }, 0)); + const { highlightedGeometries } = getTooltipInfoAndGeometriesSelector(store.getState()); + expect(highlightedGeometries).toHaveLength(0); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts new file mode 100644 index 000000000000..0d1807544792 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts @@ -0,0 +1,234 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { TooltipInfo } from '../../../../components/tooltip/types'; +import { + PointerEvent, + isPointerOutEvent, + TooltipValue, + TooltipValueFormatter, + isFollowTooltipType, + SettingsSpec, + getTooltipType, +} from '../../../../specs'; +import { TooltipType } from '../../../../specs/constants'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getTooltipHeaderFormatterSelector } from '../../../../state/selectors/get_tooltip_header_formatter'; +import { Rotation } from '../../../../utils/common'; +import { isValidPointerOverEvent } from '../../../../utils/events'; +import { IndexedGeometry } from '../../../../utils/geometry'; +import { Point } from '../../../../utils/point'; +import { getTooltipCompareFn } from '../../../../utils/series_sort'; +import { isPointOnGeometry } from '../../rendering/utils'; +import { formatTooltip } from '../../tooltip/tooltip'; +import { defaultXYLegendSeriesSort } from '../../utils/default_series_sort_fn'; +import { DataSeries } from '../../utils/series'; +import { BasicSeriesSpec, AxisSpec } from '../../utils/specs'; +import { getAxesSpecForSpecId, getSpecDomainGroupId, getSpecsById } from '../utils/spec'; +import { ComputedScales } from '../utils/types'; +import { getComputedScalesSelector } from './get_computed_scales'; +import { getElementAtCursorPositionSelector } from './get_elements_at_cursor_pos'; +import { getOrientedProjectedPointerPositionSelector } from './get_oriented_projected_pointer_position'; +import { getProjectedPointerPositionSelector } from './get_projected_pointer_position'; +import { getSiDataSeriesMapSelector } from './get_si_dataseries_map'; +import { getSeriesSpecsSelector, getAxisSpecsSelector } from './get_specs'; +import { hasSingleSeriesSelector } from './has_single_series'; + +const EMPTY_VALUES = Object.freeze({ + tooltip: { + header: null, + values: [], + }, + highlightedGeometries: [], +}); + +/** @internal */ +export interface TooltipAndHighlightedGeoms { + tooltip: TooltipInfo; + highlightedGeometries: IndexedGeometry[]; +} + +const getExternalPointerEventStateSelector = (state: GlobalChartState) => state.externalEvents.pointer; + +/** @internal */ +export const getTooltipInfoAndGeometriesSelector = createCachedSelector( + [ + getSeriesSpecsSelector, + getAxisSpecsSelector, + getSettingsSpecSelector, + getProjectedPointerPositionSelector, + getOrientedProjectedPointerPositionSelector, + getChartRotationSelector, + hasSingleSeriesSelector, + getComputedScalesSelector, + getElementAtCursorPositionSelector, + getSiDataSeriesMapSelector, + getExternalPointerEventStateSelector, + getTooltipHeaderFormatterSelector, + ], + getTooltipAndHighlightFromValue, +)(({ chartId }) => chartId); + +function getTooltipAndHighlightFromValue( + seriesSpecs: BasicSeriesSpec[], + axesSpecs: AxisSpec[], + settings: SettingsSpec, + projectedPointerPosition: Point, + orientedProjectedPointerPosition: Point, + chartRotation: Rotation, + hasSingleSeries: boolean, + scales: ComputedScales, + matchingGeoms: IndexedGeometry[], + serialIdentifierDataSeriesMap: Record, + externalPointerEvent: PointerEvent | null, + tooltipHeaderFormatter?: TooltipValueFormatter, +): TooltipAndHighlightedGeoms { + if (!scales.xScale || !scales.yScales) { + return EMPTY_VALUES; + } + + let { x, y } = orientedProjectedPointerPosition; + let tooltipType = getTooltipType(settings); + if (isValidPointerOverEvent(scales.xScale, externalPointerEvent)) { + tooltipType = getTooltipType(settings, true); + const scaledX = scales.xScale.pureScale(externalPointerEvent.value); + + if (scaledX === null) { + return EMPTY_VALUES; + } + + x = scaledX; + y = 0; + } else if (projectedPointerPosition.x === -1 || projectedPointerPosition.y === -1) { + return EMPTY_VALUES; + } + + if (tooltipType === TooltipType.None && !externalPointerEvent) { + return EMPTY_VALUES; + } + + if (matchingGeoms.length === 0) { + return EMPTY_VALUES; + } + + // build the tooltip value list + let header: TooltipValue | null = null; + const highlightedGeometries: IndexedGeometry[] = []; + const xValues = new Set(); + + const values = matchingGeoms + .filter(({ value: { y } }) => y !== null) + .reduce((acc, indexedGeometry) => { + const { + seriesIdentifier: { specId }, + } = indexedGeometry; + const spec = getSpecsById(seriesSpecs, specId); + + // safe guard check + if (!spec) { + return acc; + } + const { xAxis, yAxis } = getAxesSpecForSpecId(axesSpecs, spec.groupId); + + // yScales is ensured by the enclosing if + const yScale = scales.yScales.get(getSpecDomainGroupId(spec)); + if (!yScale) { + return acc; + } + + // check if the pointer is on the geometry (avoid checking if using external pointer event) + let isHighlighted = false; + if ( + (!externalPointerEvent || isPointerOutEvent(externalPointerEvent)) && + isPointOnGeometry(x, y, indexedGeometry, settings.pointBuffer) + ) { + isHighlighted = true; + highlightedGeometries.push(indexedGeometry); + } + + // if it's a follow tooltip, and no element is highlighted + // do _not_ add element into tooltip list + if (!isHighlighted && isFollowTooltipType(tooltipType)) { + return acc; + } + + // format the tooltip values + const yAxisFormatSpec = [0, 180].includes(chartRotation) ? yAxis : xAxis; + const formattedTooltip = formatTooltip( + indexedGeometry, + spec, + false, + isHighlighted, + hasSingleSeries, + yAxisFormatSpec, + ); + + // format only one time the x value + if (!header) { + // if we have a tooltipHeaderFormatter, then don't pass in the xAxis as the user will define a formatter + const xAxisFormatSpec = [0, 180].includes(chartRotation) ? xAxis : yAxis; + const formatterAxis = tooltipHeaderFormatter ? undefined : xAxisFormatSpec; + header = formatTooltip(indexedGeometry, spec, true, false, hasSingleSeries, formatterAxis); + } + + xValues.add(indexedGeometry.value.x); + + return [...acc, formattedTooltip]; + }, []); + + if (values.length > 1 && xValues.size === values.length) { + // TODO: remove after tooltip redesign + header = null; + } + + const tooltipSortFn = getTooltipCompareFn((settings as any).sortSeriesBy, (a, b) => { + const aDs = serialIdentifierDataSeriesMap[a.key]; + const bDs = serialIdentifierDataSeriesMap[b.key]; + return defaultXYLegendSeriesSort(aDs, bDs); + }); + + const sortedTooltipValues = values.sort((a, b) => { + return tooltipSortFn(a.seriesIdentifier, b.seriesIdentifier); + }); + return { + tooltip: { + header, + // to avoid creating a breaking change because of a different sorting order on tooltip + values: sortedTooltipValues, + }, + highlightedGeometries, + }; +} + +/** @internal */ +export const getTooltipInfoSelector = createCachedSelector( + [getTooltipInfoAndGeometriesSelector], + ({ tooltip }): TooltipInfo => tooltip, +)(getChartIdSelector); + +/** @internal */ +export const getHighlightedGeomsSelector = createCachedSelector( + [getTooltipInfoAndGeometriesSelector], + ({ highlightedGeometries }): IndexedGeometry[] => highlightedGeometries, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/has_single_series.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/has_single_series.ts new file mode 100644 index 000000000000..49856a30e5d5 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/has_single_series.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { computeSeriesDomainsSelector } from './compute_series_domains'; + +/** @internal */ +export const hasSingleSeriesSelector = createCachedSelector( + [computeSeriesDomainsSelector], + (seriesDomainsAndData): boolean => + Boolean(seriesDomainsAndData) && seriesDomainsAndData.formattedDataSeries.length > 1, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_annotation_tooltip_visible.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_annotation_tooltip_visible.ts new file mode 100644 index 000000000000..31e21b8d9f0f --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_annotation_tooltip_visible.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getAnnotationTooltipStateSelector } from './get_annotation_tooltip_state'; + +/** @internal */ +export const isAnnotationTooltipVisibleSelector = createCachedSelector( + [getAnnotationTooltipStateSelector], + (annotationTooltipState): boolean => annotationTooltipState !== null && annotationTooltipState.isVisible, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_brush_available.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_brush_available.ts new file mode 100644 index 000000000000..710aa43ae2fb --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_brush_available.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { ScaleType } from '../../../../scales/constants'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getComputedScalesSelector } from './get_computed_scales'; + +/** + * The brush is available only for Ordinal xScales charts and + * if we have configured an onBrushEnd listener + * @internal + */ +export const isBrushAvailableSelector = createCachedSelector( + [getSettingsSpecSelector, getComputedScalesSelector], + (settingsSpec, scales): boolean => { + if (!scales.xScale) { + return false; + } + return scales.xScale.type !== ScaleType.Ordinal && Boolean(settingsSpec.onBrushEnd); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_brushing.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_brushing.ts new file mode 100644 index 000000000000..1787b85f389d --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_brushing.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { isBrushAvailableSelector } from './is_brush_available'; + +const getPointerSelector = (state: GlobalChartState) => state.interactions.pointer; + +/** @internal */ +export const isBrushingSelector = createCachedSelector( + [isBrushAvailableSelector, getPointerSelector], + (isBrushAvailable, pointer): boolean => { + if (!isBrushAvailable) { + return false; + } + + return pointer.dragging; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_chart_animatable.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_chart_animatable.ts new file mode 100644 index 000000000000..3eb989857c3d --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_chart_animatable.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { computeSeriesGeometriesSelector } from './compute_series_geometries'; +// import { isChartAnimatable } from '../utils'; + +/** @internal */ +export const isChartAnimatableSelector = createCachedSelector( + [computeSeriesGeometriesSelector, getSettingsSpecSelector], + // eslint-disable-next-line arrow-body-style + () => { + // const { geometriesCounts } = seriesGeometries; + // temporary disabled until + // https://github.com/elastic/elastic-charts/issues/89 and https://github.com/elastic/elastic-charts/issues/41 + // return isChartAnimatable(geometriesCounts, settingsSpec.animateData); + return false; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_chart_empty.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_chart_empty.ts new file mode 100644 index 000000000000..cc0c7a2cd83f --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_chart_empty.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { isAllSeriesDeselected } from '../utils/common'; +import { computeLegendSelector } from './compute_legend'; + +/** @internal */ +export const isChartEmptySelector = createCachedSelector([computeLegendSelector], (legendItems): boolean => + isAllSeriesDeselected(legendItems), +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_histogram_mode_enabled.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_histogram_mode_enabled.ts new file mode 100644 index 000000000000..aedd3f8c811c --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_histogram_mode_enabled.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { isHistogramModeEnabled } from '../utils/utils'; +import { getSeriesSpecsSelector } from './get_specs'; + +/** @internal */ +export const isHistogramModeEnabledSelector = createCachedSelector([getSeriesSpecsSelector], (seriesSpecs): boolean => + isHistogramModeEnabled(seriesSpecs), +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_tooltip_snap_enabled.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_tooltip_snap_enabled.ts new file mode 100644 index 000000000000..206b27561ee0 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_tooltip_snap_enabled.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { Scale } from '../../../../scales'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { computeSeriesGeometriesSelector } from './compute_series_geometries'; +import { getTooltipSnapSelector } from './get_tooltip_snap'; + +/** @internal */ +export const isTooltipSnapEnableSelector = createCachedSelector( + [computeSeriesGeometriesSelector, getTooltipSnapSelector], + (seriesGeometries, snap) => isTooltipSnapEnabled(seriesGeometries.scales.xScale, snap), +)(getChartIdSelector); + +function isTooltipSnapEnabled(xScale: Scale, snap: boolean) { + return (xScale && xScale.bandwidth > 0) || snap; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_tooltip_visible.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_tooltip_visible.ts new file mode 100644 index 000000000000..bdea4f0ab1aa --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/is_tooltip_visible.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { TooltipInfo } from '../../../../components/tooltip/types'; +import { getTooltipType } from '../../../../specs'; +import { TooltipType } from '../../../../specs/constants'; +import { GlobalChartState, PointerStates } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { isExternalTooltipVisibleSelector } from '../../../../state/selectors/is_external_tooltip_visible'; +import { Point } from '../../../../utils/point'; +import { getProjectedPointerPositionSelector } from './get_projected_pointer_position'; +import { getTooltipInfoSelector } from './get_tooltip_values_highlighted_geoms'; +import { isAnnotationTooltipVisibleSelector } from './is_annotation_tooltip_visible'; + +const getTooltipTypeSelector = (state: GlobalChartState): TooltipType => getTooltipType(getSettingsSpecSelector(state)); + +const getPointerSelector = (state: GlobalChartState) => state.interactions.pointer; + +/** @internal */ +export const isTooltipVisibleSelector = createCachedSelector( + [ + getTooltipTypeSelector, + getPointerSelector, + getProjectedPointerPositionSelector, + getTooltipInfoSelector, + isAnnotationTooltipVisibleSelector, + isExternalTooltipVisibleSelector, + ], + isTooltipVisible, +)(getChartIdSelector); + +function isTooltipVisible( + tooltipType: TooltipType, + pointer: PointerStates, + projectedPointerPosition: Point, + tooltip: TooltipInfo, + isAnnotationTooltipVisible: boolean, + externalTooltipVisible: boolean, +) { + const isLocalTooltop = + tooltipType !== TooltipType.None && + pointer.down === null && + projectedPointerPosition.x > -1 && + projectedPointerPosition.y > -1 && + tooltip.values.length > 0 && + !isAnnotationTooltipVisible; + const isExternalTooltip = externalTooltipVisible && tooltip.values.length > 0; + return { + visible: isLocalTooltop || isExternalTooltip, + isExternal: externalTooltipVisible, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/merge_y_custom_domains.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/merge_y_custom_domains.ts new file mode 100644 index 000000000000..865bc15b02bd --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/merge_y_custom_domains.ts @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Rotation } from '../../../../utils/common'; +import { GroupId } from '../../../../utils/ids'; +import { isCompleteBound, isLowerBound, isUpperBound, isBounded } from '../../utils/axis_type_utils'; +import { isYDomain } from '../../utils/axis_utils'; +import { AxisSpec, YDomainRange } from '../../utils/specs'; + +/** @internal */ +export function mergeYCustomDomainsByGroupId( + axesSpecs: AxisSpec[], + chartRotation: Rotation, +): Map { + const domainsByGroupId = new Map(); + + axesSpecs.forEach((spec: AxisSpec) => { + const { id, groupId, domain } = spec; + + if (!domain) { + return; + } + + const isAxisYDomain = isYDomain(spec.position, chartRotation); + + if (!isAxisYDomain) { + const errorMessage = `[Axis ${id}]: custom domain for xDomain should be defined in Settings`; + throw new Error(errorMessage); + } + + if (isCompleteBound(domain) && domain.min > domain.max) { + const errorMessage = `[Axis ${id}]: custom domain is invalid, min is greater than max`; + throw new Error(errorMessage); + } + + const prevGroupDomain = domainsByGroupId.get(groupId); + + if (prevGroupDomain) { + const prevDomain = prevGroupDomain; + const prevMin = isLowerBound(prevDomain) ? prevDomain.min : undefined; + const prevMax = isUpperBound(prevDomain) ? prevDomain.max : undefined; + + let max = prevMax; + let min = prevMin; + + if (isCompleteBound(domain)) { + min = prevMin != null ? Math.min(domain.min, prevMin) : domain.min; + max = prevMax != null ? Math.max(domain.max, prevMax) : domain.max; + } else if (isLowerBound(domain)) { + min = prevMin != null ? Math.min(domain.min, prevMin) : domain.min; + } else if (isUpperBound(domain)) { + max = prevMax != null ? Math.max(domain.max, prevMax) : domain.max; + } + + const mergedDomain = { + min, + max, + }; + + if (isBounded(mergedDomain)) { + domainsByGroupId.set(groupId, mergedDomain); + } + } else { + domainsByGroupId.set(groupId, domain); + } + }); + return domainsByGroupId; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_brush_end_caller.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_brush_end_caller.ts new file mode 100644 index 000000000000..dc7405beca9d --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_brush_end_caller.ts @@ -0,0 +1,232 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; +import { Selector } from 'reselect'; + +import { ChartType } from '../../..'; +import { Scale } from '../../../../scales'; +import { GroupBrushExtent, XYBrushArea } from '../../../../specs'; +import { BrushAxis } from '../../../../specs/constants'; +import { DragState, GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { maxValueWithUpperLimit, minValueWithLowerLimit, Rotation } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { hasDragged, DragCheckProps } from '../../../../utils/events'; +import { GroupId } from '../../../../utils/ids'; +import { isVerticalRotation } from '../utils/common'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { computeSmallMultipleScalesSelector, SmallMultipleScales } from './compute_small_multiple_scales'; +import { getPlotAreaRestrictedPoint, getPointsConstraintToSinglePanel, PanelPoints } from './get_brush_area'; +import { getComputedScalesSelector } from './get_computed_scales'; +import { isBrushAvailableSelector } from './is_brush_available'; +import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; + +const getLastDragSelector = (state: GlobalChartState) => state.interactions.pointer.lastDrag; + +/** + * Will call the onBrushEnd listener every time the following preconditions are met: + * - the onBrushEnd listener is available + * - we dragged the mouse pointer + * @internal + */ +export function createOnBrushEndCaller(): (state: GlobalChartState) => void { + let prevProps: DragCheckProps | null = null; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartType.XYAxis) { + if (!isBrushAvailableSelector(state)) { + selector = null; + prevProps = null; + return; + } + selector = createCachedSelector( + [ + getLastDragSelector, + getSettingsSpecSelector, + getComputedScalesSelector, + computeChartDimensionsSelector, + isHistogramModeEnabledSelector, + computeSmallMultipleScalesSelector, + ], + ( + lastDrag, + { + onBrushEnd, + rotation, + brushAxis, + minBrushDelta, + roundHistogramBrushValues, + allowBrushingLastHistogramBucket, + }, + computedScales, + { chartDimensions }, + histogramMode, + smallMultipleScales, + ): void => { + const nextProps = { + lastDrag, + onBrushEnd, + }; + if (lastDrag !== null && hasDragged(prevProps, nextProps) && onBrushEnd) { + const brushArea: XYBrushArea = {}; + const { yScales, xScale } = computedScales; + + if (brushAxis === BrushAxis.X || brushAxis === BrushAxis.Both) { + brushArea.x = getXBrushExtent( + chartDimensions, + lastDrag, + rotation, + histogramMode, + xScale, + smallMultipleScales, + minBrushDelta, + roundHistogramBrushValues, + allowBrushingLastHistogramBucket, + ); + } + if (brushAxis === BrushAxis.Y || brushAxis === BrushAxis.Both) { + brushArea.y = getYBrushExtents( + chartDimensions, + lastDrag, + rotation, + yScales, + smallMultipleScales, + minBrushDelta, + ); + } + if (brushArea.x !== undefined || brushArea.y !== undefined) { + onBrushEnd(brushArea); + } + } + prevProps = nextProps; + }, + )(getChartIdSelector); + } + if (selector) { + selector(state); + } + }; +} + +function scalePanelPointsToPanelCoordinates( + scaleXPoint: boolean, + { start, end, vPanelStart, hPanelStart, vPanelHeight, hPanelWidth }: PanelPoints, +) { + // scale screen coordinates down to panel scale + const startPos = scaleXPoint ? start.x - hPanelStart : start.y - vPanelStart; + const endPos = scaleXPoint ? end.x - hPanelStart : end.y - vPanelStart; + const panelMax = scaleXPoint ? hPanelWidth : vPanelHeight; + return { + minPos: Math.min(startPos, endPos), + maxPos: Math.max(startPos, endPos), + panelMax, + }; +} + +function getXBrushExtent( + chartDimensions: Dimensions, + lastDrag: DragState, + rotation: Rotation, + histogramMode: boolean, + xScale: Scale, + smallMultipleScales: SmallMultipleScales, + minBrushDelta?: number, + roundHistogramBrushValues?: boolean, + allowBrushingLastHistogramBucket?: boolean, +): [number, number] | undefined { + const isXHorizontal = !isVerticalRotation(rotation); + // scale screen coordinates down to panel scale + const scaledPanelPoints = getMinMaxPos(chartDimensions, lastDrag, smallMultipleScales, isXHorizontal); + let { minPos, maxPos } = scaledPanelPoints; + // reverse the positions if chart is mirrored + if (rotation === -90 || rotation === 180) { + minPos = scaledPanelPoints.panelMax - minPos; + maxPos = scaledPanelPoints.panelMax - maxPos; + } + if (minBrushDelta !== undefined ? Math.abs(maxPos - minPos) < minBrushDelta : maxPos === minPos) { + // if 0 size brush, avoid computing the value + return; + } + + const offset = histogramMode ? 0 : -(xScale.bandwidth + xScale.bandwidthPadding) / 2; + const invertValue = roundHistogramBrushValues + ? (value: number) => xScale.invertWithStep(value, xScale.domain)?.value + : (value: number) => xScale.invert(value); + const minPosScaled = invertValue(minPos + offset); + const maxPosScaled = invertValue(maxPos + offset); + + const maxDomainValue = xScale.domain[1] + (allowBrushingLastHistogramBucket ? xScale.minInterval : 0); + + const minValue = minValueWithLowerLimit(minPosScaled, maxPosScaled, xScale.domain[0]); + const maxValue = maxValueWithUpperLimit(minPosScaled, maxPosScaled, maxDomainValue); + + return [minValue, maxValue]; +} + +function getMinMaxPos( + chartDimensions: Dimensions, + lastDrag: DragState, + smallMultipleScales: SmallMultipleScales, + scaleXPoint: boolean, +) { + const panelPoints = getPanelPoints(chartDimensions, lastDrag, smallMultipleScales); + // scale screen coordinates down to panel scale + return scalePanelPointsToPanelCoordinates(scaleXPoint, panelPoints); +} + +function getPanelPoints(chartDimensions: Dimensions, lastDrag: DragState, smallMultipleScales: SmallMultipleScales) { + const plotStartPointPx = getPlotAreaRestrictedPoint(lastDrag.start.position, chartDimensions); + const plotEndPointPx = getPlotAreaRestrictedPoint(lastDrag.end.position, chartDimensions); + return getPointsConstraintToSinglePanel(plotStartPointPx, plotEndPointPx, smallMultipleScales); +} + +function getYBrushExtents( + chartDimensions: Dimensions, + lastDrag: DragState, + rotation: Rotation, + yScales: Map, + smallMultipleScales: SmallMultipleScales, + minBrushDelta?: number, +): GroupBrushExtent[] | undefined { + const yValues: GroupBrushExtent[] = []; + yScales.forEach((yScale, groupId) => { + const isXVertical = isVerticalRotation(rotation); + // scale screen coordinates down to panel scale + const scaledPanelPoints = getMinMaxPos(chartDimensions, lastDrag, smallMultipleScales, isXVertical); + let { minPos, maxPos } = scaledPanelPoints; + + if (rotation === 90 || rotation === 180) { + minPos = scaledPanelPoints.panelMax - minPos; + maxPos = scaledPanelPoints.panelMax - maxPos; + } + if (minBrushDelta !== undefined ? Math.abs(maxPos - minPos) < minBrushDelta : maxPos === minPos) { + // if 0 size brush, avoid computing the value + return; + } + + const minPosScaled = yScale.invert(minPos); + const maxPosScaled = yScale.invert(maxPos); + const minValue = minValueWithLowerLimit(minPosScaled, maxPosScaled, yScale.domain[0]); + const maxValue = maxValueWithUpperLimit(minPosScaled, maxPosScaled, yScale.domain[1]); + yValues.push({ extent: [minValue, maxValue], groupId }); + }); + return yValues.length === 0 ? undefined : yValues; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_click_caller.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_click_caller.ts new file mode 100644 index 000000000000..01e08ec1e068 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_click_caller.ts @@ -0,0 +1,100 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; +import { Selector } from 'reselect'; + +import { ChartType } from '../../..'; +import { ProjectedValues, SettingsSpec } from '../../../../specs'; +import { GlobalChartState, PointerState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getLastClickSelector } from '../../../../state/selectors/get_last_click'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { isClicking } from '../../../../state/utils'; +import { IndexedGeometry, GeometryValue } from '../../../../utils/geometry'; +import { XYChartSeriesIdentifier } from '../../utils/series'; +import { getProjectedScaledValues } from './get_projected_scaled_values'; +import { getHighlightedGeomsSelector } from './get_tooltip_values_highlighted_geoms'; + +/** + * Will call the onElementClick listener every time the following preconditions are met: + * - the onElementClick listener is available + * - we have at least one highlighted geometry + * - the pointer state goes from down state to up state + * @internal + */ +export function createOnClickCaller(): (state: GlobalChartState) => void { + let prevClick: PointerState | null = null; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector) { + return selector(state); + } + if (state.chartType !== ChartType.XYAxis) { + return; + } + selector = createCachedSelector( + [getLastClickSelector, getSettingsSpecSelector, getHighlightedGeomsSelector, getProjectedScaledValues], + ( + lastClick: PointerState | null, + { onElementClick, onProjectionClick }: SettingsSpec, + indexedGeometries: IndexedGeometry[], + values, + ): void => { + if (!isClicking(prevClick, lastClick)) { + return; + } + const elementClickFired = tryFiringOnElementClick(indexedGeometries, onElementClick); + if (!elementClickFired) { + tryFiringOnProjectionClick(values, onProjectionClick); + } + prevClick = lastClick; + }, + )({ + keySelector: getChartIdSelector, + }); + }; +} + +function tryFiringOnElementClick( + indexedGeometries: IndexedGeometry[], + onElementClick: SettingsSpec['onElementClick'], +): boolean { + if (indexedGeometries.length === 0 || !onElementClick) { + return false; + } + + const elements = indexedGeometries.map<[GeometryValue, XYChartSeriesIdentifier]>(({ value, seriesIdentifier }) => [ + value, + seriesIdentifier, + ]); + onElementClick(elements); + return true; +} + +function tryFiringOnProjectionClick( + values: ProjectedValues | undefined, + onProjectionClick: SettingsSpec['onProjectionClick'], +): boolean { + if (values === undefined || !onProjectionClick) { + return false; + } + onProjectionClick(values); + return true; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_element_out_caller.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_element_out_caller.ts new file mode 100644 index 000000000000..9d09d743e389 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_element_out_caller.ts @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; +import { Selector } from 'react-redux'; + +import { ChartType } from '../../..'; +import { SettingsSpec } from '../../../../specs'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { IndexedGeometry } from '../../../../utils/geometry'; +import { + getTooltipInfoAndGeometriesSelector, + TooltipAndHighlightedGeoms, +} from './get_tooltip_values_highlighted_geoms'; + +interface Props { + settings: SettingsSpec | undefined; + highlightedGeometries: IndexedGeometry[]; +} + +function isOutElement(prevProps: Props | null, nextProps: Props | null) { + if (!nextProps || !prevProps) { + return false; + } + if (!nextProps.settings || !nextProps.settings.onElementOut) { + return false; + } + if (prevProps.highlightedGeometries.length > 0 && nextProps.highlightedGeometries.length === 0) { + return true; + } + return false; +} + +/** + * Will call the onElementOut listener every time the following preconditions are met: + * - the onElementOut listener is available + * - the highlighted geometries list goes from a list of at least one object to an empty one + * @internal + */ +export function createOnElementOutCaller(): (state: GlobalChartState) => void { + let prevProps: Props | null = null; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartType.XYAxis) { + selector = createCachedSelector( + [getTooltipInfoAndGeometriesSelector, getSettingsSpecSelector], + ({ highlightedGeometries }: TooltipAndHighlightedGeoms, settings: SettingsSpec): void => { + const nextProps = { + settings, + highlightedGeometries, + }; + + if (isOutElement(prevProps, nextProps) && settings.onElementOut) { + settings.onElementOut(); + } + prevProps = nextProps; + }, + )({ + keySelector: getChartIdSelector, + }); + } + if (selector) { + selector(state); + } + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_element_over_caller.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_element_over_caller.ts new file mode 100644 index 000000000000..54f8d052e091 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_element_over_caller.ts @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; +import { Selector } from 'react-redux'; + +import { ChartType } from '../../..'; +import { SettingsSpec } from '../../../../specs'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { IndexedGeometry, GeometryValue } from '../../../../utils/geometry'; +import { XYChartSeriesIdentifier } from '../../utils/series'; +import { + getTooltipInfoAndGeometriesSelector, + TooltipAndHighlightedGeoms, +} from './get_tooltip_values_highlighted_geoms'; + +interface Props { + settings: SettingsSpec | undefined; + highlightedGeometries: IndexedGeometry[]; +} + +function isOverElement(prevProps: Props | null, nextProps: Props | null) { + if (!nextProps) { + return false; + } + if (!nextProps.settings || !nextProps.settings.onElementOver) { + return false; + } + const { highlightedGeometries: nextGeomValues } = nextProps; + const prevGeomValues = prevProps ? prevProps.highlightedGeometries : []; + if (nextGeomValues.length > 0) { + if (nextGeomValues.length !== prevGeomValues.length) { + return true; + } + return !nextGeomValues.every(({ value: next }, index) => { + const prev = prevGeomValues[index].value; + return prev && prev.x === next.x && prev.y === next.y && prev.accessor === next.accessor; + }); + } + + return false; +} + +/** + * Will call the onElementOver listener every time the following preconditions are met: + * - the onElementOver listener is available + * - we have a new set of highlighted geometries on our state + * @internal + */ +export function createOnElementOverCaller(): (state: GlobalChartState) => void { + let prevProps: Props | null = null; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartType.XYAxis) { + selector = createCachedSelector( + [getTooltipInfoAndGeometriesSelector, getSettingsSpecSelector], + ({ highlightedGeometries }: TooltipAndHighlightedGeoms, settings: SettingsSpec): void => { + const nextProps = { + settings, + highlightedGeometries, + }; + + if (isOverElement(prevProps, nextProps) && settings.onElementOver) { + const elements = highlightedGeometries.map<[GeometryValue, XYChartSeriesIdentifier]>( + ({ value, seriesIdentifier }) => [value, seriesIdentifier], + ); + settings.onElementOver(elements); + } + prevProps = nextProps; + }, + )({ + keySelector: getChartIdSelector, + }); + } + if (selector) { + selector(state); + } + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_pointer_move_caller.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_pointer_move_caller.ts new file mode 100644 index 000000000000..7bc0ce4c7e5d --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/on_pointer_move_caller.ts @@ -0,0 +1,140 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; +import { Selector } from 'reselect'; + +import { ChartType } from '../../..'; +import { Scale } from '../../../../scales'; +import { SettingsSpec, PointerEvent } from '../../../../specs'; +import { PointerEventType } from '../../../../specs/constants'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { computeSeriesGeometriesSelector } from './compute_series_geometries'; +import { getGeometriesIndexKeysSelector } from './get_geometries_index_keys'; +import { getOrientedProjectedPointerPositionSelector } from './get_oriented_projected_pointer_position'; +import { PointerPosition } from './get_projected_pointer_position'; + +const getPointerEventSelector = createCachedSelector( + [ + getChartIdSelector, + getOrientedProjectedPointerPositionSelector, + computeSeriesGeometriesSelector, + getGeometriesIndexKeysSelector, + ], + (chartId, orientedProjectedPointerPosition, seriesGeometries, geometriesIndexKeys): PointerEvent => + getPointerEvent(chartId, orientedProjectedPointerPosition, seriesGeometries.scales.xScale, geometriesIndexKeys), +)(getChartIdSelector); + +function getPointerEvent( + chartId: string, + orientedProjectedPointerPosition: PointerPosition, + xScale: Scale | undefined, + geometriesIndexKeys: any[], +): PointerEvent { + // update che cursorBandPosition based on chart configuration + if (!xScale) { + return { + chartId, + type: PointerEventType.Out, + }; + } + const { x, y } = orientedProjectedPointerPosition; + if (x === -1 || y === -1) { + return { + chartId, + type: PointerEventType.Out, + }; + } + const xValue = xScale.invertWithStep(x, geometriesIndexKeys); + if (!xValue) { + return { + chartId, + type: PointerEventType.Out, + }; + } + return { + chartId, + type: PointerEventType.Over, + unit: xScale.unit, + scale: xScale.type, + value: xValue.value, + }; +} + +function hasPointerEventChanged(prevPointerEvent: PointerEvent, nextPointerEvent: PointerEvent | null) { + if (nextPointerEvent && prevPointerEvent.type !== nextPointerEvent.type) { + return true; + } + if ( + nextPointerEvent && + prevPointerEvent.type === nextPointerEvent.type && + prevPointerEvent.type === PointerEventType.Out + ) { + return false; + } + // if something changed in the pointerEvent than recompute + if ( + nextPointerEvent && + prevPointerEvent.type === PointerEventType.Over && + nextPointerEvent.type === PointerEventType.Over && + (prevPointerEvent.value !== nextPointerEvent.value || + prevPointerEvent.scale !== nextPointerEvent.scale || + prevPointerEvent.unit !== nextPointerEvent.unit) + ) { + return true; + } + return false; +} + +/** @internal */ +export function createOnPointerMoveCaller(): (state: GlobalChartState) => void { + let prevPointerEvent: PointerEvent | null = null; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartType.XYAxis) { + selector = createCachedSelector( + [getSettingsSpecSelector, getPointerEventSelector, getChartIdSelector], + (settings: SettingsSpec, nextPointerEvent: PointerEvent, chartId: string): void => { + if (prevPointerEvent === null) { + prevPointerEvent = { + chartId, + type: PointerEventType.Out, + }; + } + const tempPrev = { + ...prevPointerEvent, + }; + // we have to update the prevPointerEvents before possibly calling the onPointerUpdate + // to avoid a recursive loop of calls caused by the impossibility to update the prevPointerEvent + prevPointerEvent = nextPointerEvent; + if (settings && settings.onPointerUpdate && hasPointerEventChanged(tempPrev, nextPointerEvent)) { + settings.onPointerUpdate(nextPointerEvent); + } + }, + )({ + keySelector: getChartIdSelector, + }); + } + if (selector) { + selector(state); + } + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/utils/__snapshots__/utils.test.ts.snap b/packages/osd-charts/src/chart_types/xy_chart/state/utils/__snapshots__/utils.test.ts.snap new file mode 100644 index 000000000000..de26c84e5a20 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/utils/__snapshots__/utils.test.ts.snap @@ -0,0 +1,786 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Chart State utils should compute and format specifications for non stacked chart 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "x": 0, + "y": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "x": 1, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "x": 2, + "y": 10, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "x": 3, + "y": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + ], + "groupId": "group1", + "insertIndex": 0, + "isFiltered": false, + "isStacked": false, + "key": "groupId{group1}spec{spec1}yAccessor{y}splitAccessors{}", + "seriesKeys": Array [ + "y", + ], + "seriesType": "line", + "spec": Object { + "chartType": "xy_axis", + "data": Array [ + Object { + "x": 0, + "y": 1, + }, + Object { + "x": 1, + "y": 2, + }, + Object { + "x": 2, + "y": 10, + }, + Object { + "x": 3, + "y": 6, + }, + ], + "groupId": "group1", + "hideInLegend": false, + "histogramModeAlignment": "center", + "id": "spec1", + "seriesType": "line", + "specType": "series", + "xAccessor": "x", + "xScaleType": "linear", + "yAccessors": Array [ + "y", + ], + "yScaleType": "log", + }, + "specId": "spec1", + "splitAccessors": Map {}, + "stackMode": undefined, + "yAccessor": "y", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "x": 0, + "y": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "x": 1, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "x": 2, + "y": 10, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "x": 3, + "y": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + ], + "groupId": "group2", + "insertIndex": 1, + "isFiltered": false, + "isStacked": false, + "key": "groupId{group2}spec{spec2}yAccessor{y}splitAccessors{}", + "seriesKeys": Array [ + "y", + ], + "seriesType": "line", + "spec": Object { + "chartType": "xy_axis", + "data": Array [ + Object { + "x": 0, + "y": 1, + }, + Object { + "x": 1, + "y": 2, + }, + Object { + "x": 2, + "y": 10, + }, + Object { + "x": 3, + "y": 6, + }, + ], + "groupId": "group2", + "hideInLegend": false, + "histogramModeAlignment": "center", + "id": "spec2", + "seriesType": "line", + "specType": "series", + "xAccessor": "x", + "xScaleType": "linear", + "yAccessors": Array [ + "y", + ], + "yScaleType": "log", + }, + "specId": "spec2", + "splitAccessors": Map {}, + "stackMode": undefined, + "yAccessor": "y", + }, +] +`; + +exports[`Chart State utils should compute and format specifications for stacked chart 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g": "a", + "x": 0, + "y": 1, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 1, + "mark": null, + "x": 0, + "y0": 0, + "y1": 1, + }, + Object { + "datum": Object { + "g": "a", + "x": 1, + "y": 2, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 1, + "y0": 0, + "y1": 2, + }, + Object { + "datum": Object { + "g": "a", + "x": 2, + "y": 3, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 3, + "mark": null, + "x": 2, + "y0": 0, + "y1": 3, + }, + Object { + "datum": Object { + "g": "a", + "x": 3, + "y": 4, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 4, + "mark": null, + "x": 3, + "y0": 0, + "y1": 4, + }, + ], + "groupId": "group2", + "insertIndex": 2, + "isFiltered": false, + "isStacked": true, + "key": "groupId{group2}spec{spec2}yAccessor{y}splitAccessors{g-a}", + "seriesKeys": Array [ + "a", + "y", + ], + "seriesType": "line", + "spec": Object { + "chartType": "xy_axis", + "data": Array [ + Object { + "g": "a", + "x": 0, + "y": 1, + }, + Object { + "g": "b", + "x": 0, + "y": 2, + }, + Object { + "g": "a", + "x": 1, + "y": 2, + }, + Object { + "g": "b", + "x": 1, + "y": 3, + }, + Object { + "g": "a", + "x": 2, + "y": 3, + }, + Object { + "g": "b", + "x": 2, + "y": 4, + }, + Object { + "g": "a", + "x": 3, + "y": 4, + }, + Object { + "g": "b", + "x": 3, + "y": 5, + }, + ], + "groupId": "group2", + "hideInLegend": false, + "histogramModeAlignment": "center", + "id": "spec2", + "seriesType": "line", + "specType": "series", + "splitSeriesAccessors": Array [ + "g", + ], + "stackAccessors": Array [ + "x", + ], + "xAccessor": "x", + "xScaleType": "linear", + "yAccessors": Array [ + "y", + ], + "yScaleType": "log", + }, + "specId": "spec2", + "splitAccessors": Map { + "g" => "a", + }, + "stackMode": undefined, + "yAccessor": "y", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g": "b", + "x": 0, + "y": 2, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 0, + "y0": 1, + "y1": 3, + }, + Object { + "datum": Object { + "g": "b", + "x": 1, + "y": 3, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 3, + "mark": null, + "x": 1, + "y0": 2, + "y1": 5, + }, + Object { + "datum": Object { + "g": "b", + "x": 2, + "y": 4, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 4, + "mark": null, + "x": 2, + "y0": 3, + "y1": 7, + }, + Object { + "datum": Object { + "g": "b", + "x": 3, + "y": 5, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 5, + "mark": null, + "x": 3, + "y0": 4, + "y1": 9, + }, + ], + "groupId": "group2", + "insertIndex": 3, + "isFiltered": false, + "isStacked": true, + "key": "groupId{group2}spec{spec2}yAccessor{y}splitAccessors{g-b}", + "seriesKeys": Array [ + "b", + "y", + ], + "seriesType": "line", + "spec": Object { + "chartType": "xy_axis", + "data": Array [ + Object { + "g": "a", + "x": 0, + "y": 1, + }, + Object { + "g": "b", + "x": 0, + "y": 2, + }, + Object { + "g": "a", + "x": 1, + "y": 2, + }, + Object { + "g": "b", + "x": 1, + "y": 3, + }, + Object { + "g": "a", + "x": 2, + "y": 3, + }, + Object { + "g": "b", + "x": 2, + "y": 4, + }, + Object { + "g": "a", + "x": 3, + "y": 4, + }, + Object { + "g": "b", + "x": 3, + "y": 5, + }, + ], + "groupId": "group2", + "hideInLegend": false, + "histogramModeAlignment": "center", + "id": "spec2", + "seriesType": "line", + "specType": "series", + "splitSeriesAccessors": Array [ + "g", + ], + "stackAccessors": Array [ + "x", + ], + "xAccessor": "x", + "xScaleType": "linear", + "yAccessors": Array [ + "y", + ], + "yScaleType": "log", + }, + "specId": "spec2", + "splitAccessors": Map { + "g" => "b", + }, + "stackMode": undefined, + "yAccessor": "y", + }, +] +`; + +exports[`Chart State utils should compute and format specifications for stacked chart 2`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g": "a", + "x": 0, + "y": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g": "a", + "x": 1, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g": "a", + "x": 2, + "y": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g": "a", + "x": 3, + "y": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 4, + }, + ], + "groupId": "group1", + "insertIndex": 0, + "isFiltered": false, + "isStacked": false, + "key": "groupId{group1}spec{spec1}yAccessor{y}splitAccessors{g-a}", + "seriesKeys": Array [ + "a", + "y", + ], + "seriesType": "line", + "spec": Object { + "chartType": "xy_axis", + "data": Array [ + Object { + "g": "a", + "x": 0, + "y": 1, + }, + Object { + "g": "b", + "x": 0, + "y": 2, + }, + Object { + "g": "a", + "x": 1, + "y": 2, + }, + Object { + "g": "b", + "x": 1, + "y": 3, + }, + Object { + "g": "a", + "x": 2, + "y": 3, + }, + Object { + "g": "b", + "x": 2, + "y": 4, + }, + Object { + "g": "a", + "x": 3, + "y": 4, + }, + Object { + "g": "b", + "x": 3, + "y": 5, + }, + ], + "groupId": "group1", + "hideInLegend": false, + "histogramModeAlignment": "center", + "id": "spec1", + "seriesType": "line", + "specType": "series", + "splitSeriesAccessors": Array [ + "g", + ], + "xAccessor": "x", + "xScaleType": "linear", + "yAccessors": Array [ + "y", + ], + "yScaleType": "log", + }, + "specId": "spec1", + "splitAccessors": Map { + "g" => "a", + }, + "stackMode": undefined, + "yAccessor": "y", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g": "b", + "x": 0, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g": "b", + "x": 1, + "y": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g": "b", + "x": 2, + "y": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g": "b", + "x": 3, + "y": 5, + }, + "initialY0": null, + "initialY1": 5, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 5, + }, + ], + "groupId": "group1", + "insertIndex": 1, + "isFiltered": false, + "isStacked": false, + "key": "groupId{group1}spec{spec1}yAccessor{y}splitAccessors{g-b}", + "seriesKeys": Array [ + "b", + "y", + ], + "seriesType": "line", + "spec": Object { + "chartType": "xy_axis", + "data": Array [ + Object { + "g": "a", + "x": 0, + "y": 1, + }, + Object { + "g": "b", + "x": 0, + "y": 2, + }, + Object { + "g": "a", + "x": 1, + "y": 2, + }, + Object { + "g": "b", + "x": 1, + "y": 3, + }, + Object { + "g": "a", + "x": 2, + "y": 3, + }, + Object { + "g": "b", + "x": 2, + "y": 4, + }, + Object { + "g": "a", + "x": 3, + "y": 4, + }, + Object { + "g": "b", + "x": 3, + "y": 5, + }, + ], + "groupId": "group1", + "hideInLegend": false, + "histogramModeAlignment": "center", + "id": "spec1", + "seriesType": "line", + "specType": "series", + "splitSeriesAccessors": Array [ + "g", + ], + "xAccessor": "x", + "xScaleType": "linear", + "yAccessors": Array [ + "y", + ], + "yScaleType": "log", + }, + "specId": "spec1", + "splitAccessors": Map { + "g" => "b", + }, + "stackMode": undefined, + "yAccessor": "y", + }, +] +`; diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/utils/common.test.ts b/packages/osd-charts/src/chart_types/xy_chart/state/utils/common.test.ts new file mode 100644 index 000000000000..0ea8470d8109 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/utils/common.test.ts @@ -0,0 +1,288 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartType } from '../../..'; +import { LegendItem } from '../../../../common/legend'; +import { ScaleType } from '../../../../scales/constants'; +import { SpecType } from '../../../../specs'; +import { BARCHART_1Y1G } from '../../../../utils/data_samples/test_dataset'; +import { Point } from '../../../../utils/point'; +import { AreaSeriesSpec, SeriesType, LineSeriesSpec, BarSeriesSpec } from '../../utils/specs'; +import { + isHorizontalRotation, + isVerticalRotation, + isLineAreaOnlyChart, + isChartAnimatable, + isAllSeriesDeselected, + sortClosestToPoint, +} from './common'; + +describe('Type Checks', () => { + it('is horizontal chart rotation', () => { + expect(isHorizontalRotation(0)).toBe(true); + expect(isHorizontalRotation(180)).toBe(true); + expect(isHorizontalRotation(-90)).toBe(false); + expect(isHorizontalRotation(90)).toBe(false); + expect(isVerticalRotation(-90)).toBe(true); + expect(isVerticalRotation(90)).toBe(true); + expect(isVerticalRotation(0)).toBe(false); + expect(isVerticalRotation(180)).toBe(false); + }); + it('is vertical chart rotation', () => { + expect(isVerticalRotation(-90)).toBe(true); + expect(isVerticalRotation(90)).toBe(true); + expect(isVerticalRotation(0)).toBe(false); + expect(isVerticalRotation(180)).toBe(false); + }); + + describe('#isLineAreaOnlyChart', () => { + it('is an area or line only map', () => { + const area: AreaSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'area', + groupId: 'group1', + seriesType: SeriesType.Area, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }; + const line: LineSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'line', + groupId: 'group2', + seriesType: SeriesType.Line, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + data: BARCHART_1Y1G, + }; + const bar: BarSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'bar', + groupId: 'group2', + seriesType: SeriesType.Bar, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + data: BARCHART_1Y1G, + }; + let series = [area, line, bar]; + expect(isLineAreaOnlyChart(series)).toBe(false); + series = [area, line]; + expect(isLineAreaOnlyChart(series)).toBe(true); + series = [area]; + expect(isLineAreaOnlyChart(series)).toBe(true); + series = [line]; + expect(isLineAreaOnlyChart(series)).toBe(true); + series = [bar, { ...bar, id: 'bar2' }]; + expect(isLineAreaOnlyChart(series)).toBe(false); + }); + }); + + describe('#isChartAnimatable', () => { + it('can enable the chart animation if we have a valid number of elements', () => { + const geometriesCounts = { + points: 0, + bars: 0, + areas: 0, + areasPoints: 0, + lines: 0, + linePoints: 0, + bubbles: 0, + bubblePoints: 0, + }; + expect(isChartAnimatable(geometriesCounts, false)).toBe(false); + expect(isChartAnimatable(geometriesCounts, true)).toBe(true); + geometriesCounts.bars = 300; + expect(isChartAnimatable(geometriesCounts, true)).toBe(true); + geometriesCounts.areasPoints = 300; + expect(isChartAnimatable(geometriesCounts, true)).toBe(true); + geometriesCounts.linePoints = 300; + expect(isChartAnimatable(geometriesCounts, true)).toBe(true); + expect(isChartAnimatable(geometriesCounts, false)).toBe(false); + geometriesCounts.linePoints = 301; + expect(isChartAnimatable(geometriesCounts, true)).toBe(false); + }); + }); + + it('displays no data available if chart is empty', () => { + const legendItems1: LegendItem[] = [ + { + color: '#1EA593', + label: 'a', + seriesIdentifiers: [ + { + key: 'specId:{bars},colors:{a}', + specId: 'bars', + }, + ], + defaultExtra: { raw: 6, formatted: '6.00', legendSizingLabel: '6.00' }, + isSeriesHidden: true, + path: [], + keys: [], + }, + { + color: '#2B70F7', + label: 'b', + seriesIdentifiers: [ + { + key: 'specId:{bars},colors:{b}', + specId: 'bars', + }, + ], + defaultExtra: { raw: 2, formatted: '2.00', legendSizingLabel: '2.00' }, + isSeriesHidden: true, + path: [], + keys: [], + }, + ]; + expect(isAllSeriesDeselected(legendItems1)).toBe(true); + }); + it('displays data availble if chart is not empty', () => { + const legendItems2: LegendItem[] = [ + { + color: '#1EA593', + label: 'a', + seriesIdentifiers: [ + { + key: 'specId:{bars},colors:{a}', + specId: 'bars', + }, + ], + defaultExtra: { raw: 6, formatted: '6.00', legendSizingLabel: '6.00' }, + isSeriesHidden: false, + path: [], + keys: [], + }, + { + color: '#2B70F7', + label: 'b', + seriesIdentifiers: [ + { + key: 'specId:{bars},colors:{b}', + specId: 'bars', + }, + ], + defaultExtra: { raw: 2, formatted: '2.00', legendSizingLabel: '2.00' }, + isSeriesHidden: true, + path: [], + keys: [], + }, + ]; + expect(isAllSeriesDeselected(legendItems2)).toBe(false); + }); + + describe('#sortClosestToPoint', () => { + describe('positive cursor', () => { + const cursor: Point = { x: 10, y: 10 }; + + it('should sort points with same x', () => { + const points: Point[] = [ + { x: 10, y: -10 }, + { x: 10, y: 12 }, + { x: 10, y: 11 }, + { x: 10, y: 10 }, + { x: 10, y: 5 }, + { x: 10, y: -12 }, + ]; + expect(points.sort(sortClosestToPoint(cursor))).toEqual([ + { x: 10, y: 10 }, + { x: 10, y: 11 }, + { x: 10, y: 12 }, + { x: 10, y: 5 }, + { x: 10, y: -10 }, + { x: 10, y: -12 }, + ]); + }); + + it('should sort points with different x', () => { + const points: Point[] = [ + { x: 9, y: -10 }, + { x: -6, y: 12 }, + { x: 3, y: 11 }, + { x: 9, y: 10 }, + { x: 1, y: 5 }, + { x: -9, y: -12 }, + ]; + expect(points.sort(sortClosestToPoint(cursor))).toEqual([ + { x: 9, y: 10 }, + { x: 3, y: 11 }, + { x: 1, y: 5 }, + { x: -6, y: 12 }, + { x: 9, y: -10 }, + { x: -9, y: -12 }, + ]); + }); + }); + + describe('negative cursor', () => { + const cursor: Point = { x: -10, y: -10 }; + + it('should sort points with same x', () => { + const points: Point[] = [ + { x: 10, y: -10 }, + { x: 10, y: 12 }, + { x: 10, y: 11 }, + { x: 10, y: 10 }, + { x: 10, y: 5 }, + { x: 10, y: -12 }, + ]; + expect(points.sort(sortClosestToPoint(cursor))).toEqual([ + { x: 10, y: -10 }, + { x: 10, y: -12 }, + { x: 10, y: 5 }, + { x: 10, y: 10 }, + { x: 10, y: 11 }, + { x: 10, y: 12 }, + ]); + }); + + it('should sort points with different x', () => { + const points: Point[] = [ + { x: 9, y: -10 }, + { x: -6, y: 12 }, + { x: 3, y: 11 }, + { x: 9, y: 10 }, + { x: 1, y: 5 }, + { x: -9, y: -12 }, + ]; + expect(points.sort(sortClosestToPoint(cursor))).toEqual([ + { x: -9, y: -12 }, + { x: 1, y: 5 }, + { x: 9, y: -10 }, + { x: -6, y: 12 }, + { x: 3, y: 11 }, + { x: 9, y: 10 }, + ]); + }); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/utils/common.ts b/packages/osd-charts/src/chart_types/xy_chart/state/utils/common.ts new file mode 100644 index 000000000000..612713e46c7d --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/utils/common.ts @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegendItem } from '../../../../common/legend'; +import { getDistance, Rotation } from '../../../../utils/common'; +import { Point } from '../../../../utils/point'; +import { BasicSeriesSpec, SeriesType } from '../../utils/specs'; +import { GeometriesCounts } from './types'; + +/** @internal */ +export const MAX_ANIMATABLE_BARS = 300; +/** @internal */ +export const MAX_ANIMATABLE_LINES_AREA_POINTS = 600; + +/** @internal */ +export function isHorizontalRotation(chartRotation: Rotation) { + return chartRotation === 0 || chartRotation === 180; +} + +/** @internal */ +export function isVerticalRotation(chartRotation: Rotation) { + return chartRotation === -90 || chartRotation === 90; +} +/** + * Check if a specs map contains only line or area specs + * @param specs Map + * @internal + */ +export function isLineAreaOnlyChart(specs: BasicSeriesSpec[]) { + return !specs.some((spec) => spec.seriesType === SeriesType.Bar); +} + +/** @internal */ +export function isChartAnimatable(geometriesCounts: GeometriesCounts, animationEnabled: boolean): boolean { + if (!animationEnabled) { + return false; + } + const { bars, linePoints, areasPoints } = geometriesCounts; + const isBarsAnimatable = bars <= MAX_ANIMATABLE_BARS; + const isLinesAndAreasAnimatable = linePoints + areasPoints <= MAX_ANIMATABLE_LINES_AREA_POINTS; + return isBarsAnimatable && isLinesAndAreasAnimatable; +} + +/** @internal */ +export function isAllSeriesDeselected(legendItems: LegendItem[]): boolean { + // eslint-disable-next-line no-restricted-syntax + for (const legendItem of legendItems) { + if (!legendItem.isSeriesHidden) { + return false; + } + } + return true; +} + +/** + * Sorts points in order from closest to farthest from cursor + * @internal + */ +export const sortClosestToPoint = (cursor: Point) => (a: Point, b: Point): number => { + return getDistance(cursor, a) - getDistance(cursor, b); +}; diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/utils/get_last_value.ts b/packages/osd-charts/src/chart_types/xy_chart/state/utils/get_last_value.ts new file mode 100644 index 000000000000..6022e852526e --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/utils/get_last_value.ts @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SeriesKey } from '../../../../common/series_id'; +import { XDomain } from '../../domains/types'; +import { isDatumFilled } from '../../rendering/utils'; +import { DataSeries, getSeriesKey, XYChartSeriesIdentifier } from '../../utils/series'; +import { StackMode } from '../../utils/specs'; +import { LastValues } from './types'; + +/** + * @internal + * @param dataSeries + * @param xDomain + */ +export function getLastValues(dataSeries: DataSeries[], xDomain: XDomain): Map { + const lastValues = new Map(); + + // we need to get the latest + dataSeries.forEach((series) => { + if (series.data.length === 0) { + return; + } + + const last = series.data[series.data.length - 1]; + if (!last) { + return; + } + if (isDatumFilled(last)) { + return; + } + + if (last.x !== xDomain.domain[xDomain.domain.length - 1]) { + // we have a dataset that is not filled with all x values + // and the last value of the series is not the last value for every series + // let's skip it + return; + } + + const { y0, y1, initialY0, initialY1 } = last; + const seriesKey = getSeriesKey(series as XYChartSeriesIdentifier, series.groupId); + + if (series.stackMode === StackMode.Percentage) { + const y1InPercentage = y1 === null || y0 === null ? null : y1 - y0; + lastValues.set(seriesKey, { y0, y1: y1InPercentage }); + return; + } + if (initialY0 !== null || initialY1 !== null) { + lastValues.set(seriesKey, { y0: initialY0, y1: initialY1 }); + } + }); + return lastValues; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/utils/spec.ts b/packages/osd-charts/src/chart_types/xy_chart/state/utils/spec.ts new file mode 100644 index 000000000000..89d49ce9bc14 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/utils/spec.ts @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BasicSeriesSpec, DEFAULT_GLOBAL_ID, Spec } from '../../../../specs'; +import { GroupId } from '../../../../utils/ids'; +import { isVerticalAxis } from '../../utils/axis_type_utils'; +import { AxisSpec } from '../../utils/specs'; + +/** @internal */ +export function getSpecsById(specs: T[], id: string): T | undefined { + return specs.find((spec) => spec.id === id); +} + +/** @internal */ +export function getAxesSpecForSpecId(axesSpecs: AxisSpec[], groupId: GroupId) { + let xAxis: AxisSpec | undefined; + let yAxis: AxisSpec | undefined; + // eslint-disable-next-line no-restricted-syntax + for (const axisSpec of axesSpecs) { + if (axisSpec.groupId !== groupId) { + continue; + } + if (isVerticalAxis(axisSpec.position)) { + yAxis = axisSpec; + } else { + xAxis = axisSpec; + } + } + + return { + xAxis, + yAxis, + }; +} + +/** @internal */ +export function getSpecDomainGroupId(spec: BasicSeriesSpec): string { + if (!spec.useDefaultGroupDomain) { + return spec.groupId; + } + return typeof spec.useDefaultGroupDomain === 'boolean' ? DEFAULT_GLOBAL_ID : spec.useDefaultGroupDomain; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/utils/types.ts b/packages/osd-charts/src/chart_types/xy_chart/state/utils/types.ts new file mode 100644 index 000000000000..2eba669041fd --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/utils/types.ts @@ -0,0 +1,90 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Scale } from '../../../../scales'; +import { OrdinalDomain } from '../../../../utils/domain'; +import { + PointGeometry, + BarGeometry, + AreaGeometry, + LineGeometry, + BubbleGeometry, + PerPanel, +} from '../../../../utils/geometry'; +import { GroupId } from '../../../../utils/ids'; +import { XDomain, YDomain } from '../../domains/types'; +import { IndexedGeometryMap } from '../../utils/indexed_geometry_map'; +import { DataSeries } from '../../utils/series'; + +/** @internal */ +export interface Transform { + x: number; + y: number; + rotate: number; +} + +/** @internal */ +export interface GeometriesCounts { + points: number; + bars: number; + areas: number; + areasPoints: number; + lines: number; + linePoints: number; + bubbles: number; + bubblePoints: number; +} + +/** @internal */ +export interface ComputedScales { + xScale: Scale; + yScales: Map; +} + +/** @internal */ +export interface Geometries { + points: PointGeometry[]; + bars: Array>; + areas: Array>; + lines: Array>; + bubbles: Array>; +} + +/** @internal */ +export interface ComputedGeometries { + scales: ComputedScales; + geometries: Geometries; + geometriesIndex: IndexedGeometryMap; + geometriesCounts: GeometriesCounts; +} + +/** @internal */ +export interface SeriesDomainsAndData { + xDomain: XDomain; + yDomains: YDomain[]; + smVDomain: OrdinalDomain; + smHDomain: OrdinalDomain; + formattedDataSeries: DataSeries[]; +} + +/** @internal */ +export interface LastValues { + y0: number | null; + y1: number | null; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/utils/utils.test.ts b/packages/osd-charts/src/chart_types/xy_chart/state/utils/utils.test.ts new file mode 100644 index 000000000000..f167579b91a4 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/utils/utils.test.ts @@ -0,0 +1,845 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// eslint-disable-next-line eslint-comments/disable-enable-pair +/* eslint-disable jest/no-conditional-expect */ + +import { MockDataSeries } from '../../../../mocks/series/series'; +import { MockSeriesSpec, MockGlobalSpec } from '../../../../mocks/specs'; +import { MockStore } from '../../../../mocks/store'; +import { SeededDataGenerator } from '../../../../mocks/utils'; +import { MockXDomain, MockYDomain } from '../../../../mocks/xy/domains'; +import { ScaleContinuous } from '../../../../scales'; +import { ScaleType } from '../../../../scales/constants'; +import { Spec } from '../../../../specs'; +import { BARCHART_1Y0G, BARCHART_1Y1G } from '../../../../utils/data_samples/test_dataset'; +import { ContinuousDomain, Range } from '../../../../utils/domain'; +import { SpecId } from '../../../../utils/ids'; +import { PointShape } from '../../../../utils/themes/theme'; +import { getSeriesIndex, XYChartSeriesIdentifier } from '../../utils/series'; +import { BasicSeriesSpec, HistogramModeAlignments, SeriesColorAccessorFn } from '../../utils/specs'; +import { computeSeriesDomainsSelector } from '../selectors/compute_series_domains'; +import { computeSeriesGeometriesSelector } from '../selectors/compute_series_geometries'; +import { getScaleConfigsFromSpecs } from '../selectors/get_api_scale_configs'; +import { + computeSeriesDomains, + computeXScaleOffset, + isHistogramModeEnabled, + setBarSeriesAccessors, + getCustomSeriesColors, +} from './utils'; + +function getGeometriesFromSpecs(specs: Spec[]) { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ + theme: { + colors: { + vizColors: ['violet', 'green', 'blue'], + defaultVizColor: 'red', + }, + }, + }); + MockStore.addSpecs([...specs, settings], store); + return computeSeriesGeometriesSelector(store.getState()); +} + +describe('Chart State utils', () => { + it('should compute and format specifications for non stacked chart', () => { + const spec1 = MockSeriesSpec.line({ + id: 'spec1', + groupId: 'group1', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + data: BARCHART_1Y0G, + }); + const spec2 = MockSeriesSpec.line({ + id: 'spec2', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + data: BARCHART_1Y0G, + }); + const scaleConfig = getScaleConfigsFromSpecs([], [spec1, spec2], MockGlobalSpec.settings()); + const domains = computeSeriesDomains([spec1, spec2], scaleConfig); + expect(domains.xDomain).toEqual( + MockXDomain.fromScaleType(ScaleType.Linear, { + domain: [0, 3], + isBandScale: false, + minInterval: 1, + }), + ); + expect(domains.yDomains).toEqual([ + MockYDomain.fromScaleType(ScaleType.Log, { + domain: [0, 10], + groupId: 'group1', + isBandScale: false, + logBase: undefined, + logMinLimit: undefined, + }), + MockYDomain.fromScaleType(ScaleType.Log, { + domain: [0, 10], + groupId: 'group2', + isBandScale: false, + logBase: undefined, + logMinLimit: undefined, + }), + ]); + expect(domains.formattedDataSeries).toMatchSnapshot(); + }); + it('should compute and format specifications for stacked chart', () => { + const spec1 = MockSeriesSpec.line({ + id: 'spec1', + groupId: 'group1', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + const spec2 = MockSeriesSpec.line({ + id: 'spec2', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + data: BARCHART_1Y1G, + }); + const scaleConfig = getScaleConfigsFromSpecs([], [spec1, spec2], MockGlobalSpec.settings()); + const domains = computeSeriesDomains([spec1, spec2], scaleConfig); + expect(domains.xDomain).toEqual( + MockXDomain.fromScaleType(ScaleType.Linear, { + domain: [0, 3], + isBandScale: false, + minInterval: 1, + }), + ); + expect(domains.yDomains).toEqual([ + MockYDomain.fromScaleType(ScaleType.Log, { + domain: [0, 5], + groupId: 'group1', + isBandScale: false, + logBase: undefined, + logMinLimit: undefined, + }), + MockYDomain.fromScaleType(ScaleType.Log, { + domain: [0, 9], + groupId: 'group2', + isBandScale: false, + logBase: undefined, + logMinLimit: undefined, + }), + ]); + expect(domains.formattedDataSeries.filter(({ isStacked }) => isStacked)).toMatchSnapshot(); + expect(domains.formattedDataSeries.filter(({ isStacked }) => !isStacked)).toMatchSnapshot(); + }); + it('should check if a SeriesCollectionValue item exists in a list of SeriesCollectionValue', () => { + const dataSeriesValuesA: XYChartSeriesIdentifier = { + specId: 'a', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: ['a', 'b', 'c'], + key: 'a', + }; + const dataSeriesValuesB: XYChartSeriesIdentifier = { + specId: 'b', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: ['a', 'b', 'c'], + key: 'b', + }; + const dataSeriesValuesC: XYChartSeriesIdentifier = { + specId: 'c', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: ['a', 'b', 'd'], + key: 'c', + }; + const deselectedSeries = [dataSeriesValuesA, dataSeriesValuesB]; + expect(getSeriesIndex(deselectedSeries, dataSeriesValuesA)).toBe(0); + expect(getSeriesIndex(deselectedSeries, dataSeriesValuesC)).toBe(-1); + expect(getSeriesIndex([], dataSeriesValuesA)).toBe(-1); + }); + + describe('getCustomSeriesColors', () => { + const specId1 = 'bar1'; + const specId2 = 'bar2'; + const dg = new SeededDataGenerator(); + const data = dg.generateGroupedSeries(50, 4); + const targetKey = 'groupId{__global__}spec{bar1}yAccessor{y}splitAccessors{g-b}'; + + describe('empty series collection and specs', () => { + it('should return an empty map', () => { + const actual = getCustomSeriesColors(MockDataSeries.empty()); + + expect(actual.size).toBe(0); + }); + }); + + describe('series collection is not empty', () => { + it('should return an empty map if no color', () => { + const barSpec1 = MockSeriesSpec.bar({ id: specId1, data, splitSeriesAccessors: ['g'] }); + const barSpec2 = MockSeriesSpec.bar({ id: specId2, data, splitSeriesAccessors: ['g'] }); + const store = MockStore.default(); + MockStore.addSpecs([barSpec1, barSpec2], store); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + const actual = getCustomSeriesColors(formattedDataSeries); + + expect(actual.size).toBe(0); + }); + + it('should return string color value', () => { + const color = 'green'; + const barSpec1 = MockSeriesSpec.bar({ id: specId1, data, color }); + const barSpec2 = MockSeriesSpec.bar({ id: specId2, data }); + const store = MockStore.default(); + MockStore.addSpecs([barSpec1, barSpec2], store); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + const actual = getCustomSeriesColors(formattedDataSeries); + + expect([...actual.values()]).toEqualArrayOf(color); + }); + + describe('with customSeriesColors array', () => { + const customSeriesColors = ['red', 'blue', 'green']; + const barSpec1 = MockSeriesSpec.bar({ + id: specId1, + data, + color: customSeriesColors, + splitSeriesAccessors: ['g'], + }); + const barSpec2 = MockSeriesSpec.bar({ id: specId2, data, splitSeriesAccessors: ['g'] }); + const store = MockStore.default(); + MockStore.addSpecs([barSpec1, barSpec2], store); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + it('should return color from color array', () => { + const actual = getCustomSeriesColors(formattedDataSeries); + + expect(actual.size).toBe(4); + formattedDataSeries.forEach(({ specId, key }) => { + const color = actual.get(key); + if (specId === specId1) { + expect(customSeriesColors).toContainEqual(color); + } else { + expect(color).toBeUndefined(); + } + }); + }); + }); + + describe('with color function', () => { + const color: SeriesColorAccessorFn = ({ yAccessor, splitAccessors }) => { + if (yAccessor === 'y' && splitAccessors.get('g') === 'b') { + return 'aquamarine'; + } + + return null; + }; + const barSpec1 = MockSeriesSpec.bar({ + id: specId1, + yAccessors: ['y'], + data, + color, + splitSeriesAccessors: ['g'], + }); + const barSpec2 = MockSeriesSpec.bar({ id: specId2, data, splitSeriesAccessors: ['g'] }); + const store = MockStore.default(); + MockStore.addSpecs([barSpec1, barSpec2], store); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + it('should return color from color function', () => { + const actual = getCustomSeriesColors(formattedDataSeries); + expect(actual.size).toBe(1); + expect(actual.get(targetKey)).toBe('aquamarine'); + }); + }); + }); + }); + + describe('Geometries counts', () => { + test('can compute stacked geometries counts', () => { + const area = MockSeriesSpec.area({ + id: 'area', + groupId: 'group1', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + const line = MockSeriesSpec.line({ + id: 'line', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + data: BARCHART_1Y1G, + }); + const bar = MockSeriesSpec.bar({ + id: 'bar', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + data: BARCHART_1Y1G, + }); + const geometries = getGeometriesFromSpecs([area, line, bar]); + + expect(geometries.geometriesCounts.bars).toBe(8); + expect(geometries.geometriesCounts.linePoints).toBe(8); + expect(geometries.geometriesCounts.areasPoints).toBe(8); + expect(geometries.geometriesCounts.lines).toBe(2); + expect(geometries.geometriesCounts.areas).toBe(2); + }); + + test('can compute non stacked geometries indexes', () => { + const line1 = MockSeriesSpec.line({ + id: 'line1', + groupId: 'group1', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Ordinal, + xAccessor: 'x', + yAccessors: ['y'], + data: BARCHART_1Y0G, + }); + const line2 = MockSeriesSpec.line({ + id: 'line2', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Ordinal, + xAccessor: 'x', + yAccessors: ['y'], + data: BARCHART_1Y0G, + }); + const geometries = getGeometriesFromSpecs([line1, line2]); + + expect(geometries.geometriesIndex.size).toBe(4); + expect(geometries.geometriesIndex.find(0)?.length).toBe(2); + expect(geometries.geometriesIndex.find(1)?.length).toBe(2); + expect(geometries.geometriesIndex.find(2)?.length).toBe(2); + expect(geometries.geometriesIndex.find(3)?.length).toBe(2); + }); + + test('can compute stacked geometries indexes', () => { + const line1 = MockSeriesSpec.line({ + id: 'line1', + groupId: 'group1', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Ordinal, + xAccessor: 'x', + yAccessors: ['y'], + stackAccessors: ['x'], + data: BARCHART_1Y0G, + }); + const line2 = MockSeriesSpec.line({ + id: 'line2', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Ordinal, + xAccessor: 'x', + yAccessors: ['y'], + stackAccessors: ['x'], + data: BARCHART_1Y0G, + }); + + const geometries = getGeometriesFromSpecs([line1, line2]); + + expect(geometries.geometriesIndex.size).toBe(4); + expect(geometries.geometriesIndex.find(0)?.length).toBe(2); + expect(geometries.geometriesIndex.find(1)?.length).toBe(2); + expect(geometries.geometriesIndex.find(2)?.length).toBe(2); + expect(geometries.geometriesIndex.find(3)?.length).toBe(2); + }); + + test('can compute non stacked geometries counts', () => { + const area = MockSeriesSpec.area({ + id: 'area', + groupId: 'group1', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + const line = MockSeriesSpec.line({ + id: 'line', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + const bar = MockSeriesSpec.bar({ + id: 'bar', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + barSeriesStyle: { + rectBorder: { + stroke: 'stroke', + strokeWidth: 123, + visible: true, + }, + rect: { + opacity: 0.2, + }, + }, + displayValueSettings: { + showValueLabel: true, + }, + }); + const geometries = getGeometriesFromSpecs([area, line, bar]); + + expect(geometries.geometriesCounts.bars).toBe(8); + expect(geometries.geometriesCounts.linePoints).toBe(8); + expect(geometries.geometriesCounts.areasPoints).toBe(8); + expect(geometries.geometriesCounts.lines).toBe(2); + expect(geometries.geometriesCounts.areas).toBe(2); + }); + test('can compute line geometries counts', () => { + const line1 = MockSeriesSpec.line({ + id: 'line1', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + const line2 = MockSeriesSpec.line({ + id: 'line2', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + const line3 = MockSeriesSpec.line({ + id: 'line3', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + + const geometries = getGeometriesFromSpecs([line1, line2, line3]); + + expect(geometries.geometriesCounts.bars).toBe(0); + expect(geometries.geometriesCounts.linePoints).toBe(24); + expect(geometries.geometriesCounts.areasPoints).toBe(0); + expect(geometries.geometriesCounts.lines).toBe(6); + expect(geometries.geometriesCounts.areas).toBe(0); + }); + test('can compute area geometries counts', () => { + const area1 = MockSeriesSpec.area({ + id: 'area1', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + const area2 = MockSeriesSpec.area({ + id: 'area2', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + const area3 = MockSeriesSpec.area({ + id: 'area3', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + + const geometries = getGeometriesFromSpecs([area1, area2, area3]); + + expect(geometries.geometriesCounts.bars).toBe(0); + expect(geometries.geometriesCounts.linePoints).toBe(0); + expect(geometries.geometriesCounts.areasPoints).toBe(24); + expect(geometries.geometriesCounts.lines).toBe(0); + expect(geometries.geometriesCounts.areas).toBe(6); + }); + test('can compute line geometries with custom style', () => { + const line1 = MockSeriesSpec.line({ + id: 'line1', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + lineSeriesStyle: { + line: { + strokeWidth: 100, + }, + point: { + fill: 'green', + }, + }, + data: BARCHART_1Y1G, + }); + const line2 = MockSeriesSpec.line({ + id: 'line2', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + const line3 = MockSeriesSpec.line({ + id: 'line3', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + + const geometries = getGeometriesFromSpecs([line1, line2, line3]); + + expect(geometries.geometries.lines[0].value.color).toBe('violet'); + expect(geometries.geometries.lines[0].value.seriesLineStyle).toEqual({ + visible: true, + strokeWidth: 100, // the override strokeWidth + opacity: 1, + }); + expect(geometries.geometries.lines[0].value.seriesPointStyle).toEqual({ + visible: true, + fill: 'green', // the override strokeWidth + opacity: 1, + radius: 2, + strokeWidth: 1, + shape: PointShape.Circle, + }); + }); + test('can compute area geometries with custom style', () => { + const area1 = MockSeriesSpec.area({ + id: 'area1', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + areaSeriesStyle: { + line: { + strokeWidth: 100, + }, + point: { + fill: 'point-fill-custom-color', + }, + area: { + fill: 'area-fill-custom-color', + opacity: 0.2, + }, + }, + }); + const area2 = MockSeriesSpec.area({ + id: 'area2', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + const area3 = MockSeriesSpec.area({ + id: 'area3', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + + const geometries = getGeometriesFromSpecs([area1, area2, area3]); + + expect(geometries.geometries.areas[0].value.color).toBe('violet'); + expect(geometries.geometries.areas[0].value.seriesAreaStyle).toEqual({ + visible: true, + fill: 'area-fill-custom-color', + opacity: 0.2, + }); + expect(geometries.geometries.areas[0].value.seriesAreaLineStyle).toEqual({ + visible: true, + strokeWidth: 100, + opacity: 1, + }); + expect(geometries.geometries.areas[0].value.seriesPointStyle).toEqual({ + visible: false, + fill: 'point-fill-custom-color', // the override strokeWidth + opacity: 1, + radius: 2, + strokeWidth: 1, + shape: PointShape.Circle, + }); + }); + test('can compute bars geometries counts', () => { + const bars1 = MockSeriesSpec.bar({ + id: 'bars1', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + const bars2 = MockSeriesSpec.bar({ + id: 'bars2', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + const bars3 = MockSeriesSpec.bar({ + id: 'bars3', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + + const geometries = getGeometriesFromSpecs([bars1, bars2, bars3]); + + expect(geometries.geometriesCounts.bars).toBe(24); + expect(geometries.geometriesCounts.linePoints).toBe(0); + expect(geometries.geometriesCounts.areasPoints).toBe(0); + expect(geometries.geometriesCounts.lines).toBe(0); + expect(geometries.geometriesCounts.areas).toBe(0); + }); + test('can compute the bar offset in mixed charts', () => { + const line1 = MockSeriesSpec.line({ + id: 'line1', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + const bar1 = MockSeriesSpec.bar({ + id: 'line3', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + + const geometries = getGeometriesFromSpecs([line1, bar1]); + + expect(geometries.geometries.bars[0].value[0].x).toBe(0); + }); + }); + + test('can compute xScaleOffset dependent on histogram mode', () => { + const domain: ContinuousDomain = [0, 10]; + const range: Range = [0, 100]; + const bandwidth = 10; + const barsPadding = 0.5; + const scale = new ScaleContinuous( + { + type: ScaleType.Linear, + domain, + range, + }, + { bandwidth, minInterval: 0, timeZone: 'utc', totalBarsInCluster: 1, barsPadding }, + ); + const histogramModeEnabled = true; + const histogramModeDisabled = false; + expect(computeXScaleOffset(scale, histogramModeDisabled)).toBe(0); + // default alignment (start) + expect(computeXScaleOffset(scale, histogramModeEnabled)).toBe(5); + expect(computeXScaleOffset(scale, histogramModeEnabled, HistogramModeAlignments.Center)).toBe(0); + expect(computeXScaleOffset(scale, histogramModeEnabled, HistogramModeAlignments.End)).toBe(-5); + }); + test('can determine if histogram mode is enabled', () => { + const area = MockSeriesSpec.area({ + id: 'area', + groupId: 'group1', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + const line = MockSeriesSpec.line({ + id: 'line', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + data: BARCHART_1Y1G, + }); + const basicBar = MockSeriesSpec.bar({ + id: 'bar', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + data: BARCHART_1Y1G, + }); + const histogramBar = MockSeriesSpec.histogramBar({ + id: 'histogram', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + let seriesMap: BasicSeriesSpec[] = [area, line, basicBar, histogramBar]; + + expect(isHistogramModeEnabled(seriesMap)).toBe(true); + + seriesMap = [area, line, basicBar]; + expect(isHistogramModeEnabled(seriesMap)).toBe(false); + + seriesMap = [area, line]; + expect(isHistogramModeEnabled(seriesMap)).toBe(false); + }); + test('can set the bar series accessors dependent on histogram mode', () => { + const isNotHistogramEnabled = false; + const isHistogramEnabled = true; + const area = MockSeriesSpec.area({ + id: 'area', + groupId: 'group1', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + data: BARCHART_1Y1G, + }); + const line = MockSeriesSpec.line({ + id: 'line', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + data: BARCHART_1Y1G, + }); + const bar = MockSeriesSpec.bar({ + id: 'bar', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + stackAccessors: ['foo'], + data: BARCHART_1Y1G, + }); + const seriesMap = new Map([ + [area.id, area], + [line.id, line], + ]); + // should not affect area or line series + setBarSeriesAccessors(isHistogramEnabled, seriesMap); + expect(seriesMap).toEqual(seriesMap); + // add bar series, histogram mode not enabled + seriesMap.set(bar.id, bar); + setBarSeriesAccessors(isNotHistogramEnabled, seriesMap); + // histogram mode + setBarSeriesAccessors(isHistogramEnabled, seriesMap); + expect(bar.stackAccessors).toEqual(['foo', 'g']); + // add another bar + const bar2 = MockSeriesSpec.bar({ + id: 'bar2', + groupId: 'group2', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['bar'], + data: BARCHART_1Y1G, + }); + seriesMap.set(bar2.id, bar2); + setBarSeriesAccessors(isHistogramEnabled, seriesMap); + expect(bar2.stackAccessors).toEqual(['y', 'bar']); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/utils/utils.ts b/packages/osd-charts/src/chart_types/xy_chart/state/utils/utils.ts new file mode 100644 index 000000000000..6adeac5fb309 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/utils/utils.ts @@ -0,0 +1,577 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getPredicateFn, Predicate } from '../../../../common/predicate'; +import { SeriesKey, SeriesIdentifier } from '../../../../common/series_id'; +import { Scale } from '../../../../scales'; +import { SortSeriesByConfig } from '../../../../specs'; +import { OrderBy } from '../../../../specs/settings'; +import { mergePartial, Rotation, Color, isUniqueArray } from '../../../../utils/common'; +import { CurveType } from '../../../../utils/curves'; +import { Dimensions, Size } from '../../../../utils/dimensions'; +import { + PointGeometry, + BarGeometry, + AreaGeometry, + LineGeometry, + BubbleGeometry, + PerPanel, +} from '../../../../utils/geometry'; +import { GroupId, SpecId } from '../../../../utils/ids'; +import { getRenderingCompareFn, SeriesCompareFn } from '../../../../utils/series_sort'; +import { ColorConfig, Theme } from '../../../../utils/themes/theme'; +import { XDomain } from '../../domains/types'; +import { mergeXDomain } from '../../domains/x_domain'; +import { isStackedSpec, mergeYDomain } from '../../domains/y_domain'; +import { renderArea } from '../../rendering/area'; +import { renderBars } from '../../rendering/bars'; +import { renderBubble } from '../../rendering/bubble'; +import { renderLine } from '../../rendering/line'; +import { defaultTickFormatter } from '../../utils/axis_utils'; +import { defaultXYSeriesSort } from '../../utils/default_series_sort_fn'; +import { fillSeries } from '../../utils/fill_series'; +import { groupBy } from '../../utils/group_data_series'; +import { IndexedGeometryMap } from '../../utils/indexed_geometry_map'; +import { computeXScale, computeYScales } from '../../utils/scales'; +import { DataSeries, getFormattedDataSeries, getDataSeriesFromSpecs, getSeriesKey } from '../../utils/series'; +import { + AxisSpec, + BasicSeriesSpec, + HistogramModeAlignment, + HistogramModeAlignments, + isAreaSeriesSpec, + isBarSeriesSpec, + isLineSeriesSpec, + isBandedSpec, + Fit, + FitConfig, + isBubbleSeriesSpec, +} from '../../utils/specs'; +import { SmallMultipleScales } from '../selectors/compute_small_multiple_scales'; +import { ScaleConfigs } from '../selectors/get_api_scale_configs'; +import { SmallMultiplesGroupBy } from '../selectors/get_specs'; +import { isHorizontalRotation } from './common'; +import { getSpecsById, getAxesSpecForSpecId, getSpecDomainGroupId } from './spec'; +import { SeriesDomainsAndData, ComputedGeometries, GeometriesCounts, Transform } from './types'; + +/** + * Return map association between `seriesKey` and only the custom colors string + * @internal + * @param dataSeries + */ +export function getCustomSeriesColors(dataSeries: DataSeries[]): Map { + const updatedCustomSeriesColors = new Map(); + const counters = new Map(); + + dataSeries.forEach((ds) => { + const { spec, specId } = ds; + const dataSeriesKey = { + specId: ds.specId, + yAccessor: ds.yAccessor, + splitAccessors: ds.splitAccessors, + smVerticalAccessorValue: undefined, + smHorizontalAccessorValue: undefined, + }; + const seriesKey = getSeriesKey(dataSeriesKey, ds.groupId); + + if (!spec || !spec.color) { + return; + } + + let color: Color | undefined | null; + + if (!color && spec.color) { + if (typeof spec.color === 'string') { + // eslint-disable-next-line prefer-destructuring + color = spec.color; + } else { + const counter = counters.get(specId) || 0; + color = Array.isArray(spec.color) ? spec.color[counter % spec.color.length] : spec.color(ds); + counters.set(specId, counter + 1); + } + } + + if (color) { + updatedCustomSeriesColors.set(seriesKey, color); + } + }); + return updatedCustomSeriesColors; +} + +/** + * Compute data domains for all specified specs. + * @internal + */ +export function computeSeriesDomains( + seriesSpecs: BasicSeriesSpec[], + scaleConfigs: ScaleConfigs, + deselectedDataSeries: SeriesIdentifier[] = [], + orderOrdinalBinsBy?: OrderBy, + smallMultiples?: SmallMultiplesGroupBy, + sortSeriesBy?: SeriesCompareFn | SortSeriesByConfig, +): SeriesDomainsAndData { + const { dataSeries, xValues, fallbackScale, smHValues, smVValues } = getDataSeriesFromSpecs( + seriesSpecs, + deselectedDataSeries, + orderOrdinalBinsBy, + smallMultiples, + ); + // compute the x domain merging any custom domain + const xDomain = mergeXDomain(scaleConfigs.x, xValues, fallbackScale); + + // fill series with missing x values + const filledDataSeries = fillSeries(dataSeries, xValues, xDomain.type); + + const seriesSortFn = getRenderingCompareFn(sortSeriesBy, (a: SeriesIdentifier, b: SeriesIdentifier) => { + return defaultXYSeriesSort(a as DataSeries, b as DataSeries); + }); + + const formattedDataSeries = getFormattedDataSeries(seriesSpecs, filledDataSeries, xValues, xDomain.type).sort( + seriesSortFn, + ); + + // let's compute the yDomains after computing all stacked values + const yDomains = mergeYDomain(formattedDataSeries, scaleConfigs.y); + + // sort small multiples values + const horizontalPredicate = smallMultiples?.horizontal?.sort ?? Predicate.DataIndex; + const smHDomain = [...smHValues].sort(getPredicateFn(horizontalPredicate)); + + const verticalPredicate = smallMultiples?.vertical?.sort ?? Predicate.DataIndex; + const smVDomain = [...smVValues].sort(getPredicateFn(verticalPredicate)); + + return { + xDomain, + yDomains, + smHDomain, + smVDomain, + formattedDataSeries, + }; +} + +/** @internal */ +export function computeSeriesGeometries( + seriesSpecs: BasicSeriesSpec[], + { xDomain, yDomains, formattedDataSeries: nonFilteredDataSeries }: SeriesDomainsAndData, + seriesColorMap: Map, + chartTheme: Theme, + chartRotation: Rotation, + axesSpecs: AxisSpec[], + smallMultiplesScales: SmallMultipleScales, + enableHistogramMode: boolean, +): ComputedGeometries { + const chartColors: ColorConfig = chartTheme.colors; + const formattedDataSeries = nonFilteredDataSeries.filter(({ isFiltered }) => !isFiltered); + const barDataSeries = formattedDataSeries.filter(({ spec }) => isBarSeriesSpec(spec)); + // compute max bar in cluster per panel + const dataSeriesGroupedByPanel = groupBy( + barDataSeries, + ['smVerticalAccessorValue', 'smHorizontalAccessorValue'], + false, + ); + + const barIndexByPanel = Object.keys(dataSeriesGroupedByPanel).reduce>((acc, panelKey) => { + const panelBars = dataSeriesGroupedByPanel[panelKey]; + const barDataSeriesByBarIndex = groupBy( + panelBars, + (d) => { + return getBarIndexKey(d, enableHistogramMode); + }, + false, + ); + + acc[panelKey] = Object.keys(barDataSeriesByBarIndex); + return acc; + }, {}); + + const { horizontal, vertical } = smallMultiplesScales; + + const yScales = computeYScales({ + yDomains, + range: [isHorizontalRotation(chartRotation) ? vertical.bandwidth : horizontal.bandwidth, 0], + }); + + const computedGeoms = renderGeometries( + formattedDataSeries, + xDomain, + yScales, + vertical, + horizontal, + barIndexByPanel, + seriesSpecs, + seriesColorMap, + chartColors.defaultVizColor, + axesSpecs, + chartTheme, + enableHistogramMode, + chartRotation, + ); + + const totalBarsInCluster = Object.values(barIndexByPanel).reduce((acc, curr) => { + return Math.max(acc, curr.length); + }, 0); + + const xScale = computeXScale({ + xDomain, + totalBarsInCluster, + range: [0, isHorizontalRotation(chartRotation) ? horizontal.bandwidth : vertical.bandwidth], + barsPadding: enableHistogramMode ? chartTheme.scales.histogramPadding : chartTheme.scales.barsPadding, + enableHistogramMode, + }); + + return { + scales: { + xScale, + yScales, + }, + ...computedGeoms, + }; +} + +/** @internal */ +export function setBarSeriesAccessors(isHistogramMode: boolean, seriesSpecs: Map): void { + if (!isHistogramMode) { + return; + } + + // eslint-disable-next-line no-restricted-syntax + for (const [, spec] of seriesSpecs) { + if (isBarSeriesSpec(spec)) { + let stackAccessors = spec.stackAccessors ? [...spec.stackAccessors] : spec.yAccessors; + + if (spec.splitSeriesAccessors) { + stackAccessors = [...stackAccessors, ...spec.splitSeriesAccessors]; + } + + spec.stackAccessors = stackAccessors; + } + } +} + +/** @internal */ +export function isHistogramModeEnabled(seriesSpecs: BasicSeriesSpec[]): boolean { + return seriesSpecs.some((spec) => isBarSeriesSpec(spec) && spec.enableHistogramMode); +} + +/** @internal */ +export function computeXScaleOffset( + xScale: Scale, + enableHistogramMode: boolean, + histogramModeAlignment: HistogramModeAlignment = HistogramModeAlignments.Start, +): number { + if (!enableHistogramMode) { + return 0; + } + + const { bandwidth, barsPadding } = xScale; + const band = bandwidth / (1 - barsPadding); + const halfPadding = (band - bandwidth) / 2; + + const startAlignmentOffset = bandwidth / 2 + halfPadding; + + switch (histogramModeAlignment) { + case HistogramModeAlignments.Center: + return 0; + case HistogramModeAlignments.End: + return -startAlignmentOffset; + default: + return startAlignmentOffset; + } +} + +function renderGeometries( + dataSeries: DataSeries[], + xDomain: XDomain, + yScales: Map, + smVScale: Scale, + smHScale: Scale, + barIndexOrderPerPanel: Record, + seriesSpecs: BasicSeriesSpec[], + seriesColorsMap: Map, + defaultColor: string, + axesSpecs: AxisSpec[], + chartTheme: Theme, + enableHistogramMode: boolean, + chartRotation: Rotation, +): Omit { + const len = dataSeries.length; + let i; + const points: PointGeometry[] = []; + const bars: Array> = []; + const areas: Array> = []; + const lines: Array> = []; + const bubbles: Array> = []; + const geometriesIndex = new IndexedGeometryMap(); + const isMixedChart = isUniqueArray(seriesSpecs, ({ seriesType }) => seriesType) && seriesSpecs.length > 1; + const fallBackTickFormatter = seriesSpecs.find(({ tickFormat }) => tickFormat)?.tickFormat ?? defaultTickFormatter; + const geometriesCounts: GeometriesCounts = { + points: 0, + bars: 0, + areas: 0, + areasPoints: 0, + lines: 0, + linePoints: 0, + bubbles: 0, + bubblePoints: 0, + }; + const barsPadding = enableHistogramMode ? chartTheme.scales.histogramPadding : chartTheme.scales.barsPadding; + + for (i = 0; i < len; i++) { + const ds = dataSeries[i]; + const spec = getSpecsById(seriesSpecs, ds.specId); + if (spec === undefined) { + continue; + } + // compute the y scale + const yScale = yScales.get(getSpecDomainGroupId(ds.spec)); + if (!yScale) { + continue; + } + // compute the panel unique key + const barPanelKey = [ds.smVerticalAccessorValue, ds.smHorizontalAccessorValue].join('|'); + const barIndexOrder = barIndexOrderPerPanel[barPanelKey]; + // compute x scale + const xScale = computeXScale({ + xDomain, + totalBarsInCluster: barIndexOrder?.length ?? 0, + range: [0, isHorizontalRotation(chartRotation) ? smHScale.bandwidth : smVScale.bandwidth], + barsPadding, + enableHistogramMode, + }); + + const { stackMode } = ds; + + const leftPos = smHScale.scale(ds.smHorizontalAccessorValue) || 0; + const topPos = smVScale.scale(ds.smVerticalAccessorValue) || 0; + const panel: Dimensions = { + width: smHScale.bandwidth, + height: smVScale.bandwidth, + top: topPos, + left: leftPos, + }; + const dataSeriesKey = getSeriesKey( + { + specId: ds.specId, + yAccessor: ds.yAccessor, + splitAccessors: ds.splitAccessors, + }, + ds.groupId, + ); + + const color = seriesColorsMap.get(dataSeriesKey) || defaultColor; + + if (isBarSeriesSpec(spec)) { + const key = getBarIndexKey(ds, enableHistogramMode); + const shift = barIndexOrder.indexOf(key); + + if (shift === -1) { + // skip bar dataSeries if index is not available + continue; + } + const barSeriesStyle = mergePartial(chartTheme.barSeriesStyle, spec.barSeriesStyle, { + mergeOptionalPartialValues: true, + }); + + const { yAxis } = getAxesSpecForSpecId(axesSpecs, spec.groupId); + const valueFormatter = yAxis?.tickFormat ?? fallBackTickFormatter; + + const displayValueSettings = spec.displayValueSettings + ? { valueFormatter, ...spec.displayValueSettings } + : undefined; + + const renderedBars = renderBars( + shift, + ds, + xScale, + yScale, + panel, + color, + barSeriesStyle, + displayValueSettings, + spec.styleAccessor, + spec.minBarHeight, + stackMode, + chartRotation, + ); + geometriesIndex.merge(renderedBars.indexedGeometryMap); + bars.push({ + panel, + value: renderedBars.barGeometries, + }); + geometriesCounts.bars += renderedBars.barGeometries.length; + } else if (isBubbleSeriesSpec(spec)) { + const bubbleShift = barIndexOrder && barIndexOrder.length > 0 ? barIndexOrder.length : 1; + const bubbleSeriesStyle = spec.bubbleSeriesStyle + ? mergePartial(chartTheme.bubbleSeriesStyle, spec.bubbleSeriesStyle, { mergeOptionalPartialValues: true }) + : chartTheme.bubbleSeriesStyle; + const xScaleOffset = computeXScaleOffset(xScale, enableHistogramMode); + const renderedBubbles = renderBubble( + (xScale.bandwidth * bubbleShift) / 2, + ds, + xScale, + yScale, + color, + panel, + isBandedSpec(spec.y0Accessors), + xScaleOffset, + bubbleSeriesStyle, + { + enabled: spec.markSizeAccessor !== undefined, + ratio: chartTheme.markSizeRatio, + }, + isMixedChart, + spec.pointStyleAccessor, + ); + geometriesIndex.merge(renderedBubbles.indexedGeometryMap); + bubbles.push({ + panel, + value: renderedBubbles.bubbleGeometry, + }); + geometriesCounts.bubblePoints += renderedBubbles.bubbleGeometry.points.length; + geometriesCounts.bubbles += 1; + } else if (isLineSeriesSpec(spec)) { + const lineShift = barIndexOrder && barIndexOrder.length > 0 ? barIndexOrder.length : 1; + const lineSeriesStyle = spec.lineSeriesStyle + ? mergePartial(chartTheme.lineSeriesStyle, spec.lineSeriesStyle, { mergeOptionalPartialValues: true }) + : chartTheme.lineSeriesStyle; + + const xScaleOffset = computeXScaleOffset(xScale, enableHistogramMode, spec.histogramModeAlignment); + + const renderedLines = renderLine( + // move the point on half of the bandwidth if we have mixed bars/lines + (xScale.bandwidth * lineShift) / 2, + ds, + xScale, + yScale, + panel, + color, + spec.curve || CurveType.LINEAR, + isBandedSpec(spec.y0Accessors), + xScaleOffset, + lineSeriesStyle, + { + enabled: spec.markSizeAccessor !== undefined && lineSeriesStyle.point.visible, + ratio: chartTheme.markSizeRatio, + }, + spec.pointStyleAccessor, + hasFitFnConfigured(spec.fit), + ); + + geometriesIndex.merge(renderedLines.indexedGeometryMap); + lines.push({ + panel, + value: renderedLines.lineGeometry, + }); + geometriesCounts.linePoints += renderedLines.lineGeometry.points.length; + geometriesCounts.lines += 1; + } else if (isAreaSeriesSpec(spec)) { + const areaShift = barIndexOrder && barIndexOrder.length > 0 ? barIndexOrder.length : 1; + const areaSeriesStyle = spec.areaSeriesStyle + ? mergePartial(chartTheme.areaSeriesStyle, spec.areaSeriesStyle, { mergeOptionalPartialValues: true }) + : chartTheme.areaSeriesStyle; + const xScaleOffset = computeXScaleOffset(xScale, enableHistogramMode, spec.histogramModeAlignment); + const renderedAreas = renderArea( + // move the point on half of the bandwidth if we have mixed bars/lines + (xScale.bandwidth * areaShift) / 2, + ds, + xScale, + yScale, + panel, + color, + spec.curve || CurveType.LINEAR, + isBandedSpec(spec.y0Accessors), + xScaleOffset, + areaSeriesStyle, + { + enabled: spec.markSizeAccessor !== undefined && areaSeriesStyle.point.visible, + ratio: chartTheme.markSizeRatio, + }, + spec.stackAccessors ? spec.stackAccessors.length > 0 : false, + spec.pointStyleAccessor, + hasFitFnConfigured(spec.fit), + ); + geometriesIndex.merge(renderedAreas.indexedGeometryMap); + areas.push({ + panel, + value: renderedAreas.areaGeometry, + }); + geometriesCounts.areasPoints += renderedAreas.areaGeometry.points.length; + geometriesCounts.areas += 1; + } + } + + return { + geometries: { + points, + bars, + areas, + lines, + bubbles, + }, + geometriesIndex, + geometriesCounts, + }; +} + +/** @internal */ +export function computeChartTransform({ width, height }: Size, chartRotation: Rotation): Transform { + if (chartRotation === 90) { + return { + x: width, + y: 0, + rotate: 90, + }; + } + if (chartRotation === -90) { + return { + x: 0, + y: height, + rotate: -90, + }; + } + if (chartRotation === 180) { + return { + x: width, + y: height, + rotate: 180, + }; + } + return { + x: 0, + y: 0, + rotate: 0, + }; +} + +function hasFitFnConfigured(fit?: Fit | FitConfig) { + return Boolean(fit && ((fit as FitConfig).type || fit) !== Fit.None); +} + +/** @internal */ +export function getBarIndexKey( + { spec, specId, groupId, yAccessor, splitAccessors }: DataSeries, + histogramModeEnabled: boolean, +) { + const isStacked = isStackedSpec(spec, histogramModeEnabled); + if (isStacked) { + return [groupId, '__stacked__'].join('__-__'); + } + + return [groupId, specId, ...splitAccessors.values(), yAccessor].join('__-__'); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/tooltip/tooltip.test.ts b/packages/osd-charts/src/chart_types/xy_chart/tooltip/tooltip.test.ts new file mode 100644 index 000000000000..718215625208 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/tooltip/tooltip.test.ts @@ -0,0 +1,383 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartType } from '../..'; +import { MockBarGeometry } from '../../../mocks'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs'; +import { ScaleType } from '../../../scales/constants'; +import { SpecType } from '../../../specs/constants'; +import { Position, RecursivePartial } from '../../../utils/common'; +import { BarGeometry } from '../../../utils/geometry'; +import { AxisStyle } from '../../../utils/themes/theme'; +import { AxisSpec, BarSeriesSpec, TickFormatter } from '../utils/specs'; +import { formatTooltip } from './tooltip'; + +const style: RecursivePartial = { + tickLine: { + size: 0, + padding: 0, + }, +}; + +describe('Tooltip formatting', () => { + const SPEC_ID_1 = 'bar_1'; + const SPEC_GROUP_ID_1 = 'bar_group_1'; + const SPEC_1 = MockSeriesSpec.bar({ + id: SPEC_ID_1, + groupId: SPEC_GROUP_ID_1, + data: [], + xAccessor: 0, + yAccessors: [1], + yScaleType: ScaleType.Linear, + xScaleType: ScaleType.Linear, + }); + const bandedSpec = MockSeriesSpec.bar({ + ...SPEC_1, + y0Accessors: [1], + }); + const YAXIS_SPEC = MockGlobalSpec.axis({ + chartType: ChartType.XYAxis, + specType: SpecType.Axis, + id: 'axis_1', + groupId: SPEC_GROUP_ID_1, + hide: false, + position: Position.Left, + showOverlappingLabels: false, + showOverlappingTicks: false, + style, + tickFormat: jest.fn((d) => `${d}`), + }); + const seriesStyle = { + rect: { + opacity: 1, + }, + rectBorder: { + strokeWidth: 1, + visible: false, + }, + displayValue: { + fill: 'black', + fontFamily: '', + fontSize: 2, + offsetX: 0, + offsetY: 0, + padding: 2, + }, + }; + const indexedGeometry = MockBarGeometry.default({ + x: 0, + y: 0, + width: 0, + height: 0, + color: 'blue', + seriesIdentifier: { + specId: SPEC_ID_1, + key: '', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: [], + }, + value: { + x: 1, + y: 10, + accessor: 'y1', + mark: null, + datum: { x: 1, y: 10 }, + }, + seriesStyle, + }); + const indexedBandedGeometry = MockBarGeometry.default({ + x: 0, + y: 0, + width: 0, + height: 0, + color: 'blue', + seriesIdentifier: { + specId: SPEC_ID_1, + key: '', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: [], + }, + value: { + x: 1, + y: 10, + accessor: 'y1', + mark: null, + datum: { x: 1, y: 10 }, + }, + seriesStyle, + }); + + test('format simple tooltip', () => { + const tooltipValue = formatTooltip(indexedGeometry, SPEC_1, false, false, false, YAXIS_SPEC); + expect(tooltipValue).toBeDefined(); + expect(tooltipValue.valueAccessor).toBe('y1'); + expect(tooltipValue.label).toBe('bar_1'); + expect(tooltipValue.isHighlighted).toBe(false); + expect(tooltipValue.color).toBe('blue'); + expect(tooltipValue.value).toBe(10); + expect(tooltipValue.formattedValue).toBe('10'); + expect(tooltipValue.formattedValue).toBe('10'); + expect(YAXIS_SPEC.tickFormat).not.toBeCalledWith(null); + }); + it('should set name as spec name when provided', () => { + const name = 'test - spec'; + const tooltipValue = formatTooltip(indexedBandedGeometry, { ...SPEC_1, name }, false, false, false, YAXIS_SPEC); + expect(tooltipValue.label).toBe(name); + }); + it('should set name as spec id when name is not provided', () => { + const tooltipValue = formatTooltip(indexedBandedGeometry, SPEC_1, false, false, false, YAXIS_SPEC); + expect(tooltipValue.label).toBe(SPEC_1.id); + }); + test('format banded tooltip - upper', () => { + const tooltipValue = formatTooltip(indexedBandedGeometry, bandedSpec, false, false, false, YAXIS_SPEC); + expect(tooltipValue.label).toBe('bar_1 - upper'); + }); + test('format banded tooltip - y1AccessorFormat', () => { + const tooltipValue = formatTooltip( + indexedBandedGeometry, + { ...bandedSpec, y1AccessorFormat: ' [max]' }, + false, + false, + false, + YAXIS_SPEC, + ); + expect(tooltipValue.label).toBe('bar_1 [max]'); + }); + test('format banded tooltip - y1AccessorFormat as function', () => { + const tooltipValue = formatTooltip( + indexedBandedGeometry, + { ...bandedSpec, y1AccessorFormat: (label) => `[max] ${label}` }, + false, + false, + false, + YAXIS_SPEC, + ); + expect(tooltipValue.label).toBe('[max] bar_1'); + }); + test('format banded tooltip - lower', () => { + const tooltipValue = formatTooltip( + { + ...indexedBandedGeometry, + value: { + ...indexedBandedGeometry.value, + accessor: 'y0', + }, + }, + bandedSpec, + false, + false, + false, + YAXIS_SPEC, + ); + expect(tooltipValue.label).toBe('bar_1 - lower'); + }); + test('format banded tooltip - y0AccessorFormat', () => { + const tooltipValue = formatTooltip( + { + ...indexedBandedGeometry, + value: { + ...indexedBandedGeometry.value, + accessor: 'y0', + }, + }, + { ...bandedSpec, y0AccessorFormat: ' [min]' }, + false, + false, + false, + YAXIS_SPEC, + ); + expect(tooltipValue.label).toBe('bar_1 [min]'); + }); + test('format banded tooltip - y0AccessorFormat as function', () => { + const tooltipValue = formatTooltip( + { + ...indexedBandedGeometry, + value: { + ...indexedBandedGeometry.value, + accessor: 'y0', + }, + }, + { ...bandedSpec, y0AccessorFormat: (label) => `[min] ${label}` }, + false, + false, + false, + YAXIS_SPEC, + ); + expect(tooltipValue.label).toBe('[min] bar_1'); + }); + test('format tooltip with seriesKeys name', () => { + const geometry: BarGeometry = { + ...indexedGeometry, + seriesIdentifier: { + specId: SPEC_ID_1, + key: '', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: ['y1'], + }, + }; + const tooltipValue = formatTooltip(geometry, SPEC_1, false, false, false, YAXIS_SPEC); + expect(tooltipValue).toBeDefined(); + expect(tooltipValue.valueAccessor).toBe('y1'); + expect(tooltipValue.label).toBe('bar_1'); + expect(tooltipValue.isHighlighted).toBe(false); + expect(tooltipValue.color).toBe('blue'); + expect(tooltipValue.value).toBe(10); + expect(tooltipValue.formattedValue).toBe('10'); + }); + test('format y0 tooltip', () => { + const geometry: BarGeometry = { + ...indexedGeometry, + value: { + ...indexedGeometry.value, + accessor: 'y0', + }, + }; + const tooltipValue = formatTooltip(geometry, SPEC_1, false, false, false, YAXIS_SPEC); + expect(tooltipValue).toBeDefined(); + expect(tooltipValue.valueAccessor).toBe('y0'); + expect(tooltipValue.label).toBe('bar_1'); + expect(tooltipValue.isHighlighted).toBe(false); + expect(tooltipValue.color).toBe('blue'); + expect(tooltipValue.value).toBe(10); + expect(tooltipValue.formattedValue).toBe('10'); + }); + test('format x tooltip', () => { + const geometry: BarGeometry = { + ...indexedGeometry, + value: { + ...indexedGeometry.value, + accessor: 'y0', + }, + }; + let tooltipValue = formatTooltip(geometry, SPEC_1, true, false, false, YAXIS_SPEC); + expect(tooltipValue).toBeDefined(); + expect(tooltipValue.valueAccessor).toBe('y0'); + expect(tooltipValue.label).toBe('bar_1'); + expect(tooltipValue.isHighlighted).toBe(false); + expect(tooltipValue.color).toBe('blue'); + expect(tooltipValue.value).toBe(1); + expect(tooltipValue.formattedValue).toBe('1'); + // disable any highlight on x value + tooltipValue = formatTooltip(geometry, SPEC_1, true, true, false, YAXIS_SPEC); + expect(tooltipValue.isHighlighted).toBe(false); + }); + + it('should format ticks with custom formatter from spec', () => { + const axisTickFormatter: TickFormatter = (v) => `${v} axis`; + const tickFormatter: TickFormatter = (v) => `${v} spec`; + const axisSpec: AxisSpec = { + ...YAXIS_SPEC, + tickFormat: axisTickFormatter, + }; + const spec: BarSeriesSpec = { + ...SPEC_1, + tickFormat: tickFormatter, + }; + const tooltipValue = formatTooltip(indexedGeometry, spec, false, false, false, axisSpec); + expect(tooltipValue.value).toBe(10); + expect(tooltipValue.formattedValue).toBe('10 spec'); + }); + + it('should format ticks with custom formatter from axis', () => { + const axisTickFormatter: TickFormatter = (v) => `${v} axis`; + const axisSpec: AxisSpec = { + ...YAXIS_SPEC, + tickFormat: axisTickFormatter, + }; + const tooltipValue = formatTooltip(indexedGeometry, SPEC_1, false, false, false, axisSpec); + expect(tooltipValue.value).toBe(10); + expect(tooltipValue.formattedValue).toBe('10 axis'); + }); + + it('should format ticks with default formatter', () => { + const tooltipValue = formatTooltip(indexedGeometry, SPEC_1, false, false, false, YAXIS_SPEC); + expect(tooltipValue.value).toBe(10); + expect(tooltipValue.formattedValue).toBe('10'); + }); + + it('should format header with custom formatter from axis', () => { + const axisTickFormatter: TickFormatter = (v) => `${v} axis`; + const tickFormatter: TickFormatter = (v) => `${v} spec`; + const axisSpec: AxisSpec = { + ...YAXIS_SPEC, + tickFormat: axisTickFormatter, + }; + const spec: BarSeriesSpec = { + ...SPEC_1, + tickFormat: tickFormatter, + }; + const tooltipValue = formatTooltip(indexedGeometry, spec, true, false, false, axisSpec); + expect(tooltipValue.value).toBe(1); + expect(tooltipValue.formattedValue).toBe('1 axis'); + }); + + it('should format header with default formatter from axis', () => { + const tickFormatter: TickFormatter = (v) => `${v} spec`; + const spec: BarSeriesSpec = { + ...SPEC_1, + tickFormat: tickFormatter, + }; + const tooltipValue = formatTooltip(indexedGeometry, spec, true, false, false, YAXIS_SPEC); + expect(tooltipValue.value).toBe(1); + expect(tooltipValue.formattedValue).toBe('1'); + }); + + describe('markFormat', () => { + const markFormat = jest.fn((d) => `${d} number`); + const markIndexedGeometry: BarGeometry = { + ...indexedGeometry, + value: { + x: 1, + y: 10, + accessor: 'y1', + mark: 10, + datum: { x: 1, y: 10 }, + }, + }; + + it('should format mark value with markFormat', () => { + const tooltipValue = formatTooltip( + markIndexedGeometry, + { + ...SPEC_1, + markFormat, + }, + false, + false, + false, + YAXIS_SPEC, + ); + expect(tooltipValue).toBeDefined(); + expect(tooltipValue.markValue).toBe(10); + expect(tooltipValue.formattedMarkValue).toBe('10 number'); + expect(markFormat).toBeCalledWith(10, undefined); + }); + + it('should format mark value with defaultTickFormatter', () => { + const tooltipValue = formatTooltip(markIndexedGeometry, SPEC_1, false, false, false, YAXIS_SPEC); + expect(tooltipValue).toBeDefined(); + expect(tooltipValue.markValue).toBe(10); + expect(tooltipValue.formattedMarkValue).toBe('10'); + expect(markFormat).not.toBeCalled(); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/tooltip/tooltip.ts b/packages/osd-charts/src/chart_types/xy_chart/tooltip/tooltip.ts new file mode 100644 index 000000000000..ea3a8cb83d34 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/tooltip/tooltip.ts @@ -0,0 +1,111 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegendItemExtraValues } from '../../../common/legend'; +import { SeriesKey } from '../../../common/series_id'; +import { TooltipValue } from '../../../specs'; +import { getAccessorFormatLabel } from '../../../utils/accessor'; +import { isDefined } from '../../../utils/common'; +import { IndexedGeometry, BandedAccessorType } from '../../../utils/geometry'; +import { defaultTickFormatter } from '../utils/axis_utils'; +import { getSeriesName } from '../utils/series'; +import { + AxisSpec, + BasicSeriesSpec, + isBandedSpec, + isAreaSeriesSpec, + isBarSeriesSpec, + TickFormatterOptions, +} from '../utils/specs'; + +/** @internal */ +export const Y0_ACCESSOR_POSTFIX = ' - lower'; +/** @internal */ +export const Y1_ACCESSOR_POSTFIX = ' - upper'; + +/** @internal */ +export function getHighligthedValues( + tooltipValues: TooltipValue[], + defaultValue?: string, +): Map { + const seriesTooltipValues = new Map(); + + tooltipValues.forEach(({ formattedValue, seriesIdentifier, valueAccessor }) => { + const seriesValue = defaultValue || formattedValue; + const current: LegendItemExtraValues = seriesTooltipValues.get(seriesIdentifier.key) ?? new Map(); + if (defaultValue) { + if (!current.has(BandedAccessorType.Y0)) { + current.set(BandedAccessorType.Y0, defaultValue); + } + if (!current.has(BandedAccessorType.Y1)) { + current.set(BandedAccessorType.Y1, defaultValue); + } + } + + if (valueAccessor != null && (valueAccessor === BandedAccessorType.Y0 || valueAccessor === BandedAccessorType.Y1)) { + current.set(valueAccessor, seriesValue); + } + seriesTooltipValues.set(seriesIdentifier.key, current); + }); + return seriesTooltipValues; +} + +/** @internal */ +export function formatTooltip( + { color, value: { x, y, mark, accessor, datum }, seriesIdentifier }: IndexedGeometry, + spec: BasicSeriesSpec, + isHeader: boolean, + isHighlighted: boolean, + hasSingleSeries: boolean, + axisSpec?: AxisSpec, +): TooltipValue { + let label = getSeriesName(seriesIdentifier, hasSingleSeries, true, spec); + + if (isBandedSpec(spec.y0Accessors) && (isAreaSeriesSpec(spec) || isBarSeriesSpec(spec))) { + const { y0AccessorFormat = Y0_ACCESSOR_POSTFIX, y1AccessorFormat = Y1_ACCESSOR_POSTFIX } = spec; + const formatter = accessor === BandedAccessorType.Y0 ? y0AccessorFormat : y1AccessorFormat; + label = getAccessorFormatLabel(formatter, label); + } + const isFiltered = spec.filterSeriesInTooltip !== undefined ? spec.filterSeriesInTooltip(seriesIdentifier) : true; + const isVisible = label === '' ? false : isFiltered; + + const value = isHeader ? x : y; + const markValue = isHeader || mark === null ? null : mark; + const tickFormatOptions: TickFormatterOptions | undefined = spec.timeZone ? { timeZone: spec.timeZone } : undefined; + const tickFormatter = + (isHeader ? axisSpec?.tickFormat : spec.tickFormat ?? axisSpec?.tickFormat) ?? defaultTickFormatter; + + return { + seriesIdentifier, + valueAccessor: accessor, + label, + value, + formattedValue: tickFormatter(value, tickFormatOptions), + markValue, + ...(isDefined(markValue) && { + formattedMarkValue: spec.markFormat + ? spec.markFormat(markValue, tickFormatOptions) + : defaultTickFormatter(markValue), + }), + color, + isHighlighted: isHeader ? false : isHighlighted, + isVisible, + datum, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/__snapshots__/dimensions.test.ts.snap b/packages/osd-charts/src/chart_types/xy_chart/utils/__snapshots__/dimensions.test.ts.snap new file mode 100644 index 000000000000..83ceb016c163 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/__snapshots__/dimensions.test.ts.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Computed chart dimensions should be equal to parent dimension with no axis minus margins 1`] = ` +Object { + "height": 60, + "left": 20, + "top": 20, + "width": 60, +} +`; + +exports[`Computed chart dimensions should be padded by a bottom axis 1`] = ` +Object { + "height": 30, + "left": 25, + "top": 20, + "width": 50, +} +`; + +exports[`Computed chart dimensions should be padded by a left axis 1`] = ` +Object { + "height": 50, + "left": 50, + "top": 25, + "width": 30, +} +`; + +exports[`Computed chart dimensions should be padded by a right axis 1`] = ` +Object { + "height": 50, + "left": 20, + "top": 25, + "width": 30, +} +`; + +exports[`Computed chart dimensions should be padded by a top axis 1`] = ` +Object { + "height": 30, + "left": 25, + "top": 50, + "width": 50, +} +`; diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/__snapshots__/series.test.ts.snap b/packages/osd-charts/src/chart_types/xy_chart/utils/__snapshots__/series.test.ts.snap new file mode 100644 index 000000000000..80a56fac203e --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/__snapshots__/series.test.ts.snap @@ -0,0 +1,28877 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Series #getSeriesNameKeys Shall ignore undefined values on splitSeriesAccessors 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Array [ + 0, + 1, + "a", + ], + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Array [ + 1, + 1, + "a", + ], + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 1, + }, + Object { + "datum": Array [ + 2, + 1, + "a", + ], + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 1, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{1}splitAccessors{2-a}", + "seriesKeys": Array [ + "a", + 1, + ], + "specId": "spec1", + "splitAccessors": Map { + 2 => "a", + }, + "yAccessor": 1, + }, + Object { + "data": Array [ + Object { + "datum": Array [ + 0, + 1, + "b", + ], + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Array [ + 1, + 1, + "b", + ], + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 1, + }, + Object { + "datum": Array [ + 2, + 1, + "b", + ], + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 1, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{1}splitAccessors{2-b}", + "seriesKeys": Array [ + "b", + 1, + ], + "specId": "spec1", + "splitAccessors": Map { + 2 => "b", + }, + "yAccessor": 1, + }, +] +`; + +exports[`Series Can split dataset into 1Y0G series 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "x": 0, + "y": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "x": 1, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "x": 2, + "y": 10, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "x": 3, + "y": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y}splitAccessors{}", + "seriesKeys": Array [ + "y", + ], + "specId": "spec1", + "splitAccessors": Map {}, + "yAccessor": "y", + }, +] +`; + +exports[`Series Can split dataset into 1Y1G series 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g": "a", + "x": 0, + "y": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g": "a", + "x": 1, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g": "a", + "x": 2, + "y": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g": "a", + "x": 3, + "y": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 4, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y}splitAccessors{g-a}", + "seriesKeys": Array [ + "a", + "y", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "a", + }, + "yAccessor": "y", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g": "b", + "x": 0, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g": "b", + "x": 1, + "y": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g": "b", + "x": 2, + "y": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g": "b", + "x": 3, + "y": 5, + }, + "initialY0": null, + "initialY1": 5, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 5, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y}splitAccessors{g-b}", + "seriesKeys": Array [ + "b", + "y", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "b", + }, + "yAccessor": "y", + }, +] +`; + +exports[`Series Can split dataset into 1Y2G series 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "a", + "g2": "s", + "x": 0, + "y": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "a", + "g2": "s", + "x": 1, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "a", + "g2": "s", + "x": 2, + "y": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "a", + "g2": "s", + "x": 3, + "y": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y}splitAccessors{g1-a|g2-s}", + "seriesKeys": Array [ + "a", + "s", + "y", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "a", + "g2" => "s", + }, + "yAccessor": "y", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "a", + "g2": "p", + "x": 0, + "y": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "a", + "g2": "p", + "x": 1, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "a", + "g2": "p", + "x": 2, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "a", + "g2": "p", + "x": 3, + "y": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y}splitAccessors{g1-a|g2-p}", + "seriesKeys": Array [ + "a", + "p", + "y", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "a", + "g2" => "p", + }, + "yAccessor": "y", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "b", + "g2": "s", + "x": 0, + "y": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "b", + "g2": "s", + "x": 1, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "b", + "g2": "s", + "x": 2, + "y": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "b", + "g2": "s", + "x": 3, + "y": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y}splitAccessors{g1-b|g2-s}", + "seriesKeys": Array [ + "b", + "s", + "y", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "b", + "g2" => "s", + }, + "yAccessor": "y", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "b", + "g2": "p", + "x": 0, + "y": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "b", + "g2": "p", + "x": 1, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "b", + "g2": "p", + "x": 2, + "y": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g1": "b", + "g2": "p", + "x": 3, + "y": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y}splitAccessors{g1-b|g2-p}", + "seriesKeys": Array [ + "b", + "p", + "y", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "b", + "g2" => "p", + }, + "yAccessor": "y", + }, +] +`; + +exports[`Series Can split dataset into 2Y0G series 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "x": 0, + "y1": 1, + "y2": 3, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "x": 1, + "y1": 2, + "y2": 7, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "x": 2, + "y1": 1, + "y2": 2, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "x": 3, + "y1": 6, + "y2": 10, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{}", + "seriesKeys": Array [ + "y1", + ], + "specId": "spec1", + "splitAccessors": Map {}, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "x": 0, + "y1": 1, + "y2": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "x": 1, + "y1": 2, + "y2": 7, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "x": 2, + "y1": 1, + "y2": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "x": 3, + "y1": 6, + "y2": 10, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 10, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{}", + "seriesKeys": Array [ + "y2", + ], + "specId": "spec1", + "splitAccessors": Map {}, + "yAccessor": "y2", + }, +] +`; + +exports[`Series Can split dataset into 2Y1G series 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g": "a", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g": "a", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g": "a", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g": "a", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 7, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-a}", + "seriesKeys": Array [ + "a", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "a", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g": "a", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g": "a", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g": "a", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 5, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 5, + }, + Object { + "datum": Object { + "g": "a", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 3, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g-a}", + "seriesKeys": Array [ + "a", + "y2", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "a", + }, + "yAccessor": "y2", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g": "b", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g": "b", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g": "b", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g": "b", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-b}", + "seriesKeys": Array [ + "b", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "b", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g": "b", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g": "b", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 5, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 5, + }, + Object { + "datum": Object { + "g": "b", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g": "b", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 4, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g-b}", + "seriesKeys": Array [ + "b", + "y2", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "b", + }, + "yAccessor": "y2", + }, +] +`; + +exports[`Series Can split dataset into 2Y2G series 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 7, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g1-cdn.google.com|g2-direct-cdn}", + "seriesKeys": Array [ + "cdn.google.com", + "direct-cdn", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cdn.google.com", + "g2" => "direct-cdn", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 5, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 5, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 3, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g1-cdn.google.com|g2-direct-cdn}", + "seriesKeys": Array [ + "cdn.google.com", + "direct-cdn", + "y2", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cdn.google.com", + "g2" => "direct-cdn", + }, + "yAccessor": "y2", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 7, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g1-cdn.google.com|g2-indirect-cdn}", + "seriesKeys": Array [ + "cdn.google.com", + "indirect-cdn", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cdn.google.com", + "g2" => "indirect-cdn", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 5, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 5, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 3, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g1-cdn.google.com|g2-indirect-cdn}", + "seriesKeys": Array [ + "cdn.google.com", + "indirect-cdn", + "y2", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cdn.google.com", + "g2" => "indirect-cdn", + }, + "yAccessor": "y2", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g1-cloudflare.com|g2-direct-cdn}", + "seriesKeys": Array [ + "cloudflare.com", + "direct-cdn", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cloudflare.com", + "g2" => "direct-cdn", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 5, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 5, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 4, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g1-cloudflare.com|g2-direct-cdn}", + "seriesKeys": Array [ + "cloudflare.com", + "direct-cdn", + "y2", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cloudflare.com", + "g2" => "direct-cdn", + }, + "yAccessor": "y2", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g1-cloudflare.com|g2-indirect-cdn}", + "seriesKeys": Array [ + "cloudflare.com", + "indirect-cdn", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cloudflare.com", + "g2" => "indirect-cdn", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 5, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 5, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 4, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g1-cloudflare.com|g2-indirect-cdn}", + "seriesKeys": Array [ + "cloudflare.com", + "indirect-cdn", + "y2", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cloudflare.com", + "g2" => "indirect-cdn", + }, + "yAccessor": "y2", + }, +] +`; + +exports[`Series Can stack high volume of dataseries 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 0, + "mark": null, + "x": 0, + "y0": 0, + "y1": 0, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 1, + "mark": null, + "x": 1, + "y0": 0, + "y1": 1, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 2, + "y0": 0, + "y1": 2, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 3, + "mark": null, + "x": 3, + "y0": 0, + "y1": 3, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 4, + "mark": null, + "x": 4, + "y0": 0, + "y1": 4, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 5, + "mark": null, + "x": 5, + "y0": 0, + "y1": 5, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 6, + "mark": null, + "x": 6, + "y0": 0, + "y1": 6, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 7, + "mark": null, + "x": 7, + "y0": 0, + "y1": 7, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 8, + "mark": null, + "x": 8, + "y0": 0, + "y1": 8, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 9, + "mark": null, + "x": 9, + "y0": 0, + "y1": 9, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 10, + "mark": null, + "x": 10, + "y0": 0, + "y1": 10, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 11, + "mark": null, + "x": 11, + "y0": 0, + "y1": 11, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 12, + "mark": null, + "x": 12, + "y0": 0, + "y1": 12, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 13, + "mark": null, + "x": 13, + "y0": 0, + "y1": 13, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 14, + "mark": null, + "x": 14, + "y0": 0, + "y1": 14, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 15, + "mark": null, + "x": 15, + "y0": 0, + "y1": 15, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 16, + "mark": null, + "x": 16, + "y0": 0, + "y1": 16, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 17, + "mark": null, + "x": 17, + "y0": 0, + "y1": 17, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 18, + "mark": null, + "x": 18, + "y0": 0, + "y1": 18, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 19, + "mark": null, + "x": 19, + "y0": 0, + "y1": 19, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 20, + "mark": null, + "x": 20, + "y0": 0, + "y1": 20, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 21, + "mark": null, + "x": 21, + "y0": 0, + "y1": 21, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 22, + "mark": null, + "x": 22, + "y0": 0, + "y1": 22, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 23, + "mark": null, + "x": 23, + "y0": 0, + "y1": 23, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 24, + "mark": null, + "x": 24, + "y0": 0, + "y1": 24, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 25, + "mark": null, + "x": 25, + "y0": 0, + "y1": 25, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 26, + "mark": null, + "x": 26, + "y0": 0, + "y1": 26, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 27, + "mark": null, + "x": 27, + "y0": 0, + "y1": 27, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 28, + "mark": null, + "x": 28, + "y0": 0, + "y1": 28, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 29, + "mark": null, + "x": 29, + "y0": 0, + "y1": 29, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 30, + "mark": null, + "x": 30, + "y0": 0, + "y1": 30, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 31, + "mark": null, + "x": 31, + "y0": 0, + "y1": 31, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 32, + "mark": null, + "x": 32, + "y0": 0, + "y1": 32, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 33, + "mark": null, + "x": 33, + "y0": 0, + "y1": 33, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 34, + "mark": null, + "x": 34, + "y0": 0, + "y1": 34, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 35, + "mark": null, + "x": 35, + "y0": 0, + "y1": 35, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 36, + "mark": null, + "x": 36, + "y0": 0, + "y1": 36, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 37, + "mark": null, + "x": 37, + "y0": 0, + "y1": 37, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 38, + "mark": null, + "x": 38, + "y0": 0, + "y1": 38, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 39, + "mark": null, + "x": 39, + "y0": 0, + "y1": 39, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 40, + "mark": null, + "x": 40, + "y0": 0, + "y1": 40, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 41, + "mark": null, + "x": 41, + "y0": 0, + "y1": 41, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 42, + "mark": null, + "x": 42, + "y0": 0, + "y1": 42, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 43, + "mark": null, + "x": 43, + "y0": 0, + "y1": 43, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 44, + "mark": null, + "x": 44, + "y0": 0, + "y1": 44, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 45, + "mark": null, + "x": 45, + "y0": 0, + "y1": 45, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 46, + "mark": null, + "x": 46, + "y0": 0, + "y1": 46, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 47, + "mark": null, + "x": 47, + "y0": 0, + "y1": 47, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 48, + "mark": null, + "x": 48, + "y0": 0, + "y1": 48, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 49, + "mark": null, + "x": 49, + "y0": 0, + "y1": 49, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 50, + "mark": null, + "x": 50, + "y0": 0, + "y1": 50, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 51, + "mark": null, + "x": 51, + "y0": 0, + "y1": 51, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 52, + "mark": null, + "x": 52, + "y0": 0, + "y1": 52, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 53, + "mark": null, + "x": 53, + "y0": 0, + "y1": 53, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 54, + "mark": null, + "x": 54, + "y0": 0, + "y1": 54, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 55, + "mark": null, + "x": 55, + "y0": 0, + "y1": 55, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 56, + "mark": null, + "x": 56, + "y0": 0, + "y1": 56, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 57, + "mark": null, + "x": 57, + "y0": 0, + "y1": 57, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 58, + "mark": null, + "x": 58, + "y0": 0, + "y1": 58, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 59, + "mark": null, + "x": 59, + "y0": 0, + "y1": 59, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 60, + "mark": null, + "x": 60, + "y0": 0, + "y1": 60, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 61, + "mark": null, + "x": 61, + "y0": 0, + "y1": 61, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 62, + "mark": null, + "x": 62, + "y0": 0, + "y1": 62, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 63, + "mark": null, + "x": 63, + "y0": 0, + "y1": 63, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 64, + "mark": null, + "x": 64, + "y0": 0, + "y1": 64, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 65, + "mark": null, + "x": 65, + "y0": 0, + "y1": 65, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 66, + "mark": null, + "x": 66, + "y0": 0, + "y1": 66, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 67, + "mark": null, + "x": 67, + "y0": 0, + "y1": 67, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 68, + "mark": null, + "x": 68, + "y0": 0, + "y1": 68, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 69, + "mark": null, + "x": 69, + "y0": 0, + "y1": 69, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 70, + "mark": null, + "x": 70, + "y0": 0, + "y1": 70, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 71, + "mark": null, + "x": 71, + "y0": 0, + "y1": 71, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 72, + "mark": null, + "x": 72, + "y0": 0, + "y1": 72, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 73, + "mark": null, + "x": 73, + "y0": 0, + "y1": 73, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 74, + "mark": null, + "x": 74, + "y0": 0, + "y1": 74, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 75, + "mark": null, + "x": 75, + "y0": 0, + "y1": 75, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 76, + "mark": null, + "x": 76, + "y0": 0, + "y1": 76, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 77, + "mark": null, + "x": 77, + "y0": 0, + "y1": 77, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 78, + "mark": null, + "x": 78, + "y0": 0, + "y1": 78, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 79, + "mark": null, + "x": 79, + "y0": 0, + "y1": 79, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 80, + "mark": null, + "x": 80, + "y0": 0, + "y1": 80, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 81, + "mark": null, + "x": 81, + "y0": 0, + "y1": 81, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 82, + "mark": null, + "x": 82, + "y0": 0, + "y1": 82, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 83, + "mark": null, + "x": 83, + "y0": 0, + "y1": 83, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 84, + "mark": null, + "x": 84, + "y0": 0, + "y1": 84, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 85, + "mark": null, + "x": 85, + "y0": 0, + "y1": 85, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 86, + "mark": null, + "x": 86, + "y0": 0, + "y1": 86, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 87, + "mark": null, + "x": 87, + "y0": 0, + "y1": 87, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 88, + "mark": null, + "x": 88, + "y0": 0, + "y1": 88, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 89, + "mark": null, + "x": 89, + "y0": 0, + "y1": 89, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 90, + "mark": null, + "x": 90, + "y0": 0, + "y1": 90, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 91, + "mark": null, + "x": 91, + "y0": 0, + "y1": 91, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 92, + "mark": null, + "x": 92, + "y0": 0, + "y1": 92, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 93, + "mark": null, + "x": 93, + "y0": 0, + "y1": 93, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 94, + "mark": null, + "x": 94, + "y0": 0, + "y1": 94, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 95, + "mark": null, + "x": 95, + "y0": 0, + "y1": 95, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 96, + "mark": null, + "x": 96, + "y0": 0, + "y1": 96, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 97, + "mark": null, + "x": 97, + "y0": 0, + "y1": 97, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 98, + "mark": null, + "x": 98, + "y0": 0, + "y1": 98, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 99, + "mark": null, + "x": 99, + "y0": 0, + "y1": 99, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 100, + "mark": null, + "x": 100, + "y0": 0, + "y1": 100, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 101, + "mark": null, + "x": 101, + "y0": 0, + "y1": 101, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 102, + "mark": null, + "x": 102, + "y0": 0, + "y1": 102, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 103, + "mark": null, + "x": 103, + "y0": 0, + "y1": 103, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 104, + "mark": null, + "x": 104, + "y0": 0, + "y1": 104, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 105, + "mark": null, + "x": 105, + "y0": 0, + "y1": 105, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 106, + "mark": null, + "x": 106, + "y0": 0, + "y1": 106, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 107, + "mark": null, + "x": 107, + "y0": 0, + "y1": 107, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 108, + "mark": null, + "x": 108, + "y0": 0, + "y1": 108, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 109, + "mark": null, + "x": 109, + "y0": 0, + "y1": 109, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 110, + "mark": null, + "x": 110, + "y0": 0, + "y1": 110, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 111, + "mark": null, + "x": 111, + "y0": 0, + "y1": 111, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 112, + "mark": null, + "x": 112, + "y0": 0, + "y1": 112, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 113, + "mark": null, + "x": 113, + "y0": 0, + "y1": 113, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 114, + "mark": null, + "x": 114, + "y0": 0, + "y1": 114, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 115, + "mark": null, + "x": 115, + "y0": 0, + "y1": 115, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 116, + "mark": null, + "x": 116, + "y0": 0, + "y1": 116, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 117, + "mark": null, + "x": 117, + "y0": 0, + "y1": 117, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 118, + "mark": null, + "x": 118, + "y0": 0, + "y1": 118, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 119, + "mark": null, + "x": 119, + "y0": 0, + "y1": 119, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 120, + "mark": null, + "x": 120, + "y0": 0, + "y1": 120, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 121, + "mark": null, + "x": 121, + "y0": 0, + "y1": 121, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 122, + "mark": null, + "x": 122, + "y0": 0, + "y1": 122, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 123, + "mark": null, + "x": 123, + "y0": 0, + "y1": 123, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 124, + "mark": null, + "x": 124, + "y0": 0, + "y1": 124, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 125, + "mark": null, + "x": 125, + "y0": 0, + "y1": 125, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 126, + "mark": null, + "x": 126, + "y0": 0, + "y1": 126, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 127, + "mark": null, + "x": 127, + "y0": 0, + "y1": 127, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 128, + "mark": null, + "x": 128, + "y0": 0, + "y1": 128, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 129, + "mark": null, + "x": 129, + "y0": 0, + "y1": 129, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 130, + "mark": null, + "x": 130, + "y0": 0, + "y1": 130, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 131, + "mark": null, + "x": 131, + "y0": 0, + "y1": 131, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 132, + "mark": null, + "x": 132, + "y0": 0, + "y1": 132, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 133, + "mark": null, + "x": 133, + "y0": 0, + "y1": 133, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 134, + "mark": null, + "x": 134, + "y0": 0, + "y1": 134, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 135, + "mark": null, + "x": 135, + "y0": 0, + "y1": 135, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 136, + "mark": null, + "x": 136, + "y0": 0, + "y1": 136, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 137, + "mark": null, + "x": 137, + "y0": 0, + "y1": 137, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 138, + "mark": null, + "x": 138, + "y0": 0, + "y1": 138, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 139, + "mark": null, + "x": 139, + "y0": 0, + "y1": 139, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 140, + "mark": null, + "x": 140, + "y0": 0, + "y1": 140, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 141, + "mark": null, + "x": 141, + "y0": 0, + "y1": 141, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 142, + "mark": null, + "x": 142, + "y0": 0, + "y1": 142, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 143, + "mark": null, + "x": 143, + "y0": 0, + "y1": 143, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 144, + "mark": null, + "x": 144, + "y0": 0, + "y1": 144, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 145, + "mark": null, + "x": 145, + "y0": 0, + "y1": 145, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 146, + "mark": null, + "x": 146, + "y0": 0, + "y1": 146, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 147, + "mark": null, + "x": 147, + "y0": 0, + "y1": 147, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 148, + "mark": null, + "x": 148, + "y0": 0, + "y1": 148, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 149, + "mark": null, + "x": 149, + "y0": 0, + "y1": 149, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 150, + "mark": null, + "x": 150, + "y0": 0, + "y1": 150, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 151, + "mark": null, + "x": 151, + "y0": 0, + "y1": 151, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 152, + "mark": null, + "x": 152, + "y0": 0, + "y1": 152, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 153, + "mark": null, + "x": 153, + "y0": 0, + "y1": 153, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 154, + "mark": null, + "x": 154, + "y0": 0, + "y1": 154, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 155, + "mark": null, + "x": 155, + "y0": 0, + "y1": 155, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 156, + "mark": null, + "x": 156, + "y0": 0, + "y1": 156, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 157, + "mark": null, + "x": 157, + "y0": 0, + "y1": 157, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 158, + "mark": null, + "x": 158, + "y0": 0, + "y1": 158, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 159, + "mark": null, + "x": 159, + "y0": 0, + "y1": 159, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 160, + "mark": null, + "x": 160, + "y0": 0, + "y1": 160, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 161, + "mark": null, + "x": 161, + "y0": 0, + "y1": 161, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 162, + "mark": null, + "x": 162, + "y0": 0, + "y1": 162, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 163, + "mark": null, + "x": 163, + "y0": 0, + "y1": 163, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 164, + "mark": null, + "x": 164, + "y0": 0, + "y1": 164, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 165, + "mark": null, + "x": 165, + "y0": 0, + "y1": 165, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 166, + "mark": null, + "x": 166, + "y0": 0, + "y1": 166, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 167, + "mark": null, + "x": 167, + "y0": 0, + "y1": 167, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 168, + "mark": null, + "x": 168, + "y0": 0, + "y1": 168, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 169, + "mark": null, + "x": 169, + "y0": 0, + "y1": 169, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 170, + "mark": null, + "x": 170, + "y0": 0, + "y1": 170, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 171, + "mark": null, + "x": 171, + "y0": 0, + "y1": 171, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 172, + "mark": null, + "x": 172, + "y0": 0, + "y1": 172, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 173, + "mark": null, + "x": 173, + "y0": 0, + "y1": 173, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 174, + "mark": null, + "x": 174, + "y0": 0, + "y1": 174, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 175, + "mark": null, + "x": 175, + "y0": 0, + "y1": 175, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 176, + "mark": null, + "x": 176, + "y0": 0, + "y1": 176, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 177, + "mark": null, + "x": 177, + "y0": 0, + "y1": 177, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 178, + "mark": null, + "x": 178, + "y0": 0, + "y1": 178, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 179, + "mark": null, + "x": 179, + "y0": 0, + "y1": 179, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 180, + "mark": null, + "x": 180, + "y0": 0, + "y1": 180, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 181, + "mark": null, + "x": 181, + "y0": 0, + "y1": 181, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 182, + "mark": null, + "x": 182, + "y0": 0, + "y1": 182, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 183, + "mark": null, + "x": 183, + "y0": 0, + "y1": 183, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 184, + "mark": null, + "x": 184, + "y0": 0, + "y1": 184, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 185, + "mark": null, + "x": 185, + "y0": 0, + "y1": 185, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 186, + "mark": null, + "x": 186, + "y0": 0, + "y1": 186, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 187, + "mark": null, + "x": 187, + "y0": 0, + "y1": 187, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 188, + "mark": null, + "x": 188, + "y0": 0, + "y1": 188, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 189, + "mark": null, + "x": 189, + "y0": 0, + "y1": 189, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 190, + "mark": null, + "x": 190, + "y0": 0, + "y1": 190, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 191, + "mark": null, + "x": 191, + "y0": 0, + "y1": 191, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 192, + "mark": null, + "x": 192, + "y0": 0, + "y1": 192, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 193, + "mark": null, + "x": 193, + "y0": 0, + "y1": 193, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 194, + "mark": null, + "x": 194, + "y0": 0, + "y1": 194, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 195, + "mark": null, + "x": 195, + "y0": 0, + "y1": 195, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 196, + "mark": null, + "x": 196, + "y0": 0, + "y1": 196, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 197, + "mark": null, + "x": 197, + "y0": 0, + "y1": 197, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 198, + "mark": null, + "x": 198, + "y0": 0, + "y1": 198, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 199, + "mark": null, + "x": 199, + "y0": 0, + "y1": 199, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 200, + "mark": null, + "x": 200, + "y0": 0, + "y1": 200, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 201, + "mark": null, + "x": 201, + "y0": 0, + "y1": 201, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 202, + "mark": null, + "x": 202, + "y0": 0, + "y1": 202, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 203, + "mark": null, + "x": 203, + "y0": 0, + "y1": 203, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 204, + "mark": null, + "x": 204, + "y0": 0, + "y1": 204, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 205, + "mark": null, + "x": 205, + "y0": 0, + "y1": 205, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 206, + "mark": null, + "x": 206, + "y0": 0, + "y1": 206, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 207, + "mark": null, + "x": 207, + "y0": 0, + "y1": 207, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 208, + "mark": null, + "x": 208, + "y0": 0, + "y1": 208, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 209, + "mark": null, + "x": 209, + "y0": 0, + "y1": 209, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 210, + "mark": null, + "x": 210, + "y0": 0, + "y1": 210, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 211, + "mark": null, + "x": 211, + "y0": 0, + "y1": 211, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 212, + "mark": null, + "x": 212, + "y0": 0, + "y1": 212, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 213, + "mark": null, + "x": 213, + "y0": 0, + "y1": 213, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 214, + "mark": null, + "x": 214, + "y0": 0, + "y1": 214, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 215, + "mark": null, + "x": 215, + "y0": 0, + "y1": 215, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 216, + "mark": null, + "x": 216, + "y0": 0, + "y1": 216, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 217, + "mark": null, + "x": 217, + "y0": 0, + "y1": 217, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 218, + "mark": null, + "x": 218, + "y0": 0, + "y1": 218, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 219, + "mark": null, + "x": 219, + "y0": 0, + "y1": 219, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 220, + "mark": null, + "x": 220, + "y0": 0, + "y1": 220, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 221, + "mark": null, + "x": 221, + "y0": 0, + "y1": 221, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 222, + "mark": null, + "x": 222, + "y0": 0, + "y1": 222, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 223, + "mark": null, + "x": 223, + "y0": 0, + "y1": 223, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 224, + "mark": null, + "x": 224, + "y0": 0, + "y1": 224, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 225, + "mark": null, + "x": 225, + "y0": 0, + "y1": 225, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 226, + "mark": null, + "x": 226, + "y0": 0, + "y1": 226, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 227, + "mark": null, + "x": 227, + "y0": 0, + "y1": 227, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 228, + "mark": null, + "x": 228, + "y0": 0, + "y1": 228, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 229, + "mark": null, + "x": 229, + "y0": 0, + "y1": 229, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 230, + "mark": null, + "x": 230, + "y0": 0, + "y1": 230, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 231, + "mark": null, + "x": 231, + "y0": 0, + "y1": 231, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 232, + "mark": null, + "x": 232, + "y0": 0, + "y1": 232, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 233, + "mark": null, + "x": 233, + "y0": 0, + "y1": 233, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 234, + "mark": null, + "x": 234, + "y0": 0, + "y1": 234, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 235, + "mark": null, + "x": 235, + "y0": 0, + "y1": 235, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 236, + "mark": null, + "x": 236, + "y0": 0, + "y1": 236, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 237, + "mark": null, + "x": 237, + "y0": 0, + "y1": 237, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 238, + "mark": null, + "x": 238, + "y0": 0, + "y1": 238, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 239, + "mark": null, + "x": 239, + "y0": 0, + "y1": 239, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 240, + "mark": null, + "x": 240, + "y0": 0, + "y1": 240, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 241, + "mark": null, + "x": 241, + "y0": 0, + "y1": 241, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 242, + "mark": null, + "x": 242, + "y0": 0, + "y1": 242, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 243, + "mark": null, + "x": 243, + "y0": 0, + "y1": 243, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 244, + "mark": null, + "x": 244, + "y0": 0, + "y1": 244, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 245, + "mark": null, + "x": 245, + "y0": 0, + "y1": 245, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 246, + "mark": null, + "x": 246, + "y0": 0, + "y1": 246, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 247, + "mark": null, + "x": 247, + "y0": 0, + "y1": 247, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 248, + "mark": null, + "x": 248, + "y0": 0, + "y1": 248, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 249, + "mark": null, + "x": 249, + "y0": 0, + "y1": 249, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 250, + "mark": null, + "x": 250, + "y0": 0, + "y1": 250, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 251, + "mark": null, + "x": 251, + "y0": 0, + "y1": 251, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 252, + "mark": null, + "x": 252, + "y0": 0, + "y1": 252, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 253, + "mark": null, + "x": 253, + "y0": 0, + "y1": 253, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 254, + "mark": null, + "x": 254, + "y0": 0, + "y1": 254, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 255, + "mark": null, + "x": 255, + "y0": 0, + "y1": 255, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 256, + "mark": null, + "x": 256, + "y0": 0, + "y1": 256, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 257, + "mark": null, + "x": 257, + "y0": 0, + "y1": 257, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 258, + "mark": null, + "x": 258, + "y0": 0, + "y1": 258, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 259, + "mark": null, + "x": 259, + "y0": 0, + "y1": 259, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 260, + "mark": null, + "x": 260, + "y0": 0, + "y1": 260, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 261, + "mark": null, + "x": 261, + "y0": 0, + "y1": 261, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 262, + "mark": null, + "x": 262, + "y0": 0, + "y1": 262, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 263, + "mark": null, + "x": 263, + "y0": 0, + "y1": 263, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 264, + "mark": null, + "x": 264, + "y0": 0, + "y1": 264, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 265, + "mark": null, + "x": 265, + "y0": 0, + "y1": 265, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 266, + "mark": null, + "x": 266, + "y0": 0, + "y1": 266, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 267, + "mark": null, + "x": 267, + "y0": 0, + "y1": 267, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 268, + "mark": null, + "x": 268, + "y0": 0, + "y1": 268, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 269, + "mark": null, + "x": 269, + "y0": 0, + "y1": 269, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 270, + "mark": null, + "x": 270, + "y0": 0, + "y1": 270, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 271, + "mark": null, + "x": 271, + "y0": 0, + "y1": 271, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 272, + "mark": null, + "x": 272, + "y0": 0, + "y1": 272, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 273, + "mark": null, + "x": 273, + "y0": 0, + "y1": 273, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 274, + "mark": null, + "x": 274, + "y0": 0, + "y1": 274, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 275, + "mark": null, + "x": 275, + "y0": 0, + "y1": 275, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 276, + "mark": null, + "x": 276, + "y0": 0, + "y1": 276, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 277, + "mark": null, + "x": 277, + "y0": 0, + "y1": 277, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 278, + "mark": null, + "x": 278, + "y0": 0, + "y1": 278, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 279, + "mark": null, + "x": 279, + "y0": 0, + "y1": 279, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 280, + "mark": null, + "x": 280, + "y0": 0, + "y1": 280, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 281, + "mark": null, + "x": 281, + "y0": 0, + "y1": 281, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 282, + "mark": null, + "x": 282, + "y0": 0, + "y1": 282, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 283, + "mark": null, + "x": 283, + "y0": 0, + "y1": 283, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 284, + "mark": null, + "x": 284, + "y0": 0, + "y1": 284, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 285, + "mark": null, + "x": 285, + "y0": 0, + "y1": 285, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 286, + "mark": null, + "x": 286, + "y0": 0, + "y1": 286, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 287, + "mark": null, + "x": 287, + "y0": 0, + "y1": 287, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 288, + "mark": null, + "x": 288, + "y0": 0, + "y1": 288, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 289, + "mark": null, + "x": 289, + "y0": 0, + "y1": 289, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 290, + "mark": null, + "x": 290, + "y0": 0, + "y1": 290, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 291, + "mark": null, + "x": 291, + "y0": 0, + "y1": 291, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 292, + "mark": null, + "x": 292, + "y0": 0, + "y1": 292, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 293, + "mark": null, + "x": 293, + "y0": 0, + "y1": 293, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 294, + "mark": null, + "x": 294, + "y0": 0, + "y1": 294, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 295, + "mark": null, + "x": 295, + "y0": 0, + "y1": 295, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 296, + "mark": null, + "x": 296, + "y0": 0, + "y1": 296, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 297, + "mark": null, + "x": 297, + "y0": 0, + "y1": 297, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 298, + "mark": null, + "x": 298, + "y0": 0, + "y1": 298, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 299, + "mark": null, + "x": 299, + "y0": 0, + "y1": 299, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 300, + "mark": null, + "x": 300, + "y0": 0, + "y1": 300, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 301, + "mark": null, + "x": 301, + "y0": 0, + "y1": 301, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 302, + "mark": null, + "x": 302, + "y0": 0, + "y1": 302, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 303, + "mark": null, + "x": 303, + "y0": 0, + "y1": 303, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 304, + "mark": null, + "x": 304, + "y0": 0, + "y1": 304, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 305, + "mark": null, + "x": 305, + "y0": 0, + "y1": 305, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 306, + "mark": null, + "x": 306, + "y0": 0, + "y1": 306, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 307, + "mark": null, + "x": 307, + "y0": 0, + "y1": 307, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 308, + "mark": null, + "x": 308, + "y0": 0, + "y1": 308, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 309, + "mark": null, + "x": 309, + "y0": 0, + "y1": 309, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 310, + "mark": null, + "x": 310, + "y0": 0, + "y1": 310, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 311, + "mark": null, + "x": 311, + "y0": 0, + "y1": 311, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 312, + "mark": null, + "x": 312, + "y0": 0, + "y1": 312, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 313, + "mark": null, + "x": 313, + "y0": 0, + "y1": 313, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 314, + "mark": null, + "x": 314, + "y0": 0, + "y1": 314, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 315, + "mark": null, + "x": 315, + "y0": 0, + "y1": 315, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 316, + "mark": null, + "x": 316, + "y0": 0, + "y1": 316, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 317, + "mark": null, + "x": 317, + "y0": 0, + "y1": 317, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 318, + "mark": null, + "x": 318, + "y0": 0, + "y1": 318, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 319, + "mark": null, + "x": 319, + "y0": 0, + "y1": 319, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 320, + "mark": null, + "x": 320, + "y0": 0, + "y1": 320, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 321, + "mark": null, + "x": 321, + "y0": 0, + "y1": 321, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 322, + "mark": null, + "x": 322, + "y0": 0, + "y1": 322, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 323, + "mark": null, + "x": 323, + "y0": 0, + "y1": 323, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 324, + "mark": null, + "x": 324, + "y0": 0, + "y1": 324, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 325, + "mark": null, + "x": 325, + "y0": 0, + "y1": 325, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 326, + "mark": null, + "x": 326, + "y0": 0, + "y1": 326, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 327, + "mark": null, + "x": 327, + "y0": 0, + "y1": 327, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 328, + "mark": null, + "x": 328, + "y0": 0, + "y1": 328, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 329, + "mark": null, + "x": 329, + "y0": 0, + "y1": 329, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 330, + "mark": null, + "x": 330, + "y0": 0, + "y1": 330, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 331, + "mark": null, + "x": 331, + "y0": 0, + "y1": 331, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 332, + "mark": null, + "x": 332, + "y0": 0, + "y1": 332, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 333, + "mark": null, + "x": 333, + "y0": 0, + "y1": 333, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 334, + "mark": null, + "x": 334, + "y0": 0, + "y1": 334, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 335, + "mark": null, + "x": 335, + "y0": 0, + "y1": 335, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 336, + "mark": null, + "x": 336, + "y0": 0, + "y1": 336, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 337, + "mark": null, + "x": 337, + "y0": 0, + "y1": 337, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 338, + "mark": null, + "x": 338, + "y0": 0, + "y1": 338, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 339, + "mark": null, + "x": 339, + "y0": 0, + "y1": 339, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 340, + "mark": null, + "x": 340, + "y0": 0, + "y1": 340, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 341, + "mark": null, + "x": 341, + "y0": 0, + "y1": 341, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 342, + "mark": null, + "x": 342, + "y0": 0, + "y1": 342, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 343, + "mark": null, + "x": 343, + "y0": 0, + "y1": 343, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 344, + "mark": null, + "x": 344, + "y0": 0, + "y1": 344, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 345, + "mark": null, + "x": 345, + "y0": 0, + "y1": 345, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 346, + "mark": null, + "x": 346, + "y0": 0, + "y1": 346, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 347, + "mark": null, + "x": 347, + "y0": 0, + "y1": 347, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 348, + "mark": null, + "x": 348, + "y0": 0, + "y1": 348, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 349, + "mark": null, + "x": 349, + "y0": 0, + "y1": 349, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 350, + "mark": null, + "x": 350, + "y0": 0, + "y1": 350, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 351, + "mark": null, + "x": 351, + "y0": 0, + "y1": 351, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 352, + "mark": null, + "x": 352, + "y0": 0, + "y1": 352, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 353, + "mark": null, + "x": 353, + "y0": 0, + "y1": 353, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 354, + "mark": null, + "x": 354, + "y0": 0, + "y1": 354, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 355, + "mark": null, + "x": 355, + "y0": 0, + "y1": 355, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 356, + "mark": null, + "x": 356, + "y0": 0, + "y1": 356, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 357, + "mark": null, + "x": 357, + "y0": 0, + "y1": 357, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 358, + "mark": null, + "x": 358, + "y0": 0, + "y1": 358, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 359, + "mark": null, + "x": 359, + "y0": 0, + "y1": 359, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 360, + "mark": null, + "x": 360, + "y0": 0, + "y1": 360, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 361, + "mark": null, + "x": 361, + "y0": 0, + "y1": 361, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 362, + "mark": null, + "x": 362, + "y0": 0, + "y1": 362, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 363, + "mark": null, + "x": 363, + "y0": 0, + "y1": 363, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 364, + "mark": null, + "x": 364, + "y0": 0, + "y1": 364, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 365, + "mark": null, + "x": 365, + "y0": 0, + "y1": 365, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 366, + "mark": null, + "x": 366, + "y0": 0, + "y1": 366, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 367, + "mark": null, + "x": 367, + "y0": 0, + "y1": 367, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 368, + "mark": null, + "x": 368, + "y0": 0, + "y1": 368, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 369, + "mark": null, + "x": 369, + "y0": 0, + "y1": 369, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 370, + "mark": null, + "x": 370, + "y0": 0, + "y1": 370, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 371, + "mark": null, + "x": 371, + "y0": 0, + "y1": 371, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 372, + "mark": null, + "x": 372, + "y0": 0, + "y1": 372, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 373, + "mark": null, + "x": 373, + "y0": 0, + "y1": 373, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 374, + "mark": null, + "x": 374, + "y0": 0, + "y1": 374, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 375, + "mark": null, + "x": 375, + "y0": 0, + "y1": 375, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 376, + "mark": null, + "x": 376, + "y0": 0, + "y1": 376, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 377, + "mark": null, + "x": 377, + "y0": 0, + "y1": 377, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 378, + "mark": null, + "x": 378, + "y0": 0, + "y1": 378, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 379, + "mark": null, + "x": 379, + "y0": 0, + "y1": 379, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 380, + "mark": null, + "x": 380, + "y0": 0, + "y1": 380, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 381, + "mark": null, + "x": 381, + "y0": 0, + "y1": 381, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 382, + "mark": null, + "x": 382, + "y0": 0, + "y1": 382, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 383, + "mark": null, + "x": 383, + "y0": 0, + "y1": 383, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 384, + "mark": null, + "x": 384, + "y0": 0, + "y1": 384, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 385, + "mark": null, + "x": 385, + "y0": 0, + "y1": 385, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 386, + "mark": null, + "x": 386, + "y0": 0, + "y1": 386, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 387, + "mark": null, + "x": 387, + "y0": 0, + "y1": 387, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 388, + "mark": null, + "x": 388, + "y0": 0, + "y1": 388, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 389, + "mark": null, + "x": 389, + "y0": 0, + "y1": 389, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 390, + "mark": null, + "x": 390, + "y0": 0, + "y1": 390, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 391, + "mark": null, + "x": 391, + "y0": 0, + "y1": 391, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 392, + "mark": null, + "x": 392, + "y0": 0, + "y1": 392, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 393, + "mark": null, + "x": 393, + "y0": 0, + "y1": 393, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 394, + "mark": null, + "x": 394, + "y0": 0, + "y1": 394, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 395, + "mark": null, + "x": 395, + "y0": 0, + "y1": 395, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 396, + "mark": null, + "x": 396, + "y0": 0, + "y1": 396, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 397, + "mark": null, + "x": 397, + "y0": 0, + "y1": 397, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 398, + "mark": null, + "x": 398, + "y0": 0, + "y1": 398, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 399, + "mark": null, + "x": 399, + "y0": 0, + "y1": 399, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 400, + "mark": null, + "x": 400, + "y0": 0, + "y1": 400, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 401, + "mark": null, + "x": 401, + "y0": 0, + "y1": 401, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 402, + "mark": null, + "x": 402, + "y0": 0, + "y1": 402, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 403, + "mark": null, + "x": 403, + "y0": 0, + "y1": 403, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 404, + "mark": null, + "x": 404, + "y0": 0, + "y1": 404, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 405, + "mark": null, + "x": 405, + "y0": 0, + "y1": 405, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 406, + "mark": null, + "x": 406, + "y0": 0, + "y1": 406, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 407, + "mark": null, + "x": 407, + "y0": 0, + "y1": 407, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 408, + "mark": null, + "x": 408, + "y0": 0, + "y1": 408, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 409, + "mark": null, + "x": 409, + "y0": 0, + "y1": 409, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 410, + "mark": null, + "x": 410, + "y0": 0, + "y1": 410, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 411, + "mark": null, + "x": 411, + "y0": 0, + "y1": 411, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 412, + "mark": null, + "x": 412, + "y0": 0, + "y1": 412, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 413, + "mark": null, + "x": 413, + "y0": 0, + "y1": 413, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 414, + "mark": null, + "x": 414, + "y0": 0, + "y1": 414, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 415, + "mark": null, + "x": 415, + "y0": 0, + "y1": 415, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 416, + "mark": null, + "x": 416, + "y0": 0, + "y1": 416, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 417, + "mark": null, + "x": 417, + "y0": 0, + "y1": 417, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 418, + "mark": null, + "x": 418, + "y0": 0, + "y1": 418, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 419, + "mark": null, + "x": 419, + "y0": 0, + "y1": 419, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 420, + "mark": null, + "x": 420, + "y0": 0, + "y1": 420, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 421, + "mark": null, + "x": 421, + "y0": 0, + "y1": 421, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 422, + "mark": null, + "x": 422, + "y0": 0, + "y1": 422, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 423, + "mark": null, + "x": 423, + "y0": 0, + "y1": 423, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 424, + "mark": null, + "x": 424, + "y0": 0, + "y1": 424, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 425, + "mark": null, + "x": 425, + "y0": 0, + "y1": 425, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 426, + "mark": null, + "x": 426, + "y0": 0, + "y1": 426, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 427, + "mark": null, + "x": 427, + "y0": 0, + "y1": 427, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 428, + "mark": null, + "x": 428, + "y0": 0, + "y1": 428, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 429, + "mark": null, + "x": 429, + "y0": 0, + "y1": 429, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 430, + "mark": null, + "x": 430, + "y0": 0, + "y1": 430, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 431, + "mark": null, + "x": 431, + "y0": 0, + "y1": 431, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 432, + "mark": null, + "x": 432, + "y0": 0, + "y1": 432, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 433, + "mark": null, + "x": 433, + "y0": 0, + "y1": 433, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 434, + "mark": null, + "x": 434, + "y0": 0, + "y1": 434, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 435, + "mark": null, + "x": 435, + "y0": 0, + "y1": 435, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 436, + "mark": null, + "x": 436, + "y0": 0, + "y1": 436, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 437, + "mark": null, + "x": 437, + "y0": 0, + "y1": 437, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 438, + "mark": null, + "x": 438, + "y0": 0, + "y1": 438, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 439, + "mark": null, + "x": 439, + "y0": 0, + "y1": 439, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 440, + "mark": null, + "x": 440, + "y0": 0, + "y1": 440, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 441, + "mark": null, + "x": 441, + "y0": 0, + "y1": 441, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 442, + "mark": null, + "x": 442, + "y0": 0, + "y1": 442, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 443, + "mark": null, + "x": 443, + "y0": 0, + "y1": 443, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 444, + "mark": null, + "x": 444, + "y0": 0, + "y1": 444, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 445, + "mark": null, + "x": 445, + "y0": 0, + "y1": 445, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 446, + "mark": null, + "x": 446, + "y0": 0, + "y1": 446, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 447, + "mark": null, + "x": 447, + "y0": 0, + "y1": 447, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 448, + "mark": null, + "x": 448, + "y0": 0, + "y1": 448, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 449, + "mark": null, + "x": 449, + "y0": 0, + "y1": 449, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 450, + "mark": null, + "x": 450, + "y0": 0, + "y1": 450, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 451, + "mark": null, + "x": 451, + "y0": 0, + "y1": 451, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 452, + "mark": null, + "x": 452, + "y0": 0, + "y1": 452, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 453, + "mark": null, + "x": 453, + "y0": 0, + "y1": 453, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 454, + "mark": null, + "x": 454, + "y0": 0, + "y1": 454, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 455, + "mark": null, + "x": 455, + "y0": 0, + "y1": 455, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 456, + "mark": null, + "x": 456, + "y0": 0, + "y1": 456, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 457, + "mark": null, + "x": 457, + "y0": 0, + "y1": 457, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 458, + "mark": null, + "x": 458, + "y0": 0, + "y1": 458, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 459, + "mark": null, + "x": 459, + "y0": 0, + "y1": 459, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 460, + "mark": null, + "x": 460, + "y0": 0, + "y1": 460, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 461, + "mark": null, + "x": 461, + "y0": 0, + "y1": 461, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 462, + "mark": null, + "x": 462, + "y0": 0, + "y1": 462, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 463, + "mark": null, + "x": 463, + "y0": 0, + "y1": 463, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 464, + "mark": null, + "x": 464, + "y0": 0, + "y1": 464, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 465, + "mark": null, + "x": 465, + "y0": 0, + "y1": 465, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 466, + "mark": null, + "x": 466, + "y0": 0, + "y1": 466, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 467, + "mark": null, + "x": 467, + "y0": 0, + "y1": 467, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 468, + "mark": null, + "x": 468, + "y0": 0, + "y1": 468, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 469, + "mark": null, + "x": 469, + "y0": 0, + "y1": 469, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 470, + "mark": null, + "x": 470, + "y0": 0, + "y1": 470, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 471, + "mark": null, + "x": 471, + "y0": 0, + "y1": 471, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 472, + "mark": null, + "x": 472, + "y0": 0, + "y1": 472, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 473, + "mark": null, + "x": 473, + "y0": 0, + "y1": 473, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 474, + "mark": null, + "x": 474, + "y0": 0, + "y1": 474, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 475, + "mark": null, + "x": 475, + "y0": 0, + "y1": 475, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 476, + "mark": null, + "x": 476, + "y0": 0, + "y1": 476, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 477, + "mark": null, + "x": 477, + "y0": 0, + "y1": 477, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 478, + "mark": null, + "x": 478, + "y0": 0, + "y1": 478, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 479, + "mark": null, + "x": 479, + "y0": 0, + "y1": 479, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 480, + "mark": null, + "x": 480, + "y0": 0, + "y1": 480, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 481, + "mark": null, + "x": 481, + "y0": 0, + "y1": 481, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 482, + "mark": null, + "x": 482, + "y0": 0, + "y1": 482, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 483, + "mark": null, + "x": 483, + "y0": 0, + "y1": 483, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 484, + "mark": null, + "x": 484, + "y0": 0, + "y1": 484, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 485, + "mark": null, + "x": 485, + "y0": 0, + "y1": 485, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 486, + "mark": null, + "x": 486, + "y0": 0, + "y1": 486, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 487, + "mark": null, + "x": 487, + "y0": 0, + "y1": 487, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 488, + "mark": null, + "x": 488, + "y0": 0, + "y1": 488, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 489, + "mark": null, + "x": 489, + "y0": 0, + "y1": 489, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 490, + "mark": null, + "x": 490, + "y0": 0, + "y1": 490, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 491, + "mark": null, + "x": 491, + "y0": 0, + "y1": 491, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 492, + "mark": null, + "x": 492, + "y0": 0, + "y1": 492, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 493, + "mark": null, + "x": 493, + "y0": 0, + "y1": 493, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 494, + "mark": null, + "x": 494, + "y0": 0, + "y1": 494, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 495, + "mark": null, + "x": 495, + "y0": 0, + "y1": 495, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 496, + "mark": null, + "x": 496, + "y0": 0, + "y1": 496, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 497, + "mark": null, + "x": 497, + "y0": 0, + "y1": 497, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 498, + "mark": null, + "x": 498, + "y0": 0, + "y1": 498, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 499, + "mark": null, + "x": 499, + "y0": 0, + "y1": 499, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 500, + "mark": null, + "x": 500, + "y0": 0, + "y1": 500, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 501, + "mark": null, + "x": 501, + "y0": 0, + "y1": 501, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 502, + "mark": null, + "x": 502, + "y0": 0, + "y1": 502, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 503, + "mark": null, + "x": 503, + "y0": 0, + "y1": 503, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 504, + "mark": null, + "x": 504, + "y0": 0, + "y1": 504, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 505, + "mark": null, + "x": 505, + "y0": 0, + "y1": 505, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 506, + "mark": null, + "x": 506, + "y0": 0, + "y1": 506, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 507, + "mark": null, + "x": 507, + "y0": 0, + "y1": 507, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 508, + "mark": null, + "x": 508, + "y0": 0, + "y1": 508, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 509, + "mark": null, + "x": 509, + "y0": 0, + "y1": 509, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 510, + "mark": null, + "x": 510, + "y0": 0, + "y1": 510, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 511, + "mark": null, + "x": 511, + "y0": 0, + "y1": 511, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 512, + "mark": null, + "x": 512, + "y0": 0, + "y1": 512, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 513, + "mark": null, + "x": 513, + "y0": 0, + "y1": 513, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 514, + "mark": null, + "x": 514, + "y0": 0, + "y1": 514, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 515, + "mark": null, + "x": 515, + "y0": 0, + "y1": 515, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 516, + "mark": null, + "x": 516, + "y0": 0, + "y1": 516, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 517, + "mark": null, + "x": 517, + "y0": 0, + "y1": 517, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 518, + "mark": null, + "x": 518, + "y0": 0, + "y1": 518, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 519, + "mark": null, + "x": 519, + "y0": 0, + "y1": 519, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 520, + "mark": null, + "x": 520, + "y0": 0, + "y1": 520, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 521, + "mark": null, + "x": 521, + "y0": 0, + "y1": 521, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 522, + "mark": null, + "x": 522, + "y0": 0, + "y1": 522, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 523, + "mark": null, + "x": 523, + "y0": 0, + "y1": 523, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 524, + "mark": null, + "x": 524, + "y0": 0, + "y1": 524, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 525, + "mark": null, + "x": 525, + "y0": 0, + "y1": 525, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 526, + "mark": null, + "x": 526, + "y0": 0, + "y1": 526, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 527, + "mark": null, + "x": 527, + "y0": 0, + "y1": 527, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 528, + "mark": null, + "x": 528, + "y0": 0, + "y1": 528, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 529, + "mark": null, + "x": 529, + "y0": 0, + "y1": 529, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 530, + "mark": null, + "x": 530, + "y0": 0, + "y1": 530, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 531, + "mark": null, + "x": 531, + "y0": 0, + "y1": 531, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 532, + "mark": null, + "x": 532, + "y0": 0, + "y1": 532, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 533, + "mark": null, + "x": 533, + "y0": 0, + "y1": 533, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 534, + "mark": null, + "x": 534, + "y0": 0, + "y1": 534, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 535, + "mark": null, + "x": 535, + "y0": 0, + "y1": 535, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 536, + "mark": null, + "x": 536, + "y0": 0, + "y1": 536, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 537, + "mark": null, + "x": 537, + "y0": 0, + "y1": 537, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 538, + "mark": null, + "x": 538, + "y0": 0, + "y1": 538, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 539, + "mark": null, + "x": 539, + "y0": 0, + "y1": 539, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 540, + "mark": null, + "x": 540, + "y0": 0, + "y1": 540, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 541, + "mark": null, + "x": 541, + "y0": 0, + "y1": 541, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 542, + "mark": null, + "x": 542, + "y0": 0, + "y1": 542, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 543, + "mark": null, + "x": 543, + "y0": 0, + "y1": 543, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 544, + "mark": null, + "x": 544, + "y0": 0, + "y1": 544, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 545, + "mark": null, + "x": 545, + "y0": 0, + "y1": 545, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 546, + "mark": null, + "x": 546, + "y0": 0, + "y1": 546, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 547, + "mark": null, + "x": 547, + "y0": 0, + "y1": 547, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 548, + "mark": null, + "x": 548, + "y0": 0, + "y1": 548, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 549, + "mark": null, + "x": 549, + "y0": 0, + "y1": 549, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 550, + "mark": null, + "x": 550, + "y0": 0, + "y1": 550, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 551, + "mark": null, + "x": 551, + "y0": 0, + "y1": 551, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 552, + "mark": null, + "x": 552, + "y0": 0, + "y1": 552, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 553, + "mark": null, + "x": 553, + "y0": 0, + "y1": 553, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 554, + "mark": null, + "x": 554, + "y0": 0, + "y1": 554, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 555, + "mark": null, + "x": 555, + "y0": 0, + "y1": 555, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 556, + "mark": null, + "x": 556, + "y0": 0, + "y1": 556, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 557, + "mark": null, + "x": 557, + "y0": 0, + "y1": 557, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 558, + "mark": null, + "x": 558, + "y0": 0, + "y1": 558, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 559, + "mark": null, + "x": 559, + "y0": 0, + "y1": 559, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 560, + "mark": null, + "x": 560, + "y0": 0, + "y1": 560, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 561, + "mark": null, + "x": 561, + "y0": 0, + "y1": 561, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 562, + "mark": null, + "x": 562, + "y0": 0, + "y1": 562, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 563, + "mark": null, + "x": 563, + "y0": 0, + "y1": 563, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 564, + "mark": null, + "x": 564, + "y0": 0, + "y1": 564, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 565, + "mark": null, + "x": 565, + "y0": 0, + "y1": 565, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 566, + "mark": null, + "x": 566, + "y0": 0, + "y1": 566, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 567, + "mark": null, + "x": 567, + "y0": 0, + "y1": 567, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 568, + "mark": null, + "x": 568, + "y0": 0, + "y1": 568, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 569, + "mark": null, + "x": 569, + "y0": 0, + "y1": 569, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 570, + "mark": null, + "x": 570, + "y0": 0, + "y1": 570, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 571, + "mark": null, + "x": 571, + "y0": 0, + "y1": 571, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 572, + "mark": null, + "x": 572, + "y0": 0, + "y1": 572, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 573, + "mark": null, + "x": 573, + "y0": 0, + "y1": 573, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 574, + "mark": null, + "x": 574, + "y0": 0, + "y1": 574, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 575, + "mark": null, + "x": 575, + "y0": 0, + "y1": 575, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 576, + "mark": null, + "x": 576, + "y0": 0, + "y1": 576, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 577, + "mark": null, + "x": 577, + "y0": 0, + "y1": 577, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 578, + "mark": null, + "x": 578, + "y0": 0, + "y1": 578, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 579, + "mark": null, + "x": 579, + "y0": 0, + "y1": 579, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 580, + "mark": null, + "x": 580, + "y0": 0, + "y1": 580, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 581, + "mark": null, + "x": 581, + "y0": 0, + "y1": 581, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 582, + "mark": null, + "x": 582, + "y0": 0, + "y1": 582, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 583, + "mark": null, + "x": 583, + "y0": 0, + "y1": 583, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 584, + "mark": null, + "x": 584, + "y0": 0, + "y1": 584, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 585, + "mark": null, + "x": 585, + "y0": 0, + "y1": 585, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 586, + "mark": null, + "x": 586, + "y0": 0, + "y1": 586, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 587, + "mark": null, + "x": 587, + "y0": 0, + "y1": 587, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 588, + "mark": null, + "x": 588, + "y0": 0, + "y1": 588, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 589, + "mark": null, + "x": 589, + "y0": 0, + "y1": 589, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 590, + "mark": null, + "x": 590, + "y0": 0, + "y1": 590, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 591, + "mark": null, + "x": 591, + "y0": 0, + "y1": 591, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 592, + "mark": null, + "x": 592, + "y0": 0, + "y1": 592, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 593, + "mark": null, + "x": 593, + "y0": 0, + "y1": 593, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 594, + "mark": null, + "x": 594, + "y0": 0, + "y1": 594, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 595, + "mark": null, + "x": 595, + "y0": 0, + "y1": 595, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 596, + "mark": null, + "x": 596, + "y0": 0, + "y1": 596, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 597, + "mark": null, + "x": 597, + "y0": 0, + "y1": 597, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 598, + "mark": null, + "x": 598, + "y0": 0, + "y1": 598, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 599, + "mark": null, + "x": 599, + "y0": 0, + "y1": 599, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 600, + "mark": null, + "x": 600, + "y0": 0, + "y1": 600, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 601, + "mark": null, + "x": 601, + "y0": 0, + "y1": 601, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 602, + "mark": null, + "x": 602, + "y0": 0, + "y1": 602, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 603, + "mark": null, + "x": 603, + "y0": 0, + "y1": 603, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 604, + "mark": null, + "x": 604, + "y0": 0, + "y1": 604, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 605, + "mark": null, + "x": 605, + "y0": 0, + "y1": 605, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 606, + "mark": null, + "x": 606, + "y0": 0, + "y1": 606, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 607, + "mark": null, + "x": 607, + "y0": 0, + "y1": 607, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 608, + "mark": null, + "x": 608, + "y0": 0, + "y1": 608, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 609, + "mark": null, + "x": 609, + "y0": 0, + "y1": 609, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 610, + "mark": null, + "x": 610, + "y0": 0, + "y1": 610, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 611, + "mark": null, + "x": 611, + "y0": 0, + "y1": 611, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 612, + "mark": null, + "x": 612, + "y0": 0, + "y1": 612, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 613, + "mark": null, + "x": 613, + "y0": 0, + "y1": 613, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 614, + "mark": null, + "x": 614, + "y0": 0, + "y1": 614, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 615, + "mark": null, + "x": 615, + "y0": 0, + "y1": 615, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 616, + "mark": null, + "x": 616, + "y0": 0, + "y1": 616, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 617, + "mark": null, + "x": 617, + "y0": 0, + "y1": 617, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 618, + "mark": null, + "x": 618, + "y0": 0, + "y1": 618, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 619, + "mark": null, + "x": 619, + "y0": 0, + "y1": 619, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 620, + "mark": null, + "x": 620, + "y0": 0, + "y1": 620, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 621, + "mark": null, + "x": 621, + "y0": 0, + "y1": 621, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 622, + "mark": null, + "x": 622, + "y0": 0, + "y1": 622, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 623, + "mark": null, + "x": 623, + "y0": 0, + "y1": 623, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 624, + "mark": null, + "x": 624, + "y0": 0, + "y1": 624, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 625, + "mark": null, + "x": 625, + "y0": 0, + "y1": 625, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 626, + "mark": null, + "x": 626, + "y0": 0, + "y1": 626, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 627, + "mark": null, + "x": 627, + "y0": 0, + "y1": 627, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 628, + "mark": null, + "x": 628, + "y0": 0, + "y1": 628, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 629, + "mark": null, + "x": 629, + "y0": 0, + "y1": 629, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 630, + "mark": null, + "x": 630, + "y0": 0, + "y1": 630, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 631, + "mark": null, + "x": 631, + "y0": 0, + "y1": 631, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 632, + "mark": null, + "x": 632, + "y0": 0, + "y1": 632, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 633, + "mark": null, + "x": 633, + "y0": 0, + "y1": 633, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 634, + "mark": null, + "x": 634, + "y0": 0, + "y1": 634, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 635, + "mark": null, + "x": 635, + "y0": 0, + "y1": 635, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 636, + "mark": null, + "x": 636, + "y0": 0, + "y1": 636, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 637, + "mark": null, + "x": 637, + "y0": 0, + "y1": 637, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 638, + "mark": null, + "x": 638, + "y0": 0, + "y1": 638, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 639, + "mark": null, + "x": 639, + "y0": 0, + "y1": 639, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 640, + "mark": null, + "x": 640, + "y0": 0, + "y1": 640, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 641, + "mark": null, + "x": 641, + "y0": 0, + "y1": 641, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 642, + "mark": null, + "x": 642, + "y0": 0, + "y1": 642, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 643, + "mark": null, + "x": 643, + "y0": 0, + "y1": 643, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 644, + "mark": null, + "x": 644, + "y0": 0, + "y1": 644, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 645, + "mark": null, + "x": 645, + "y0": 0, + "y1": 645, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 646, + "mark": null, + "x": 646, + "y0": 0, + "y1": 646, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 647, + "mark": null, + "x": 647, + "y0": 0, + "y1": 647, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 648, + "mark": null, + "x": 648, + "y0": 0, + "y1": 648, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 649, + "mark": null, + "x": 649, + "y0": 0, + "y1": 649, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 650, + "mark": null, + "x": 650, + "y0": 0, + "y1": 650, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 651, + "mark": null, + "x": 651, + "y0": 0, + "y1": 651, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 652, + "mark": null, + "x": 652, + "y0": 0, + "y1": 652, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 653, + "mark": null, + "x": 653, + "y0": 0, + "y1": 653, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 654, + "mark": null, + "x": 654, + "y0": 0, + "y1": 654, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 655, + "mark": null, + "x": 655, + "y0": 0, + "y1": 655, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 656, + "mark": null, + "x": 656, + "y0": 0, + "y1": 656, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 657, + "mark": null, + "x": 657, + "y0": 0, + "y1": 657, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 658, + "mark": null, + "x": 658, + "y0": 0, + "y1": 658, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 659, + "mark": null, + "x": 659, + "y0": 0, + "y1": 659, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 660, + "mark": null, + "x": 660, + "y0": 0, + "y1": 660, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 661, + "mark": null, + "x": 661, + "y0": 0, + "y1": 661, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 662, + "mark": null, + "x": 662, + "y0": 0, + "y1": 662, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 663, + "mark": null, + "x": 663, + "y0": 0, + "y1": 663, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 664, + "mark": null, + "x": 664, + "y0": 0, + "y1": 664, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 665, + "mark": null, + "x": 665, + "y0": 0, + "y1": 665, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 666, + "mark": null, + "x": 666, + "y0": 0, + "y1": 666, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 667, + "mark": null, + "x": 667, + "y0": 0, + "y1": 667, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 668, + "mark": null, + "x": 668, + "y0": 0, + "y1": 668, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 669, + "mark": null, + "x": 669, + "y0": 0, + "y1": 669, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 670, + "mark": null, + "x": 670, + "y0": 0, + "y1": 670, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 671, + "mark": null, + "x": 671, + "y0": 0, + "y1": 671, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 672, + "mark": null, + "x": 672, + "y0": 0, + "y1": 672, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 673, + "mark": null, + "x": 673, + "y0": 0, + "y1": 673, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 674, + "mark": null, + "x": 674, + "y0": 0, + "y1": 674, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 675, + "mark": null, + "x": 675, + "y0": 0, + "y1": 675, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 676, + "mark": null, + "x": 676, + "y0": 0, + "y1": 676, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 677, + "mark": null, + "x": 677, + "y0": 0, + "y1": 677, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 678, + "mark": null, + "x": 678, + "y0": 0, + "y1": 678, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 679, + "mark": null, + "x": 679, + "y0": 0, + "y1": 679, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 680, + "mark": null, + "x": 680, + "y0": 0, + "y1": 680, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 681, + "mark": null, + "x": 681, + "y0": 0, + "y1": 681, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 682, + "mark": null, + "x": 682, + "y0": 0, + "y1": 682, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 683, + "mark": null, + "x": 683, + "y0": 0, + "y1": 683, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 684, + "mark": null, + "x": 684, + "y0": 0, + "y1": 684, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 685, + "mark": null, + "x": 685, + "y0": 0, + "y1": 685, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 686, + "mark": null, + "x": 686, + "y0": 0, + "y1": 686, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 687, + "mark": null, + "x": 687, + "y0": 0, + "y1": 687, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 688, + "mark": null, + "x": 688, + "y0": 0, + "y1": 688, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 689, + "mark": null, + "x": 689, + "y0": 0, + "y1": 689, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 690, + "mark": null, + "x": 690, + "y0": 0, + "y1": 690, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 691, + "mark": null, + "x": 691, + "y0": 0, + "y1": 691, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 692, + "mark": null, + "x": 692, + "y0": 0, + "y1": 692, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 693, + "mark": null, + "x": 693, + "y0": 0, + "y1": 693, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 694, + "mark": null, + "x": 694, + "y0": 0, + "y1": 694, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 695, + "mark": null, + "x": 695, + "y0": 0, + "y1": 695, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 696, + "mark": null, + "x": 696, + "y0": 0, + "y1": 696, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 697, + "mark": null, + "x": 697, + "y0": 0, + "y1": 697, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 698, + "mark": null, + "x": 698, + "y0": 0, + "y1": 698, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 699, + "mark": null, + "x": 699, + "y0": 0, + "y1": 699, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 700, + "mark": null, + "x": 700, + "y0": 0, + "y1": 700, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 701, + "mark": null, + "x": 701, + "y0": 0, + "y1": 701, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 702, + "mark": null, + "x": 702, + "y0": 0, + "y1": 702, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 703, + "mark": null, + "x": 703, + "y0": 0, + "y1": 703, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 704, + "mark": null, + "x": 704, + "y0": 0, + "y1": 704, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 705, + "mark": null, + "x": 705, + "y0": 0, + "y1": 705, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 706, + "mark": null, + "x": 706, + "y0": 0, + "y1": 706, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 707, + "mark": null, + "x": 707, + "y0": 0, + "y1": 707, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 708, + "mark": null, + "x": 708, + "y0": 0, + "y1": 708, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 709, + "mark": null, + "x": 709, + "y0": 0, + "y1": 709, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 710, + "mark": null, + "x": 710, + "y0": 0, + "y1": 710, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 711, + "mark": null, + "x": 711, + "y0": 0, + "y1": 711, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 712, + "mark": null, + "x": 712, + "y0": 0, + "y1": 712, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 713, + "mark": null, + "x": 713, + "y0": 0, + "y1": 713, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 714, + "mark": null, + "x": 714, + "y0": 0, + "y1": 714, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 715, + "mark": null, + "x": 715, + "y0": 0, + "y1": 715, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 716, + "mark": null, + "x": 716, + "y0": 0, + "y1": 716, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 717, + "mark": null, + "x": 717, + "y0": 0, + "y1": 717, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 718, + "mark": null, + "x": 718, + "y0": 0, + "y1": 718, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 719, + "mark": null, + "x": 719, + "y0": 0, + "y1": 719, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 720, + "mark": null, + "x": 720, + "y0": 0, + "y1": 720, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 721, + "mark": null, + "x": 721, + "y0": 0, + "y1": 721, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 722, + "mark": null, + "x": 722, + "y0": 0, + "y1": 722, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 723, + "mark": null, + "x": 723, + "y0": 0, + "y1": 723, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 724, + "mark": null, + "x": 724, + "y0": 0, + "y1": 724, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 725, + "mark": null, + "x": 725, + "y0": 0, + "y1": 725, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 726, + "mark": null, + "x": 726, + "y0": 0, + "y1": 726, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 727, + "mark": null, + "x": 727, + "y0": 0, + "y1": 727, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 728, + "mark": null, + "x": 728, + "y0": 0, + "y1": 728, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 729, + "mark": null, + "x": 729, + "y0": 0, + "y1": 729, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 730, + "mark": null, + "x": 730, + "y0": 0, + "y1": 730, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 731, + "mark": null, + "x": 731, + "y0": 0, + "y1": 731, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 732, + "mark": null, + "x": 732, + "y0": 0, + "y1": 732, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 733, + "mark": null, + "x": 733, + "y0": 0, + "y1": 733, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 734, + "mark": null, + "x": 734, + "y0": 0, + "y1": 734, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 735, + "mark": null, + "x": 735, + "y0": 0, + "y1": 735, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 736, + "mark": null, + "x": 736, + "y0": 0, + "y1": 736, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 737, + "mark": null, + "x": 737, + "y0": 0, + "y1": 737, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 738, + "mark": null, + "x": 738, + "y0": 0, + "y1": 738, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 739, + "mark": null, + "x": 739, + "y0": 0, + "y1": 739, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 740, + "mark": null, + "x": 740, + "y0": 0, + "y1": 740, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 741, + "mark": null, + "x": 741, + "y0": 0, + "y1": 741, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 742, + "mark": null, + "x": 742, + "y0": 0, + "y1": 742, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 743, + "mark": null, + "x": 743, + "y0": 0, + "y1": 743, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 744, + "mark": null, + "x": 744, + "y0": 0, + "y1": 744, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 745, + "mark": null, + "x": 745, + "y0": 0, + "y1": 745, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 746, + "mark": null, + "x": 746, + "y0": 0, + "y1": 746, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 747, + "mark": null, + "x": 747, + "y0": 0, + "y1": 747, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 748, + "mark": null, + "x": 748, + "y0": 0, + "y1": 748, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 749, + "mark": null, + "x": 749, + "y0": 0, + "y1": 749, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 750, + "mark": null, + "x": 750, + "y0": 0, + "y1": 750, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 751, + "mark": null, + "x": 751, + "y0": 0, + "y1": 751, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 752, + "mark": null, + "x": 752, + "y0": 0, + "y1": 752, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 753, + "mark": null, + "x": 753, + "y0": 0, + "y1": 753, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 754, + "mark": null, + "x": 754, + "y0": 0, + "y1": 754, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 755, + "mark": null, + "x": 755, + "y0": 0, + "y1": 755, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 756, + "mark": null, + "x": 756, + "y0": 0, + "y1": 756, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 757, + "mark": null, + "x": 757, + "y0": 0, + "y1": 757, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 758, + "mark": null, + "x": 758, + "y0": 0, + "y1": 758, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 759, + "mark": null, + "x": 759, + "y0": 0, + "y1": 759, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 760, + "mark": null, + "x": 760, + "y0": 0, + "y1": 760, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 761, + "mark": null, + "x": 761, + "y0": 0, + "y1": 761, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 762, + "mark": null, + "x": 762, + "y0": 0, + "y1": 762, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 763, + "mark": null, + "x": 763, + "y0": 0, + "y1": 763, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 764, + "mark": null, + "x": 764, + "y0": 0, + "y1": 764, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 765, + "mark": null, + "x": 765, + "y0": 0, + "y1": 765, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 766, + "mark": null, + "x": 766, + "y0": 0, + "y1": 766, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 767, + "mark": null, + "x": 767, + "y0": 0, + "y1": 767, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 768, + "mark": null, + "x": 768, + "y0": 0, + "y1": 768, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 769, + "mark": null, + "x": 769, + "y0": 0, + "y1": 769, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 770, + "mark": null, + "x": 770, + "y0": 0, + "y1": 770, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 771, + "mark": null, + "x": 771, + "y0": 0, + "y1": 771, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 772, + "mark": null, + "x": 772, + "y0": 0, + "y1": 772, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 773, + "mark": null, + "x": 773, + "y0": 0, + "y1": 773, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 774, + "mark": null, + "x": 774, + "y0": 0, + "y1": 774, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 775, + "mark": null, + "x": 775, + "y0": 0, + "y1": 775, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 776, + "mark": null, + "x": 776, + "y0": 0, + "y1": 776, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 777, + "mark": null, + "x": 777, + "y0": 0, + "y1": 777, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 778, + "mark": null, + "x": 778, + "y0": 0, + "y1": 778, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 779, + "mark": null, + "x": 779, + "y0": 0, + "y1": 779, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 780, + "mark": null, + "x": 780, + "y0": 0, + "y1": 780, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 781, + "mark": null, + "x": 781, + "y0": 0, + "y1": 781, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 782, + "mark": null, + "x": 782, + "y0": 0, + "y1": 782, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 783, + "mark": null, + "x": 783, + "y0": 0, + "y1": 783, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 784, + "mark": null, + "x": 784, + "y0": 0, + "y1": 784, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 785, + "mark": null, + "x": 785, + "y0": 0, + "y1": 785, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 786, + "mark": null, + "x": 786, + "y0": 0, + "y1": 786, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 787, + "mark": null, + "x": 787, + "y0": 0, + "y1": 787, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 788, + "mark": null, + "x": 788, + "y0": 0, + "y1": 788, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 789, + "mark": null, + "x": 789, + "y0": 0, + "y1": 789, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 790, + "mark": null, + "x": 790, + "y0": 0, + "y1": 790, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 791, + "mark": null, + "x": 791, + "y0": 0, + "y1": 791, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 792, + "mark": null, + "x": 792, + "y0": 0, + "y1": 792, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 793, + "mark": null, + "x": 793, + "y0": 0, + "y1": 793, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 794, + "mark": null, + "x": 794, + "y0": 0, + "y1": 794, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 795, + "mark": null, + "x": 795, + "y0": 0, + "y1": 795, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 796, + "mark": null, + "x": 796, + "y0": 0, + "y1": 796, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 797, + "mark": null, + "x": 797, + "y0": 0, + "y1": 797, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 798, + "mark": null, + "x": 798, + "y0": 0, + "y1": 798, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 799, + "mark": null, + "x": 799, + "y0": 0, + "y1": 799, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 800, + "mark": null, + "x": 800, + "y0": 0, + "y1": 800, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 801, + "mark": null, + "x": 801, + "y0": 0, + "y1": 801, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 802, + "mark": null, + "x": 802, + "y0": 0, + "y1": 802, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 803, + "mark": null, + "x": 803, + "y0": 0, + "y1": 803, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 804, + "mark": null, + "x": 804, + "y0": 0, + "y1": 804, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 805, + "mark": null, + "x": 805, + "y0": 0, + "y1": 805, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 806, + "mark": null, + "x": 806, + "y0": 0, + "y1": 806, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 807, + "mark": null, + "x": 807, + "y0": 0, + "y1": 807, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 808, + "mark": null, + "x": 808, + "y0": 0, + "y1": 808, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 809, + "mark": null, + "x": 809, + "y0": 0, + "y1": 809, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 810, + "mark": null, + "x": 810, + "y0": 0, + "y1": 810, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 811, + "mark": null, + "x": 811, + "y0": 0, + "y1": 811, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 812, + "mark": null, + "x": 812, + "y0": 0, + "y1": 812, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 813, + "mark": null, + "x": 813, + "y0": 0, + "y1": 813, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 814, + "mark": null, + "x": 814, + "y0": 0, + "y1": 814, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 815, + "mark": null, + "x": 815, + "y0": 0, + "y1": 815, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 816, + "mark": null, + "x": 816, + "y0": 0, + "y1": 816, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 817, + "mark": null, + "x": 817, + "y0": 0, + "y1": 817, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 818, + "mark": null, + "x": 818, + "y0": 0, + "y1": 818, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 819, + "mark": null, + "x": 819, + "y0": 0, + "y1": 819, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 820, + "mark": null, + "x": 820, + "y0": 0, + "y1": 820, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 821, + "mark": null, + "x": 821, + "y0": 0, + "y1": 821, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 822, + "mark": null, + "x": 822, + "y0": 0, + "y1": 822, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 823, + "mark": null, + "x": 823, + "y0": 0, + "y1": 823, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 824, + "mark": null, + "x": 824, + "y0": 0, + "y1": 824, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 825, + "mark": null, + "x": 825, + "y0": 0, + "y1": 825, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 826, + "mark": null, + "x": 826, + "y0": 0, + "y1": 826, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 827, + "mark": null, + "x": 827, + "y0": 0, + "y1": 827, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 828, + "mark": null, + "x": 828, + "y0": 0, + "y1": 828, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 829, + "mark": null, + "x": 829, + "y0": 0, + "y1": 829, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 830, + "mark": null, + "x": 830, + "y0": 0, + "y1": 830, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 831, + "mark": null, + "x": 831, + "y0": 0, + "y1": 831, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 832, + "mark": null, + "x": 832, + "y0": 0, + "y1": 832, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 833, + "mark": null, + "x": 833, + "y0": 0, + "y1": 833, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 834, + "mark": null, + "x": 834, + "y0": 0, + "y1": 834, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 835, + "mark": null, + "x": 835, + "y0": 0, + "y1": 835, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 836, + "mark": null, + "x": 836, + "y0": 0, + "y1": 836, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 837, + "mark": null, + "x": 837, + "y0": 0, + "y1": 837, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 838, + "mark": null, + "x": 838, + "y0": 0, + "y1": 838, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 839, + "mark": null, + "x": 839, + "y0": 0, + "y1": 839, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 840, + "mark": null, + "x": 840, + "y0": 0, + "y1": 840, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 841, + "mark": null, + "x": 841, + "y0": 0, + "y1": 841, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 842, + "mark": null, + "x": 842, + "y0": 0, + "y1": 842, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 843, + "mark": null, + "x": 843, + "y0": 0, + "y1": 843, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 844, + "mark": null, + "x": 844, + "y0": 0, + "y1": 844, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 845, + "mark": null, + "x": 845, + "y0": 0, + "y1": 845, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 846, + "mark": null, + "x": 846, + "y0": 0, + "y1": 846, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 847, + "mark": null, + "x": 847, + "y0": 0, + "y1": 847, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 848, + "mark": null, + "x": 848, + "y0": 0, + "y1": 848, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 849, + "mark": null, + "x": 849, + "y0": 0, + "y1": 849, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 850, + "mark": null, + "x": 850, + "y0": 0, + "y1": 850, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 851, + "mark": null, + "x": 851, + "y0": 0, + "y1": 851, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 852, + "mark": null, + "x": 852, + "y0": 0, + "y1": 852, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 853, + "mark": null, + "x": 853, + "y0": 0, + "y1": 853, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 854, + "mark": null, + "x": 854, + "y0": 0, + "y1": 854, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 855, + "mark": null, + "x": 855, + "y0": 0, + "y1": 855, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 856, + "mark": null, + "x": 856, + "y0": 0, + "y1": 856, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 857, + "mark": null, + "x": 857, + "y0": 0, + "y1": 857, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 858, + "mark": null, + "x": 858, + "y0": 0, + "y1": 858, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 859, + "mark": null, + "x": 859, + "y0": 0, + "y1": 859, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 860, + "mark": null, + "x": 860, + "y0": 0, + "y1": 860, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 861, + "mark": null, + "x": 861, + "y0": 0, + "y1": 861, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 862, + "mark": null, + "x": 862, + "y0": 0, + "y1": 862, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 863, + "mark": null, + "x": 863, + "y0": 0, + "y1": 863, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 864, + "mark": null, + "x": 864, + "y0": 0, + "y1": 864, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 865, + "mark": null, + "x": 865, + "y0": 0, + "y1": 865, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 866, + "mark": null, + "x": 866, + "y0": 0, + "y1": 866, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 867, + "mark": null, + "x": 867, + "y0": 0, + "y1": 867, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 868, + "mark": null, + "x": 868, + "y0": 0, + "y1": 868, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 869, + "mark": null, + "x": 869, + "y0": 0, + "y1": 869, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 870, + "mark": null, + "x": 870, + "y0": 0, + "y1": 870, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 871, + "mark": null, + "x": 871, + "y0": 0, + "y1": 871, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 872, + "mark": null, + "x": 872, + "y0": 0, + "y1": 872, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 873, + "mark": null, + "x": 873, + "y0": 0, + "y1": 873, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 874, + "mark": null, + "x": 874, + "y0": 0, + "y1": 874, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 875, + "mark": null, + "x": 875, + "y0": 0, + "y1": 875, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 876, + "mark": null, + "x": 876, + "y0": 0, + "y1": 876, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 877, + "mark": null, + "x": 877, + "y0": 0, + "y1": 877, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 878, + "mark": null, + "x": 878, + "y0": 0, + "y1": 878, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 879, + "mark": null, + "x": 879, + "y0": 0, + "y1": 879, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 880, + "mark": null, + "x": 880, + "y0": 0, + "y1": 880, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 881, + "mark": null, + "x": 881, + "y0": 0, + "y1": 881, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 882, + "mark": null, + "x": 882, + "y0": 0, + "y1": 882, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 883, + "mark": null, + "x": 883, + "y0": 0, + "y1": 883, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 884, + "mark": null, + "x": 884, + "y0": 0, + "y1": 884, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 885, + "mark": null, + "x": 885, + "y0": 0, + "y1": 885, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 886, + "mark": null, + "x": 886, + "y0": 0, + "y1": 886, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 887, + "mark": null, + "x": 887, + "y0": 0, + "y1": 887, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 888, + "mark": null, + "x": 888, + "y0": 0, + "y1": 888, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 889, + "mark": null, + "x": 889, + "y0": 0, + "y1": 889, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 890, + "mark": null, + "x": 890, + "y0": 0, + "y1": 890, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 891, + "mark": null, + "x": 891, + "y0": 0, + "y1": 891, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 892, + "mark": null, + "x": 892, + "y0": 0, + "y1": 892, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 893, + "mark": null, + "x": 893, + "y0": 0, + "y1": 893, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 894, + "mark": null, + "x": 894, + "y0": 0, + "y1": 894, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 895, + "mark": null, + "x": 895, + "y0": 0, + "y1": 895, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 896, + "mark": null, + "x": 896, + "y0": 0, + "y1": 896, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 897, + "mark": null, + "x": 897, + "y0": 0, + "y1": 897, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 898, + "mark": null, + "x": 898, + "y0": 0, + "y1": 898, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 899, + "mark": null, + "x": 899, + "y0": 0, + "y1": 899, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 900, + "mark": null, + "x": 900, + "y0": 0, + "y1": 900, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 901, + "mark": null, + "x": 901, + "y0": 0, + "y1": 901, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 902, + "mark": null, + "x": 902, + "y0": 0, + "y1": 902, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 903, + "mark": null, + "x": 903, + "y0": 0, + "y1": 903, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 904, + "mark": null, + "x": 904, + "y0": 0, + "y1": 904, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 905, + "mark": null, + "x": 905, + "y0": 0, + "y1": 905, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 906, + "mark": null, + "x": 906, + "y0": 0, + "y1": 906, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 907, + "mark": null, + "x": 907, + "y0": 0, + "y1": 907, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 908, + "mark": null, + "x": 908, + "y0": 0, + "y1": 908, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 909, + "mark": null, + "x": 909, + "y0": 0, + "y1": 909, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 910, + "mark": null, + "x": 910, + "y0": 0, + "y1": 910, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 911, + "mark": null, + "x": 911, + "y0": 0, + "y1": 911, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 912, + "mark": null, + "x": 912, + "y0": 0, + "y1": 912, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 913, + "mark": null, + "x": 913, + "y0": 0, + "y1": 913, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 914, + "mark": null, + "x": 914, + "y0": 0, + "y1": 914, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 915, + "mark": null, + "x": 915, + "y0": 0, + "y1": 915, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 916, + "mark": null, + "x": 916, + "y0": 0, + "y1": 916, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 917, + "mark": null, + "x": 917, + "y0": 0, + "y1": 917, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 918, + "mark": null, + "x": 918, + "y0": 0, + "y1": 918, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 919, + "mark": null, + "x": 919, + "y0": 0, + "y1": 919, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 920, + "mark": null, + "x": 920, + "y0": 0, + "y1": 920, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 921, + "mark": null, + "x": 921, + "y0": 0, + "y1": 921, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 922, + "mark": null, + "x": 922, + "y0": 0, + "y1": 922, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 923, + "mark": null, + "x": 923, + "y0": 0, + "y1": 923, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 924, + "mark": null, + "x": 924, + "y0": 0, + "y1": 924, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 925, + "mark": null, + "x": 925, + "y0": 0, + "y1": 925, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 926, + "mark": null, + "x": 926, + "y0": 0, + "y1": 926, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 927, + "mark": null, + "x": 927, + "y0": 0, + "y1": 927, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 928, + "mark": null, + "x": 928, + "y0": 0, + "y1": 928, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 929, + "mark": null, + "x": 929, + "y0": 0, + "y1": 929, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 930, + "mark": null, + "x": 930, + "y0": 0, + "y1": 930, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 931, + "mark": null, + "x": 931, + "y0": 0, + "y1": 931, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 932, + "mark": null, + "x": 932, + "y0": 0, + "y1": 932, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 933, + "mark": null, + "x": 933, + "y0": 0, + "y1": 933, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 934, + "mark": null, + "x": 934, + "y0": 0, + "y1": 934, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 935, + "mark": null, + "x": 935, + "y0": 0, + "y1": 935, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 936, + "mark": null, + "x": 936, + "y0": 0, + "y1": 936, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 937, + "mark": null, + "x": 937, + "y0": 0, + "y1": 937, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 938, + "mark": null, + "x": 938, + "y0": 0, + "y1": 938, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 939, + "mark": null, + "x": 939, + "y0": 0, + "y1": 939, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 940, + "mark": null, + "x": 940, + "y0": 0, + "y1": 940, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 941, + "mark": null, + "x": 941, + "y0": 0, + "y1": 941, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 942, + "mark": null, + "x": 942, + "y0": 0, + "y1": 942, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 943, + "mark": null, + "x": 943, + "y0": 0, + "y1": 943, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 944, + "mark": null, + "x": 944, + "y0": 0, + "y1": 944, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 945, + "mark": null, + "x": 945, + "y0": 0, + "y1": 945, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 946, + "mark": null, + "x": 946, + "y0": 0, + "y1": 946, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 947, + "mark": null, + "x": 947, + "y0": 0, + "y1": 947, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 948, + "mark": null, + "x": 948, + "y0": 0, + "y1": 948, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 949, + "mark": null, + "x": 949, + "y0": 0, + "y1": 949, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 950, + "mark": null, + "x": 950, + "y0": 0, + "y1": 950, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 951, + "mark": null, + "x": 951, + "y0": 0, + "y1": 951, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 952, + "mark": null, + "x": 952, + "y0": 0, + "y1": 952, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 953, + "mark": null, + "x": 953, + "y0": 0, + "y1": 953, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 954, + "mark": null, + "x": 954, + "y0": 0, + "y1": 954, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 955, + "mark": null, + "x": 955, + "y0": 0, + "y1": 955, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 956, + "mark": null, + "x": 956, + "y0": 0, + "y1": 956, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 957, + "mark": null, + "x": 957, + "y0": 0, + "y1": 957, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 958, + "mark": null, + "x": 958, + "y0": 0, + "y1": 958, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 959, + "mark": null, + "x": 959, + "y0": 0, + "y1": 959, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 960, + "mark": null, + "x": 960, + "y0": 0, + "y1": 960, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 961, + "mark": null, + "x": 961, + "y0": 0, + "y1": 961, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 962, + "mark": null, + "x": 962, + "y0": 0, + "y1": 962, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 963, + "mark": null, + "x": 963, + "y0": 0, + "y1": 963, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 964, + "mark": null, + "x": 964, + "y0": 0, + "y1": 964, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 965, + "mark": null, + "x": 965, + "y0": 0, + "y1": 965, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 966, + "mark": null, + "x": 966, + "y0": 0, + "y1": 966, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 967, + "mark": null, + "x": 967, + "y0": 0, + "y1": 967, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 968, + "mark": null, + "x": 968, + "y0": 0, + "y1": 968, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 969, + "mark": null, + "x": 969, + "y0": 0, + "y1": 969, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 970, + "mark": null, + "x": 970, + "y0": 0, + "y1": 970, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 971, + "mark": null, + "x": 971, + "y0": 0, + "y1": 971, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 972, + "mark": null, + "x": 972, + "y0": 0, + "y1": 972, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 973, + "mark": null, + "x": 973, + "y0": 0, + "y1": 973, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 974, + "mark": null, + "x": 974, + "y0": 0, + "y1": 974, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 975, + "mark": null, + "x": 975, + "y0": 0, + "y1": 975, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 976, + "mark": null, + "x": 976, + "y0": 0, + "y1": 976, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 977, + "mark": null, + "x": 977, + "y0": 0, + "y1": 977, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 978, + "mark": null, + "x": 978, + "y0": 0, + "y1": 978, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 979, + "mark": null, + "x": 979, + "y0": 0, + "y1": 979, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 980, + "mark": null, + "x": 980, + "y0": 0, + "y1": 980, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 981, + "mark": null, + "x": 981, + "y0": 0, + "y1": 981, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 982, + "mark": null, + "x": 982, + "y0": 0, + "y1": 982, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 983, + "mark": null, + "x": 983, + "y0": 0, + "y1": 983, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 984, + "mark": null, + "x": 984, + "y0": 0, + "y1": 984, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 985, + "mark": null, + "x": 985, + "y0": 0, + "y1": 985, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 986, + "mark": null, + "x": 986, + "y0": 0, + "y1": 986, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 987, + "mark": null, + "x": 987, + "y0": 0, + "y1": 987, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 988, + "mark": null, + "x": 988, + "y0": 0, + "y1": 988, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 989, + "mark": null, + "x": 989, + "y0": 0, + "y1": 989, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 990, + "mark": null, + "x": 990, + "y0": 0, + "y1": 990, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 991, + "mark": null, + "x": 991, + "y0": 0, + "y1": 991, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 992, + "mark": null, + "x": 992, + "y0": 0, + "y1": 992, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 993, + "mark": null, + "x": 993, + "y0": 0, + "y1": 993, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 994, + "mark": null, + "x": 994, + "y0": 0, + "y1": 994, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 995, + "mark": null, + "x": 995, + "y0": 0, + "y1": 995, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 996, + "mark": null, + "x": 996, + "y0": 0, + "y1": 996, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 997, + "mark": null, + "x": 997, + "y0": 0, + "y1": 997, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 998, + "mark": null, + "x": 998, + "y0": 0, + "y1": 998, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 999, + "mark": null, + "x": 999, + "y0": 0, + "y1": 999, + }, + ], + "key": "a", + "seriesKeys": Array [ + "a", + ], + "specId": "spec1", + "splitAccessors": Map {}, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 0, + "mark": null, + "x": 0, + "y0": 0, + "y1": 0, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 1, + "mark": null, + "x": 1, + "y0": 1, + "y1": 2, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 2, + "y0": 2, + "y1": 4, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 3, + "mark": null, + "x": 3, + "y0": 3, + "y1": 6, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 4, + "mark": null, + "x": 4, + "y0": 4, + "y1": 8, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 5, + "mark": null, + "x": 5, + "y0": 5, + "y1": 10, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 6, + "mark": null, + "x": 6, + "y0": 6, + "y1": 12, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 7, + "mark": null, + "x": 7, + "y0": 7, + "y1": 14, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 8, + "mark": null, + "x": 8, + "y0": 8, + "y1": 16, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 9, + "mark": null, + "x": 9, + "y0": 9, + "y1": 18, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 10, + "mark": null, + "x": 10, + "y0": 10, + "y1": 20, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 11, + "mark": null, + "x": 11, + "y0": 11, + "y1": 22, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 12, + "mark": null, + "x": 12, + "y0": 12, + "y1": 24, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 13, + "mark": null, + "x": 13, + "y0": 13, + "y1": 26, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 14, + "mark": null, + "x": 14, + "y0": 14, + "y1": 28, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 15, + "mark": null, + "x": 15, + "y0": 15, + "y1": 30, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 16, + "mark": null, + "x": 16, + "y0": 16, + "y1": 32, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 17, + "mark": null, + "x": 17, + "y0": 17, + "y1": 34, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 18, + "mark": null, + "x": 18, + "y0": 18, + "y1": 36, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 19, + "mark": null, + "x": 19, + "y0": 19, + "y1": 38, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 20, + "mark": null, + "x": 20, + "y0": 20, + "y1": 40, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 21, + "mark": null, + "x": 21, + "y0": 21, + "y1": 42, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 22, + "mark": null, + "x": 22, + "y0": 22, + "y1": 44, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 23, + "mark": null, + "x": 23, + "y0": 23, + "y1": 46, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 24, + "mark": null, + "x": 24, + "y0": 24, + "y1": 48, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 25, + "mark": null, + "x": 25, + "y0": 25, + "y1": 50, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 26, + "mark": null, + "x": 26, + "y0": 26, + "y1": 52, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 27, + "mark": null, + "x": 27, + "y0": 27, + "y1": 54, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 28, + "mark": null, + "x": 28, + "y0": 28, + "y1": 56, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 29, + "mark": null, + "x": 29, + "y0": 29, + "y1": 58, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 30, + "mark": null, + "x": 30, + "y0": 30, + "y1": 60, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 31, + "mark": null, + "x": 31, + "y0": 31, + "y1": 62, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 32, + "mark": null, + "x": 32, + "y0": 32, + "y1": 64, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 33, + "mark": null, + "x": 33, + "y0": 33, + "y1": 66, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 34, + "mark": null, + "x": 34, + "y0": 34, + "y1": 68, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 35, + "mark": null, + "x": 35, + "y0": 35, + "y1": 70, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 36, + "mark": null, + "x": 36, + "y0": 36, + "y1": 72, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 37, + "mark": null, + "x": 37, + "y0": 37, + "y1": 74, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 38, + "mark": null, + "x": 38, + "y0": 38, + "y1": 76, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 39, + "mark": null, + "x": 39, + "y0": 39, + "y1": 78, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 40, + "mark": null, + "x": 40, + "y0": 40, + "y1": 80, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 41, + "mark": null, + "x": 41, + "y0": 41, + "y1": 82, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 42, + "mark": null, + "x": 42, + "y0": 42, + "y1": 84, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 43, + "mark": null, + "x": 43, + "y0": 43, + "y1": 86, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 44, + "mark": null, + "x": 44, + "y0": 44, + "y1": 88, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 45, + "mark": null, + "x": 45, + "y0": 45, + "y1": 90, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 46, + "mark": null, + "x": 46, + "y0": 46, + "y1": 92, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 47, + "mark": null, + "x": 47, + "y0": 47, + "y1": 94, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 48, + "mark": null, + "x": 48, + "y0": 48, + "y1": 96, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 49, + "mark": null, + "x": 49, + "y0": 49, + "y1": 98, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 50, + "mark": null, + "x": 50, + "y0": 50, + "y1": 100, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 51, + "mark": null, + "x": 51, + "y0": 51, + "y1": 102, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 52, + "mark": null, + "x": 52, + "y0": 52, + "y1": 104, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 53, + "mark": null, + "x": 53, + "y0": 53, + "y1": 106, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 54, + "mark": null, + "x": 54, + "y0": 54, + "y1": 108, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 55, + "mark": null, + "x": 55, + "y0": 55, + "y1": 110, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 56, + "mark": null, + "x": 56, + "y0": 56, + "y1": 112, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 57, + "mark": null, + "x": 57, + "y0": 57, + "y1": 114, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 58, + "mark": null, + "x": 58, + "y0": 58, + "y1": 116, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 59, + "mark": null, + "x": 59, + "y0": 59, + "y1": 118, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 60, + "mark": null, + "x": 60, + "y0": 60, + "y1": 120, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 61, + "mark": null, + "x": 61, + "y0": 61, + "y1": 122, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 62, + "mark": null, + "x": 62, + "y0": 62, + "y1": 124, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 63, + "mark": null, + "x": 63, + "y0": 63, + "y1": 126, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 64, + "mark": null, + "x": 64, + "y0": 64, + "y1": 128, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 65, + "mark": null, + "x": 65, + "y0": 65, + "y1": 130, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 66, + "mark": null, + "x": 66, + "y0": 66, + "y1": 132, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 67, + "mark": null, + "x": 67, + "y0": 67, + "y1": 134, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 68, + "mark": null, + "x": 68, + "y0": 68, + "y1": 136, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 69, + "mark": null, + "x": 69, + "y0": 69, + "y1": 138, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 70, + "mark": null, + "x": 70, + "y0": 70, + "y1": 140, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 71, + "mark": null, + "x": 71, + "y0": 71, + "y1": 142, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 72, + "mark": null, + "x": 72, + "y0": 72, + "y1": 144, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 73, + "mark": null, + "x": 73, + "y0": 73, + "y1": 146, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 74, + "mark": null, + "x": 74, + "y0": 74, + "y1": 148, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 75, + "mark": null, + "x": 75, + "y0": 75, + "y1": 150, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 76, + "mark": null, + "x": 76, + "y0": 76, + "y1": 152, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 77, + "mark": null, + "x": 77, + "y0": 77, + "y1": 154, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 78, + "mark": null, + "x": 78, + "y0": 78, + "y1": 156, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 79, + "mark": null, + "x": 79, + "y0": 79, + "y1": 158, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 80, + "mark": null, + "x": 80, + "y0": 80, + "y1": 160, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 81, + "mark": null, + "x": 81, + "y0": 81, + "y1": 162, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 82, + "mark": null, + "x": 82, + "y0": 82, + "y1": 164, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 83, + "mark": null, + "x": 83, + "y0": 83, + "y1": 166, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 84, + "mark": null, + "x": 84, + "y0": 84, + "y1": 168, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 85, + "mark": null, + "x": 85, + "y0": 85, + "y1": 170, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 86, + "mark": null, + "x": 86, + "y0": 86, + "y1": 172, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 87, + "mark": null, + "x": 87, + "y0": 87, + "y1": 174, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 88, + "mark": null, + "x": 88, + "y0": 88, + "y1": 176, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 89, + "mark": null, + "x": 89, + "y0": 89, + "y1": 178, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 90, + "mark": null, + "x": 90, + "y0": 90, + "y1": 180, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 91, + "mark": null, + "x": 91, + "y0": 91, + "y1": 182, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 92, + "mark": null, + "x": 92, + "y0": 92, + "y1": 184, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 93, + "mark": null, + "x": 93, + "y0": 93, + "y1": 186, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 94, + "mark": null, + "x": 94, + "y0": 94, + "y1": 188, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 95, + "mark": null, + "x": 95, + "y0": 95, + "y1": 190, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 96, + "mark": null, + "x": 96, + "y0": 96, + "y1": 192, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 97, + "mark": null, + "x": 97, + "y0": 97, + "y1": 194, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 98, + "mark": null, + "x": 98, + "y0": 98, + "y1": 196, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 99, + "mark": null, + "x": 99, + "y0": 99, + "y1": 198, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 100, + "mark": null, + "x": 100, + "y0": 100, + "y1": 200, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 101, + "mark": null, + "x": 101, + "y0": 101, + "y1": 202, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 102, + "mark": null, + "x": 102, + "y0": 102, + "y1": 204, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 103, + "mark": null, + "x": 103, + "y0": 103, + "y1": 206, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 104, + "mark": null, + "x": 104, + "y0": 104, + "y1": 208, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 105, + "mark": null, + "x": 105, + "y0": 105, + "y1": 210, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 106, + "mark": null, + "x": 106, + "y0": 106, + "y1": 212, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 107, + "mark": null, + "x": 107, + "y0": 107, + "y1": 214, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 108, + "mark": null, + "x": 108, + "y0": 108, + "y1": 216, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 109, + "mark": null, + "x": 109, + "y0": 109, + "y1": 218, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 110, + "mark": null, + "x": 110, + "y0": 110, + "y1": 220, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 111, + "mark": null, + "x": 111, + "y0": 111, + "y1": 222, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 112, + "mark": null, + "x": 112, + "y0": 112, + "y1": 224, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 113, + "mark": null, + "x": 113, + "y0": 113, + "y1": 226, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 114, + "mark": null, + "x": 114, + "y0": 114, + "y1": 228, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 115, + "mark": null, + "x": 115, + "y0": 115, + "y1": 230, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 116, + "mark": null, + "x": 116, + "y0": 116, + "y1": 232, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 117, + "mark": null, + "x": 117, + "y0": 117, + "y1": 234, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 118, + "mark": null, + "x": 118, + "y0": 118, + "y1": 236, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 119, + "mark": null, + "x": 119, + "y0": 119, + "y1": 238, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 120, + "mark": null, + "x": 120, + "y0": 120, + "y1": 240, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 121, + "mark": null, + "x": 121, + "y0": 121, + "y1": 242, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 122, + "mark": null, + "x": 122, + "y0": 122, + "y1": 244, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 123, + "mark": null, + "x": 123, + "y0": 123, + "y1": 246, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 124, + "mark": null, + "x": 124, + "y0": 124, + "y1": 248, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 125, + "mark": null, + "x": 125, + "y0": 125, + "y1": 250, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 126, + "mark": null, + "x": 126, + "y0": 126, + "y1": 252, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 127, + "mark": null, + "x": 127, + "y0": 127, + "y1": 254, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 128, + "mark": null, + "x": 128, + "y0": 128, + "y1": 256, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 129, + "mark": null, + "x": 129, + "y0": 129, + "y1": 258, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 130, + "mark": null, + "x": 130, + "y0": 130, + "y1": 260, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 131, + "mark": null, + "x": 131, + "y0": 131, + "y1": 262, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 132, + "mark": null, + "x": 132, + "y0": 132, + "y1": 264, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 133, + "mark": null, + "x": 133, + "y0": 133, + "y1": 266, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 134, + "mark": null, + "x": 134, + "y0": 134, + "y1": 268, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 135, + "mark": null, + "x": 135, + "y0": 135, + "y1": 270, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 136, + "mark": null, + "x": 136, + "y0": 136, + "y1": 272, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 137, + "mark": null, + "x": 137, + "y0": 137, + "y1": 274, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 138, + "mark": null, + "x": 138, + "y0": 138, + "y1": 276, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 139, + "mark": null, + "x": 139, + "y0": 139, + "y1": 278, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 140, + "mark": null, + "x": 140, + "y0": 140, + "y1": 280, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 141, + "mark": null, + "x": 141, + "y0": 141, + "y1": 282, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 142, + "mark": null, + "x": 142, + "y0": 142, + "y1": 284, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 143, + "mark": null, + "x": 143, + "y0": 143, + "y1": 286, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 144, + "mark": null, + "x": 144, + "y0": 144, + "y1": 288, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 145, + "mark": null, + "x": 145, + "y0": 145, + "y1": 290, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 146, + "mark": null, + "x": 146, + "y0": 146, + "y1": 292, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 147, + "mark": null, + "x": 147, + "y0": 147, + "y1": 294, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 148, + "mark": null, + "x": 148, + "y0": 148, + "y1": 296, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 149, + "mark": null, + "x": 149, + "y0": 149, + "y1": 298, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 150, + "mark": null, + "x": 150, + "y0": 150, + "y1": 300, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 151, + "mark": null, + "x": 151, + "y0": 151, + "y1": 302, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 152, + "mark": null, + "x": 152, + "y0": 152, + "y1": 304, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 153, + "mark": null, + "x": 153, + "y0": 153, + "y1": 306, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 154, + "mark": null, + "x": 154, + "y0": 154, + "y1": 308, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 155, + "mark": null, + "x": 155, + "y0": 155, + "y1": 310, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 156, + "mark": null, + "x": 156, + "y0": 156, + "y1": 312, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 157, + "mark": null, + "x": 157, + "y0": 157, + "y1": 314, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 158, + "mark": null, + "x": 158, + "y0": 158, + "y1": 316, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 159, + "mark": null, + "x": 159, + "y0": 159, + "y1": 318, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 160, + "mark": null, + "x": 160, + "y0": 160, + "y1": 320, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 161, + "mark": null, + "x": 161, + "y0": 161, + "y1": 322, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 162, + "mark": null, + "x": 162, + "y0": 162, + "y1": 324, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 163, + "mark": null, + "x": 163, + "y0": 163, + "y1": 326, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 164, + "mark": null, + "x": 164, + "y0": 164, + "y1": 328, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 165, + "mark": null, + "x": 165, + "y0": 165, + "y1": 330, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 166, + "mark": null, + "x": 166, + "y0": 166, + "y1": 332, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 167, + "mark": null, + "x": 167, + "y0": 167, + "y1": 334, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 168, + "mark": null, + "x": 168, + "y0": 168, + "y1": 336, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 169, + "mark": null, + "x": 169, + "y0": 169, + "y1": 338, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 170, + "mark": null, + "x": 170, + "y0": 170, + "y1": 340, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 171, + "mark": null, + "x": 171, + "y0": 171, + "y1": 342, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 172, + "mark": null, + "x": 172, + "y0": 172, + "y1": 344, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 173, + "mark": null, + "x": 173, + "y0": 173, + "y1": 346, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 174, + "mark": null, + "x": 174, + "y0": 174, + "y1": 348, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 175, + "mark": null, + "x": 175, + "y0": 175, + "y1": 350, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 176, + "mark": null, + "x": 176, + "y0": 176, + "y1": 352, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 177, + "mark": null, + "x": 177, + "y0": 177, + "y1": 354, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 178, + "mark": null, + "x": 178, + "y0": 178, + "y1": 356, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 179, + "mark": null, + "x": 179, + "y0": 179, + "y1": 358, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 180, + "mark": null, + "x": 180, + "y0": 180, + "y1": 360, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 181, + "mark": null, + "x": 181, + "y0": 181, + "y1": 362, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 182, + "mark": null, + "x": 182, + "y0": 182, + "y1": 364, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 183, + "mark": null, + "x": 183, + "y0": 183, + "y1": 366, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 184, + "mark": null, + "x": 184, + "y0": 184, + "y1": 368, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 185, + "mark": null, + "x": 185, + "y0": 185, + "y1": 370, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 186, + "mark": null, + "x": 186, + "y0": 186, + "y1": 372, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 187, + "mark": null, + "x": 187, + "y0": 187, + "y1": 374, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 188, + "mark": null, + "x": 188, + "y0": 188, + "y1": 376, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 189, + "mark": null, + "x": 189, + "y0": 189, + "y1": 378, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 190, + "mark": null, + "x": 190, + "y0": 190, + "y1": 380, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 191, + "mark": null, + "x": 191, + "y0": 191, + "y1": 382, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 192, + "mark": null, + "x": 192, + "y0": 192, + "y1": 384, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 193, + "mark": null, + "x": 193, + "y0": 193, + "y1": 386, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 194, + "mark": null, + "x": 194, + "y0": 194, + "y1": 388, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 195, + "mark": null, + "x": 195, + "y0": 195, + "y1": 390, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 196, + "mark": null, + "x": 196, + "y0": 196, + "y1": 392, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 197, + "mark": null, + "x": 197, + "y0": 197, + "y1": 394, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 198, + "mark": null, + "x": 198, + "y0": 198, + "y1": 396, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 199, + "mark": null, + "x": 199, + "y0": 199, + "y1": 398, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 200, + "mark": null, + "x": 200, + "y0": 200, + "y1": 400, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 201, + "mark": null, + "x": 201, + "y0": 201, + "y1": 402, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 202, + "mark": null, + "x": 202, + "y0": 202, + "y1": 404, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 203, + "mark": null, + "x": 203, + "y0": 203, + "y1": 406, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 204, + "mark": null, + "x": 204, + "y0": 204, + "y1": 408, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 205, + "mark": null, + "x": 205, + "y0": 205, + "y1": 410, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 206, + "mark": null, + "x": 206, + "y0": 206, + "y1": 412, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 207, + "mark": null, + "x": 207, + "y0": 207, + "y1": 414, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 208, + "mark": null, + "x": 208, + "y0": 208, + "y1": 416, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 209, + "mark": null, + "x": 209, + "y0": 209, + "y1": 418, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 210, + "mark": null, + "x": 210, + "y0": 210, + "y1": 420, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 211, + "mark": null, + "x": 211, + "y0": 211, + "y1": 422, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 212, + "mark": null, + "x": 212, + "y0": 212, + "y1": 424, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 213, + "mark": null, + "x": 213, + "y0": 213, + "y1": 426, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 214, + "mark": null, + "x": 214, + "y0": 214, + "y1": 428, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 215, + "mark": null, + "x": 215, + "y0": 215, + "y1": 430, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 216, + "mark": null, + "x": 216, + "y0": 216, + "y1": 432, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 217, + "mark": null, + "x": 217, + "y0": 217, + "y1": 434, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 218, + "mark": null, + "x": 218, + "y0": 218, + "y1": 436, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 219, + "mark": null, + "x": 219, + "y0": 219, + "y1": 438, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 220, + "mark": null, + "x": 220, + "y0": 220, + "y1": 440, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 221, + "mark": null, + "x": 221, + "y0": 221, + "y1": 442, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 222, + "mark": null, + "x": 222, + "y0": 222, + "y1": 444, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 223, + "mark": null, + "x": 223, + "y0": 223, + "y1": 446, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 224, + "mark": null, + "x": 224, + "y0": 224, + "y1": 448, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 225, + "mark": null, + "x": 225, + "y0": 225, + "y1": 450, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 226, + "mark": null, + "x": 226, + "y0": 226, + "y1": 452, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 227, + "mark": null, + "x": 227, + "y0": 227, + "y1": 454, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 228, + "mark": null, + "x": 228, + "y0": 228, + "y1": 456, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 229, + "mark": null, + "x": 229, + "y0": 229, + "y1": 458, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 230, + "mark": null, + "x": 230, + "y0": 230, + "y1": 460, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 231, + "mark": null, + "x": 231, + "y0": 231, + "y1": 462, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 232, + "mark": null, + "x": 232, + "y0": 232, + "y1": 464, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 233, + "mark": null, + "x": 233, + "y0": 233, + "y1": 466, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 234, + "mark": null, + "x": 234, + "y0": 234, + "y1": 468, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 235, + "mark": null, + "x": 235, + "y0": 235, + "y1": 470, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 236, + "mark": null, + "x": 236, + "y0": 236, + "y1": 472, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 237, + "mark": null, + "x": 237, + "y0": 237, + "y1": 474, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 238, + "mark": null, + "x": 238, + "y0": 238, + "y1": 476, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 239, + "mark": null, + "x": 239, + "y0": 239, + "y1": 478, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 240, + "mark": null, + "x": 240, + "y0": 240, + "y1": 480, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 241, + "mark": null, + "x": 241, + "y0": 241, + "y1": 482, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 242, + "mark": null, + "x": 242, + "y0": 242, + "y1": 484, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 243, + "mark": null, + "x": 243, + "y0": 243, + "y1": 486, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 244, + "mark": null, + "x": 244, + "y0": 244, + "y1": 488, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 245, + "mark": null, + "x": 245, + "y0": 245, + "y1": 490, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 246, + "mark": null, + "x": 246, + "y0": 246, + "y1": 492, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 247, + "mark": null, + "x": 247, + "y0": 247, + "y1": 494, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 248, + "mark": null, + "x": 248, + "y0": 248, + "y1": 496, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 249, + "mark": null, + "x": 249, + "y0": 249, + "y1": 498, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 250, + "mark": null, + "x": 250, + "y0": 250, + "y1": 500, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 251, + "mark": null, + "x": 251, + "y0": 251, + "y1": 502, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 252, + "mark": null, + "x": 252, + "y0": 252, + "y1": 504, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 253, + "mark": null, + "x": 253, + "y0": 253, + "y1": 506, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 254, + "mark": null, + "x": 254, + "y0": 254, + "y1": 508, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 255, + "mark": null, + "x": 255, + "y0": 255, + "y1": 510, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 256, + "mark": null, + "x": 256, + "y0": 256, + "y1": 512, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 257, + "mark": null, + "x": 257, + "y0": 257, + "y1": 514, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 258, + "mark": null, + "x": 258, + "y0": 258, + "y1": 516, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 259, + "mark": null, + "x": 259, + "y0": 259, + "y1": 518, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 260, + "mark": null, + "x": 260, + "y0": 260, + "y1": 520, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 261, + "mark": null, + "x": 261, + "y0": 261, + "y1": 522, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 262, + "mark": null, + "x": 262, + "y0": 262, + "y1": 524, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 263, + "mark": null, + "x": 263, + "y0": 263, + "y1": 526, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 264, + "mark": null, + "x": 264, + "y0": 264, + "y1": 528, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 265, + "mark": null, + "x": 265, + "y0": 265, + "y1": 530, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 266, + "mark": null, + "x": 266, + "y0": 266, + "y1": 532, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 267, + "mark": null, + "x": 267, + "y0": 267, + "y1": 534, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 268, + "mark": null, + "x": 268, + "y0": 268, + "y1": 536, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 269, + "mark": null, + "x": 269, + "y0": 269, + "y1": 538, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 270, + "mark": null, + "x": 270, + "y0": 270, + "y1": 540, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 271, + "mark": null, + "x": 271, + "y0": 271, + "y1": 542, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 272, + "mark": null, + "x": 272, + "y0": 272, + "y1": 544, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 273, + "mark": null, + "x": 273, + "y0": 273, + "y1": 546, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 274, + "mark": null, + "x": 274, + "y0": 274, + "y1": 548, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 275, + "mark": null, + "x": 275, + "y0": 275, + "y1": 550, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 276, + "mark": null, + "x": 276, + "y0": 276, + "y1": 552, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 277, + "mark": null, + "x": 277, + "y0": 277, + "y1": 554, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 278, + "mark": null, + "x": 278, + "y0": 278, + "y1": 556, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 279, + "mark": null, + "x": 279, + "y0": 279, + "y1": 558, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 280, + "mark": null, + "x": 280, + "y0": 280, + "y1": 560, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 281, + "mark": null, + "x": 281, + "y0": 281, + "y1": 562, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 282, + "mark": null, + "x": 282, + "y0": 282, + "y1": 564, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 283, + "mark": null, + "x": 283, + "y0": 283, + "y1": 566, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 284, + "mark": null, + "x": 284, + "y0": 284, + "y1": 568, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 285, + "mark": null, + "x": 285, + "y0": 285, + "y1": 570, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 286, + "mark": null, + "x": 286, + "y0": 286, + "y1": 572, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 287, + "mark": null, + "x": 287, + "y0": 287, + "y1": 574, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 288, + "mark": null, + "x": 288, + "y0": 288, + "y1": 576, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 289, + "mark": null, + "x": 289, + "y0": 289, + "y1": 578, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 290, + "mark": null, + "x": 290, + "y0": 290, + "y1": 580, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 291, + "mark": null, + "x": 291, + "y0": 291, + "y1": 582, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 292, + "mark": null, + "x": 292, + "y0": 292, + "y1": 584, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 293, + "mark": null, + "x": 293, + "y0": 293, + "y1": 586, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 294, + "mark": null, + "x": 294, + "y0": 294, + "y1": 588, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 295, + "mark": null, + "x": 295, + "y0": 295, + "y1": 590, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 296, + "mark": null, + "x": 296, + "y0": 296, + "y1": 592, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 297, + "mark": null, + "x": 297, + "y0": 297, + "y1": 594, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 298, + "mark": null, + "x": 298, + "y0": 298, + "y1": 596, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 299, + "mark": null, + "x": 299, + "y0": 299, + "y1": 598, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 300, + "mark": null, + "x": 300, + "y0": 300, + "y1": 600, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 301, + "mark": null, + "x": 301, + "y0": 301, + "y1": 602, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 302, + "mark": null, + "x": 302, + "y0": 302, + "y1": 604, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 303, + "mark": null, + "x": 303, + "y0": 303, + "y1": 606, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 304, + "mark": null, + "x": 304, + "y0": 304, + "y1": 608, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 305, + "mark": null, + "x": 305, + "y0": 305, + "y1": 610, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 306, + "mark": null, + "x": 306, + "y0": 306, + "y1": 612, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 307, + "mark": null, + "x": 307, + "y0": 307, + "y1": 614, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 308, + "mark": null, + "x": 308, + "y0": 308, + "y1": 616, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 309, + "mark": null, + "x": 309, + "y0": 309, + "y1": 618, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 310, + "mark": null, + "x": 310, + "y0": 310, + "y1": 620, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 311, + "mark": null, + "x": 311, + "y0": 311, + "y1": 622, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 312, + "mark": null, + "x": 312, + "y0": 312, + "y1": 624, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 313, + "mark": null, + "x": 313, + "y0": 313, + "y1": 626, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 314, + "mark": null, + "x": 314, + "y0": 314, + "y1": 628, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 315, + "mark": null, + "x": 315, + "y0": 315, + "y1": 630, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 316, + "mark": null, + "x": 316, + "y0": 316, + "y1": 632, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 317, + "mark": null, + "x": 317, + "y0": 317, + "y1": 634, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 318, + "mark": null, + "x": 318, + "y0": 318, + "y1": 636, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 319, + "mark": null, + "x": 319, + "y0": 319, + "y1": 638, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 320, + "mark": null, + "x": 320, + "y0": 320, + "y1": 640, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 321, + "mark": null, + "x": 321, + "y0": 321, + "y1": 642, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 322, + "mark": null, + "x": 322, + "y0": 322, + "y1": 644, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 323, + "mark": null, + "x": 323, + "y0": 323, + "y1": 646, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 324, + "mark": null, + "x": 324, + "y0": 324, + "y1": 648, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 325, + "mark": null, + "x": 325, + "y0": 325, + "y1": 650, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 326, + "mark": null, + "x": 326, + "y0": 326, + "y1": 652, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 327, + "mark": null, + "x": 327, + "y0": 327, + "y1": 654, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 328, + "mark": null, + "x": 328, + "y0": 328, + "y1": 656, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 329, + "mark": null, + "x": 329, + "y0": 329, + "y1": 658, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 330, + "mark": null, + "x": 330, + "y0": 330, + "y1": 660, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 331, + "mark": null, + "x": 331, + "y0": 331, + "y1": 662, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 332, + "mark": null, + "x": 332, + "y0": 332, + "y1": 664, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 333, + "mark": null, + "x": 333, + "y0": 333, + "y1": 666, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 334, + "mark": null, + "x": 334, + "y0": 334, + "y1": 668, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 335, + "mark": null, + "x": 335, + "y0": 335, + "y1": 670, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 336, + "mark": null, + "x": 336, + "y0": 336, + "y1": 672, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 337, + "mark": null, + "x": 337, + "y0": 337, + "y1": 674, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 338, + "mark": null, + "x": 338, + "y0": 338, + "y1": 676, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 339, + "mark": null, + "x": 339, + "y0": 339, + "y1": 678, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 340, + "mark": null, + "x": 340, + "y0": 340, + "y1": 680, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 341, + "mark": null, + "x": 341, + "y0": 341, + "y1": 682, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 342, + "mark": null, + "x": 342, + "y0": 342, + "y1": 684, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 343, + "mark": null, + "x": 343, + "y0": 343, + "y1": 686, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 344, + "mark": null, + "x": 344, + "y0": 344, + "y1": 688, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 345, + "mark": null, + "x": 345, + "y0": 345, + "y1": 690, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 346, + "mark": null, + "x": 346, + "y0": 346, + "y1": 692, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 347, + "mark": null, + "x": 347, + "y0": 347, + "y1": 694, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 348, + "mark": null, + "x": 348, + "y0": 348, + "y1": 696, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 349, + "mark": null, + "x": 349, + "y0": 349, + "y1": 698, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 350, + "mark": null, + "x": 350, + "y0": 350, + "y1": 700, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 351, + "mark": null, + "x": 351, + "y0": 351, + "y1": 702, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 352, + "mark": null, + "x": 352, + "y0": 352, + "y1": 704, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 353, + "mark": null, + "x": 353, + "y0": 353, + "y1": 706, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 354, + "mark": null, + "x": 354, + "y0": 354, + "y1": 708, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 355, + "mark": null, + "x": 355, + "y0": 355, + "y1": 710, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 356, + "mark": null, + "x": 356, + "y0": 356, + "y1": 712, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 357, + "mark": null, + "x": 357, + "y0": 357, + "y1": 714, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 358, + "mark": null, + "x": 358, + "y0": 358, + "y1": 716, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 359, + "mark": null, + "x": 359, + "y0": 359, + "y1": 718, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 360, + "mark": null, + "x": 360, + "y0": 360, + "y1": 720, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 361, + "mark": null, + "x": 361, + "y0": 361, + "y1": 722, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 362, + "mark": null, + "x": 362, + "y0": 362, + "y1": 724, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 363, + "mark": null, + "x": 363, + "y0": 363, + "y1": 726, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 364, + "mark": null, + "x": 364, + "y0": 364, + "y1": 728, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 365, + "mark": null, + "x": 365, + "y0": 365, + "y1": 730, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 366, + "mark": null, + "x": 366, + "y0": 366, + "y1": 732, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 367, + "mark": null, + "x": 367, + "y0": 367, + "y1": 734, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 368, + "mark": null, + "x": 368, + "y0": 368, + "y1": 736, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 369, + "mark": null, + "x": 369, + "y0": 369, + "y1": 738, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 370, + "mark": null, + "x": 370, + "y0": 370, + "y1": 740, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 371, + "mark": null, + "x": 371, + "y0": 371, + "y1": 742, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 372, + "mark": null, + "x": 372, + "y0": 372, + "y1": 744, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 373, + "mark": null, + "x": 373, + "y0": 373, + "y1": 746, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 374, + "mark": null, + "x": 374, + "y0": 374, + "y1": 748, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 375, + "mark": null, + "x": 375, + "y0": 375, + "y1": 750, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 376, + "mark": null, + "x": 376, + "y0": 376, + "y1": 752, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 377, + "mark": null, + "x": 377, + "y0": 377, + "y1": 754, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 378, + "mark": null, + "x": 378, + "y0": 378, + "y1": 756, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 379, + "mark": null, + "x": 379, + "y0": 379, + "y1": 758, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 380, + "mark": null, + "x": 380, + "y0": 380, + "y1": 760, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 381, + "mark": null, + "x": 381, + "y0": 381, + "y1": 762, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 382, + "mark": null, + "x": 382, + "y0": 382, + "y1": 764, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 383, + "mark": null, + "x": 383, + "y0": 383, + "y1": 766, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 384, + "mark": null, + "x": 384, + "y0": 384, + "y1": 768, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 385, + "mark": null, + "x": 385, + "y0": 385, + "y1": 770, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 386, + "mark": null, + "x": 386, + "y0": 386, + "y1": 772, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 387, + "mark": null, + "x": 387, + "y0": 387, + "y1": 774, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 388, + "mark": null, + "x": 388, + "y0": 388, + "y1": 776, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 389, + "mark": null, + "x": 389, + "y0": 389, + "y1": 778, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 390, + "mark": null, + "x": 390, + "y0": 390, + "y1": 780, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 391, + "mark": null, + "x": 391, + "y0": 391, + "y1": 782, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 392, + "mark": null, + "x": 392, + "y0": 392, + "y1": 784, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 393, + "mark": null, + "x": 393, + "y0": 393, + "y1": 786, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 394, + "mark": null, + "x": 394, + "y0": 394, + "y1": 788, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 395, + "mark": null, + "x": 395, + "y0": 395, + "y1": 790, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 396, + "mark": null, + "x": 396, + "y0": 396, + "y1": 792, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 397, + "mark": null, + "x": 397, + "y0": 397, + "y1": 794, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 398, + "mark": null, + "x": 398, + "y0": 398, + "y1": 796, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 399, + "mark": null, + "x": 399, + "y0": 399, + "y1": 798, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 400, + "mark": null, + "x": 400, + "y0": 400, + "y1": 800, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 401, + "mark": null, + "x": 401, + "y0": 401, + "y1": 802, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 402, + "mark": null, + "x": 402, + "y0": 402, + "y1": 804, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 403, + "mark": null, + "x": 403, + "y0": 403, + "y1": 806, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 404, + "mark": null, + "x": 404, + "y0": 404, + "y1": 808, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 405, + "mark": null, + "x": 405, + "y0": 405, + "y1": 810, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 406, + "mark": null, + "x": 406, + "y0": 406, + "y1": 812, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 407, + "mark": null, + "x": 407, + "y0": 407, + "y1": 814, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 408, + "mark": null, + "x": 408, + "y0": 408, + "y1": 816, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 409, + "mark": null, + "x": 409, + "y0": 409, + "y1": 818, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 410, + "mark": null, + "x": 410, + "y0": 410, + "y1": 820, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 411, + "mark": null, + "x": 411, + "y0": 411, + "y1": 822, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 412, + "mark": null, + "x": 412, + "y0": 412, + "y1": 824, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 413, + "mark": null, + "x": 413, + "y0": 413, + "y1": 826, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 414, + "mark": null, + "x": 414, + "y0": 414, + "y1": 828, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 415, + "mark": null, + "x": 415, + "y0": 415, + "y1": 830, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 416, + "mark": null, + "x": 416, + "y0": 416, + "y1": 832, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 417, + "mark": null, + "x": 417, + "y0": 417, + "y1": 834, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 418, + "mark": null, + "x": 418, + "y0": 418, + "y1": 836, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 419, + "mark": null, + "x": 419, + "y0": 419, + "y1": 838, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 420, + "mark": null, + "x": 420, + "y0": 420, + "y1": 840, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 421, + "mark": null, + "x": 421, + "y0": 421, + "y1": 842, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 422, + "mark": null, + "x": 422, + "y0": 422, + "y1": 844, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 423, + "mark": null, + "x": 423, + "y0": 423, + "y1": 846, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 424, + "mark": null, + "x": 424, + "y0": 424, + "y1": 848, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 425, + "mark": null, + "x": 425, + "y0": 425, + "y1": 850, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 426, + "mark": null, + "x": 426, + "y0": 426, + "y1": 852, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 427, + "mark": null, + "x": 427, + "y0": 427, + "y1": 854, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 428, + "mark": null, + "x": 428, + "y0": 428, + "y1": 856, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 429, + "mark": null, + "x": 429, + "y0": 429, + "y1": 858, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 430, + "mark": null, + "x": 430, + "y0": 430, + "y1": 860, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 431, + "mark": null, + "x": 431, + "y0": 431, + "y1": 862, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 432, + "mark": null, + "x": 432, + "y0": 432, + "y1": 864, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 433, + "mark": null, + "x": 433, + "y0": 433, + "y1": 866, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 434, + "mark": null, + "x": 434, + "y0": 434, + "y1": 868, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 435, + "mark": null, + "x": 435, + "y0": 435, + "y1": 870, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 436, + "mark": null, + "x": 436, + "y0": 436, + "y1": 872, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 437, + "mark": null, + "x": 437, + "y0": 437, + "y1": 874, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 438, + "mark": null, + "x": 438, + "y0": 438, + "y1": 876, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 439, + "mark": null, + "x": 439, + "y0": 439, + "y1": 878, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 440, + "mark": null, + "x": 440, + "y0": 440, + "y1": 880, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 441, + "mark": null, + "x": 441, + "y0": 441, + "y1": 882, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 442, + "mark": null, + "x": 442, + "y0": 442, + "y1": 884, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 443, + "mark": null, + "x": 443, + "y0": 443, + "y1": 886, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 444, + "mark": null, + "x": 444, + "y0": 444, + "y1": 888, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 445, + "mark": null, + "x": 445, + "y0": 445, + "y1": 890, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 446, + "mark": null, + "x": 446, + "y0": 446, + "y1": 892, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 447, + "mark": null, + "x": 447, + "y0": 447, + "y1": 894, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 448, + "mark": null, + "x": 448, + "y0": 448, + "y1": 896, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 449, + "mark": null, + "x": 449, + "y0": 449, + "y1": 898, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 450, + "mark": null, + "x": 450, + "y0": 450, + "y1": 900, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 451, + "mark": null, + "x": 451, + "y0": 451, + "y1": 902, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 452, + "mark": null, + "x": 452, + "y0": 452, + "y1": 904, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 453, + "mark": null, + "x": 453, + "y0": 453, + "y1": 906, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 454, + "mark": null, + "x": 454, + "y0": 454, + "y1": 908, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 455, + "mark": null, + "x": 455, + "y0": 455, + "y1": 910, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 456, + "mark": null, + "x": 456, + "y0": 456, + "y1": 912, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 457, + "mark": null, + "x": 457, + "y0": 457, + "y1": 914, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 458, + "mark": null, + "x": 458, + "y0": 458, + "y1": 916, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 459, + "mark": null, + "x": 459, + "y0": 459, + "y1": 918, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 460, + "mark": null, + "x": 460, + "y0": 460, + "y1": 920, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 461, + "mark": null, + "x": 461, + "y0": 461, + "y1": 922, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 462, + "mark": null, + "x": 462, + "y0": 462, + "y1": 924, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 463, + "mark": null, + "x": 463, + "y0": 463, + "y1": 926, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 464, + "mark": null, + "x": 464, + "y0": 464, + "y1": 928, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 465, + "mark": null, + "x": 465, + "y0": 465, + "y1": 930, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 466, + "mark": null, + "x": 466, + "y0": 466, + "y1": 932, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 467, + "mark": null, + "x": 467, + "y0": 467, + "y1": 934, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 468, + "mark": null, + "x": 468, + "y0": 468, + "y1": 936, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 469, + "mark": null, + "x": 469, + "y0": 469, + "y1": 938, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 470, + "mark": null, + "x": 470, + "y0": 470, + "y1": 940, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 471, + "mark": null, + "x": 471, + "y0": 471, + "y1": 942, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 472, + "mark": null, + "x": 472, + "y0": 472, + "y1": 944, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 473, + "mark": null, + "x": 473, + "y0": 473, + "y1": 946, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 474, + "mark": null, + "x": 474, + "y0": 474, + "y1": 948, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 475, + "mark": null, + "x": 475, + "y0": 475, + "y1": 950, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 476, + "mark": null, + "x": 476, + "y0": 476, + "y1": 952, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 477, + "mark": null, + "x": 477, + "y0": 477, + "y1": 954, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 478, + "mark": null, + "x": 478, + "y0": 478, + "y1": 956, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 479, + "mark": null, + "x": 479, + "y0": 479, + "y1": 958, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 480, + "mark": null, + "x": 480, + "y0": 480, + "y1": 960, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 481, + "mark": null, + "x": 481, + "y0": 481, + "y1": 962, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 482, + "mark": null, + "x": 482, + "y0": 482, + "y1": 964, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 483, + "mark": null, + "x": 483, + "y0": 483, + "y1": 966, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 484, + "mark": null, + "x": 484, + "y0": 484, + "y1": 968, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 485, + "mark": null, + "x": 485, + "y0": 485, + "y1": 970, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 486, + "mark": null, + "x": 486, + "y0": 486, + "y1": 972, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 487, + "mark": null, + "x": 487, + "y0": 487, + "y1": 974, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 488, + "mark": null, + "x": 488, + "y0": 488, + "y1": 976, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 489, + "mark": null, + "x": 489, + "y0": 489, + "y1": 978, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 490, + "mark": null, + "x": 490, + "y0": 490, + "y1": 980, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 491, + "mark": null, + "x": 491, + "y0": 491, + "y1": 982, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 492, + "mark": null, + "x": 492, + "y0": 492, + "y1": 984, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 493, + "mark": null, + "x": 493, + "y0": 493, + "y1": 986, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 494, + "mark": null, + "x": 494, + "y0": 494, + "y1": 988, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 495, + "mark": null, + "x": 495, + "y0": 495, + "y1": 990, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 496, + "mark": null, + "x": 496, + "y0": 496, + "y1": 992, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 497, + "mark": null, + "x": 497, + "y0": 497, + "y1": 994, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 498, + "mark": null, + "x": 498, + "y0": 498, + "y1": 996, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 499, + "mark": null, + "x": 499, + "y0": 499, + "y1": 998, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 500, + "mark": null, + "x": 500, + "y0": 500, + "y1": 1000, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 501, + "mark": null, + "x": 501, + "y0": 501, + "y1": 1002, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 502, + "mark": null, + "x": 502, + "y0": 502, + "y1": 1004, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 503, + "mark": null, + "x": 503, + "y0": 503, + "y1": 1006, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 504, + "mark": null, + "x": 504, + "y0": 504, + "y1": 1008, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 505, + "mark": null, + "x": 505, + "y0": 505, + "y1": 1010, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 506, + "mark": null, + "x": 506, + "y0": 506, + "y1": 1012, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 507, + "mark": null, + "x": 507, + "y0": 507, + "y1": 1014, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 508, + "mark": null, + "x": 508, + "y0": 508, + "y1": 1016, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 509, + "mark": null, + "x": 509, + "y0": 509, + "y1": 1018, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 510, + "mark": null, + "x": 510, + "y0": 510, + "y1": 1020, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 511, + "mark": null, + "x": 511, + "y0": 511, + "y1": 1022, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 512, + "mark": null, + "x": 512, + "y0": 512, + "y1": 1024, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 513, + "mark": null, + "x": 513, + "y0": 513, + "y1": 1026, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 514, + "mark": null, + "x": 514, + "y0": 514, + "y1": 1028, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 515, + "mark": null, + "x": 515, + "y0": 515, + "y1": 1030, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 516, + "mark": null, + "x": 516, + "y0": 516, + "y1": 1032, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 517, + "mark": null, + "x": 517, + "y0": 517, + "y1": 1034, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 518, + "mark": null, + "x": 518, + "y0": 518, + "y1": 1036, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 519, + "mark": null, + "x": 519, + "y0": 519, + "y1": 1038, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 520, + "mark": null, + "x": 520, + "y0": 520, + "y1": 1040, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 521, + "mark": null, + "x": 521, + "y0": 521, + "y1": 1042, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 522, + "mark": null, + "x": 522, + "y0": 522, + "y1": 1044, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 523, + "mark": null, + "x": 523, + "y0": 523, + "y1": 1046, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 524, + "mark": null, + "x": 524, + "y0": 524, + "y1": 1048, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 525, + "mark": null, + "x": 525, + "y0": 525, + "y1": 1050, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 526, + "mark": null, + "x": 526, + "y0": 526, + "y1": 1052, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 527, + "mark": null, + "x": 527, + "y0": 527, + "y1": 1054, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 528, + "mark": null, + "x": 528, + "y0": 528, + "y1": 1056, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 529, + "mark": null, + "x": 529, + "y0": 529, + "y1": 1058, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 530, + "mark": null, + "x": 530, + "y0": 530, + "y1": 1060, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 531, + "mark": null, + "x": 531, + "y0": 531, + "y1": 1062, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 532, + "mark": null, + "x": 532, + "y0": 532, + "y1": 1064, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 533, + "mark": null, + "x": 533, + "y0": 533, + "y1": 1066, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 534, + "mark": null, + "x": 534, + "y0": 534, + "y1": 1068, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 535, + "mark": null, + "x": 535, + "y0": 535, + "y1": 1070, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 536, + "mark": null, + "x": 536, + "y0": 536, + "y1": 1072, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 537, + "mark": null, + "x": 537, + "y0": 537, + "y1": 1074, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 538, + "mark": null, + "x": 538, + "y0": 538, + "y1": 1076, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 539, + "mark": null, + "x": 539, + "y0": 539, + "y1": 1078, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 540, + "mark": null, + "x": 540, + "y0": 540, + "y1": 1080, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 541, + "mark": null, + "x": 541, + "y0": 541, + "y1": 1082, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 542, + "mark": null, + "x": 542, + "y0": 542, + "y1": 1084, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 543, + "mark": null, + "x": 543, + "y0": 543, + "y1": 1086, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 544, + "mark": null, + "x": 544, + "y0": 544, + "y1": 1088, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 545, + "mark": null, + "x": 545, + "y0": 545, + "y1": 1090, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 546, + "mark": null, + "x": 546, + "y0": 546, + "y1": 1092, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 547, + "mark": null, + "x": 547, + "y0": 547, + "y1": 1094, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 548, + "mark": null, + "x": 548, + "y0": 548, + "y1": 1096, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 549, + "mark": null, + "x": 549, + "y0": 549, + "y1": 1098, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 550, + "mark": null, + "x": 550, + "y0": 550, + "y1": 1100, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 551, + "mark": null, + "x": 551, + "y0": 551, + "y1": 1102, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 552, + "mark": null, + "x": 552, + "y0": 552, + "y1": 1104, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 553, + "mark": null, + "x": 553, + "y0": 553, + "y1": 1106, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 554, + "mark": null, + "x": 554, + "y0": 554, + "y1": 1108, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 555, + "mark": null, + "x": 555, + "y0": 555, + "y1": 1110, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 556, + "mark": null, + "x": 556, + "y0": 556, + "y1": 1112, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 557, + "mark": null, + "x": 557, + "y0": 557, + "y1": 1114, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 558, + "mark": null, + "x": 558, + "y0": 558, + "y1": 1116, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 559, + "mark": null, + "x": 559, + "y0": 559, + "y1": 1118, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 560, + "mark": null, + "x": 560, + "y0": 560, + "y1": 1120, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 561, + "mark": null, + "x": 561, + "y0": 561, + "y1": 1122, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 562, + "mark": null, + "x": 562, + "y0": 562, + "y1": 1124, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 563, + "mark": null, + "x": 563, + "y0": 563, + "y1": 1126, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 564, + "mark": null, + "x": 564, + "y0": 564, + "y1": 1128, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 565, + "mark": null, + "x": 565, + "y0": 565, + "y1": 1130, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 566, + "mark": null, + "x": 566, + "y0": 566, + "y1": 1132, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 567, + "mark": null, + "x": 567, + "y0": 567, + "y1": 1134, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 568, + "mark": null, + "x": 568, + "y0": 568, + "y1": 1136, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 569, + "mark": null, + "x": 569, + "y0": 569, + "y1": 1138, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 570, + "mark": null, + "x": 570, + "y0": 570, + "y1": 1140, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 571, + "mark": null, + "x": 571, + "y0": 571, + "y1": 1142, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 572, + "mark": null, + "x": 572, + "y0": 572, + "y1": 1144, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 573, + "mark": null, + "x": 573, + "y0": 573, + "y1": 1146, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 574, + "mark": null, + "x": 574, + "y0": 574, + "y1": 1148, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 575, + "mark": null, + "x": 575, + "y0": 575, + "y1": 1150, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 576, + "mark": null, + "x": 576, + "y0": 576, + "y1": 1152, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 577, + "mark": null, + "x": 577, + "y0": 577, + "y1": 1154, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 578, + "mark": null, + "x": 578, + "y0": 578, + "y1": 1156, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 579, + "mark": null, + "x": 579, + "y0": 579, + "y1": 1158, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 580, + "mark": null, + "x": 580, + "y0": 580, + "y1": 1160, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 581, + "mark": null, + "x": 581, + "y0": 581, + "y1": 1162, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 582, + "mark": null, + "x": 582, + "y0": 582, + "y1": 1164, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 583, + "mark": null, + "x": 583, + "y0": 583, + "y1": 1166, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 584, + "mark": null, + "x": 584, + "y0": 584, + "y1": 1168, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 585, + "mark": null, + "x": 585, + "y0": 585, + "y1": 1170, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 586, + "mark": null, + "x": 586, + "y0": 586, + "y1": 1172, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 587, + "mark": null, + "x": 587, + "y0": 587, + "y1": 1174, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 588, + "mark": null, + "x": 588, + "y0": 588, + "y1": 1176, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 589, + "mark": null, + "x": 589, + "y0": 589, + "y1": 1178, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 590, + "mark": null, + "x": 590, + "y0": 590, + "y1": 1180, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 591, + "mark": null, + "x": 591, + "y0": 591, + "y1": 1182, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 592, + "mark": null, + "x": 592, + "y0": 592, + "y1": 1184, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 593, + "mark": null, + "x": 593, + "y0": 593, + "y1": 1186, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 594, + "mark": null, + "x": 594, + "y0": 594, + "y1": 1188, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 595, + "mark": null, + "x": 595, + "y0": 595, + "y1": 1190, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 596, + "mark": null, + "x": 596, + "y0": 596, + "y1": 1192, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 597, + "mark": null, + "x": 597, + "y0": 597, + "y1": 1194, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 598, + "mark": null, + "x": 598, + "y0": 598, + "y1": 1196, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 599, + "mark": null, + "x": 599, + "y0": 599, + "y1": 1198, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 600, + "mark": null, + "x": 600, + "y0": 600, + "y1": 1200, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 601, + "mark": null, + "x": 601, + "y0": 601, + "y1": 1202, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 602, + "mark": null, + "x": 602, + "y0": 602, + "y1": 1204, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 603, + "mark": null, + "x": 603, + "y0": 603, + "y1": 1206, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 604, + "mark": null, + "x": 604, + "y0": 604, + "y1": 1208, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 605, + "mark": null, + "x": 605, + "y0": 605, + "y1": 1210, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 606, + "mark": null, + "x": 606, + "y0": 606, + "y1": 1212, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 607, + "mark": null, + "x": 607, + "y0": 607, + "y1": 1214, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 608, + "mark": null, + "x": 608, + "y0": 608, + "y1": 1216, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 609, + "mark": null, + "x": 609, + "y0": 609, + "y1": 1218, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 610, + "mark": null, + "x": 610, + "y0": 610, + "y1": 1220, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 611, + "mark": null, + "x": 611, + "y0": 611, + "y1": 1222, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 612, + "mark": null, + "x": 612, + "y0": 612, + "y1": 1224, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 613, + "mark": null, + "x": 613, + "y0": 613, + "y1": 1226, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 614, + "mark": null, + "x": 614, + "y0": 614, + "y1": 1228, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 615, + "mark": null, + "x": 615, + "y0": 615, + "y1": 1230, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 616, + "mark": null, + "x": 616, + "y0": 616, + "y1": 1232, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 617, + "mark": null, + "x": 617, + "y0": 617, + "y1": 1234, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 618, + "mark": null, + "x": 618, + "y0": 618, + "y1": 1236, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 619, + "mark": null, + "x": 619, + "y0": 619, + "y1": 1238, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 620, + "mark": null, + "x": 620, + "y0": 620, + "y1": 1240, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 621, + "mark": null, + "x": 621, + "y0": 621, + "y1": 1242, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 622, + "mark": null, + "x": 622, + "y0": 622, + "y1": 1244, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 623, + "mark": null, + "x": 623, + "y0": 623, + "y1": 1246, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 624, + "mark": null, + "x": 624, + "y0": 624, + "y1": 1248, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 625, + "mark": null, + "x": 625, + "y0": 625, + "y1": 1250, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 626, + "mark": null, + "x": 626, + "y0": 626, + "y1": 1252, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 627, + "mark": null, + "x": 627, + "y0": 627, + "y1": 1254, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 628, + "mark": null, + "x": 628, + "y0": 628, + "y1": 1256, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 629, + "mark": null, + "x": 629, + "y0": 629, + "y1": 1258, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 630, + "mark": null, + "x": 630, + "y0": 630, + "y1": 1260, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 631, + "mark": null, + "x": 631, + "y0": 631, + "y1": 1262, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 632, + "mark": null, + "x": 632, + "y0": 632, + "y1": 1264, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 633, + "mark": null, + "x": 633, + "y0": 633, + "y1": 1266, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 634, + "mark": null, + "x": 634, + "y0": 634, + "y1": 1268, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 635, + "mark": null, + "x": 635, + "y0": 635, + "y1": 1270, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 636, + "mark": null, + "x": 636, + "y0": 636, + "y1": 1272, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 637, + "mark": null, + "x": 637, + "y0": 637, + "y1": 1274, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 638, + "mark": null, + "x": 638, + "y0": 638, + "y1": 1276, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 639, + "mark": null, + "x": 639, + "y0": 639, + "y1": 1278, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 640, + "mark": null, + "x": 640, + "y0": 640, + "y1": 1280, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 641, + "mark": null, + "x": 641, + "y0": 641, + "y1": 1282, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 642, + "mark": null, + "x": 642, + "y0": 642, + "y1": 1284, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 643, + "mark": null, + "x": 643, + "y0": 643, + "y1": 1286, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 644, + "mark": null, + "x": 644, + "y0": 644, + "y1": 1288, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 645, + "mark": null, + "x": 645, + "y0": 645, + "y1": 1290, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 646, + "mark": null, + "x": 646, + "y0": 646, + "y1": 1292, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 647, + "mark": null, + "x": 647, + "y0": 647, + "y1": 1294, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 648, + "mark": null, + "x": 648, + "y0": 648, + "y1": 1296, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 649, + "mark": null, + "x": 649, + "y0": 649, + "y1": 1298, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 650, + "mark": null, + "x": 650, + "y0": 650, + "y1": 1300, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 651, + "mark": null, + "x": 651, + "y0": 651, + "y1": 1302, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 652, + "mark": null, + "x": 652, + "y0": 652, + "y1": 1304, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 653, + "mark": null, + "x": 653, + "y0": 653, + "y1": 1306, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 654, + "mark": null, + "x": 654, + "y0": 654, + "y1": 1308, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 655, + "mark": null, + "x": 655, + "y0": 655, + "y1": 1310, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 656, + "mark": null, + "x": 656, + "y0": 656, + "y1": 1312, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 657, + "mark": null, + "x": 657, + "y0": 657, + "y1": 1314, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 658, + "mark": null, + "x": 658, + "y0": 658, + "y1": 1316, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 659, + "mark": null, + "x": 659, + "y0": 659, + "y1": 1318, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 660, + "mark": null, + "x": 660, + "y0": 660, + "y1": 1320, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 661, + "mark": null, + "x": 661, + "y0": 661, + "y1": 1322, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 662, + "mark": null, + "x": 662, + "y0": 662, + "y1": 1324, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 663, + "mark": null, + "x": 663, + "y0": 663, + "y1": 1326, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 664, + "mark": null, + "x": 664, + "y0": 664, + "y1": 1328, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 665, + "mark": null, + "x": 665, + "y0": 665, + "y1": 1330, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 666, + "mark": null, + "x": 666, + "y0": 666, + "y1": 1332, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 667, + "mark": null, + "x": 667, + "y0": 667, + "y1": 1334, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 668, + "mark": null, + "x": 668, + "y0": 668, + "y1": 1336, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 669, + "mark": null, + "x": 669, + "y0": 669, + "y1": 1338, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 670, + "mark": null, + "x": 670, + "y0": 670, + "y1": 1340, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 671, + "mark": null, + "x": 671, + "y0": 671, + "y1": 1342, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 672, + "mark": null, + "x": 672, + "y0": 672, + "y1": 1344, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 673, + "mark": null, + "x": 673, + "y0": 673, + "y1": 1346, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 674, + "mark": null, + "x": 674, + "y0": 674, + "y1": 1348, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 675, + "mark": null, + "x": 675, + "y0": 675, + "y1": 1350, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 676, + "mark": null, + "x": 676, + "y0": 676, + "y1": 1352, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 677, + "mark": null, + "x": 677, + "y0": 677, + "y1": 1354, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 678, + "mark": null, + "x": 678, + "y0": 678, + "y1": 1356, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 679, + "mark": null, + "x": 679, + "y0": 679, + "y1": 1358, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 680, + "mark": null, + "x": 680, + "y0": 680, + "y1": 1360, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 681, + "mark": null, + "x": 681, + "y0": 681, + "y1": 1362, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 682, + "mark": null, + "x": 682, + "y0": 682, + "y1": 1364, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 683, + "mark": null, + "x": 683, + "y0": 683, + "y1": 1366, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 684, + "mark": null, + "x": 684, + "y0": 684, + "y1": 1368, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 685, + "mark": null, + "x": 685, + "y0": 685, + "y1": 1370, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 686, + "mark": null, + "x": 686, + "y0": 686, + "y1": 1372, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 687, + "mark": null, + "x": 687, + "y0": 687, + "y1": 1374, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 688, + "mark": null, + "x": 688, + "y0": 688, + "y1": 1376, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 689, + "mark": null, + "x": 689, + "y0": 689, + "y1": 1378, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 690, + "mark": null, + "x": 690, + "y0": 690, + "y1": 1380, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 691, + "mark": null, + "x": 691, + "y0": 691, + "y1": 1382, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 692, + "mark": null, + "x": 692, + "y0": 692, + "y1": 1384, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 693, + "mark": null, + "x": 693, + "y0": 693, + "y1": 1386, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 694, + "mark": null, + "x": 694, + "y0": 694, + "y1": 1388, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 695, + "mark": null, + "x": 695, + "y0": 695, + "y1": 1390, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 696, + "mark": null, + "x": 696, + "y0": 696, + "y1": 1392, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 697, + "mark": null, + "x": 697, + "y0": 697, + "y1": 1394, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 698, + "mark": null, + "x": 698, + "y0": 698, + "y1": 1396, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 699, + "mark": null, + "x": 699, + "y0": 699, + "y1": 1398, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 700, + "mark": null, + "x": 700, + "y0": 700, + "y1": 1400, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 701, + "mark": null, + "x": 701, + "y0": 701, + "y1": 1402, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 702, + "mark": null, + "x": 702, + "y0": 702, + "y1": 1404, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 703, + "mark": null, + "x": 703, + "y0": 703, + "y1": 1406, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 704, + "mark": null, + "x": 704, + "y0": 704, + "y1": 1408, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 705, + "mark": null, + "x": 705, + "y0": 705, + "y1": 1410, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 706, + "mark": null, + "x": 706, + "y0": 706, + "y1": 1412, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 707, + "mark": null, + "x": 707, + "y0": 707, + "y1": 1414, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 708, + "mark": null, + "x": 708, + "y0": 708, + "y1": 1416, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 709, + "mark": null, + "x": 709, + "y0": 709, + "y1": 1418, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 710, + "mark": null, + "x": 710, + "y0": 710, + "y1": 1420, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 711, + "mark": null, + "x": 711, + "y0": 711, + "y1": 1422, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 712, + "mark": null, + "x": 712, + "y0": 712, + "y1": 1424, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 713, + "mark": null, + "x": 713, + "y0": 713, + "y1": 1426, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 714, + "mark": null, + "x": 714, + "y0": 714, + "y1": 1428, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 715, + "mark": null, + "x": 715, + "y0": 715, + "y1": 1430, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 716, + "mark": null, + "x": 716, + "y0": 716, + "y1": 1432, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 717, + "mark": null, + "x": 717, + "y0": 717, + "y1": 1434, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 718, + "mark": null, + "x": 718, + "y0": 718, + "y1": 1436, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 719, + "mark": null, + "x": 719, + "y0": 719, + "y1": 1438, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 720, + "mark": null, + "x": 720, + "y0": 720, + "y1": 1440, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 721, + "mark": null, + "x": 721, + "y0": 721, + "y1": 1442, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 722, + "mark": null, + "x": 722, + "y0": 722, + "y1": 1444, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 723, + "mark": null, + "x": 723, + "y0": 723, + "y1": 1446, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 724, + "mark": null, + "x": 724, + "y0": 724, + "y1": 1448, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 725, + "mark": null, + "x": 725, + "y0": 725, + "y1": 1450, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 726, + "mark": null, + "x": 726, + "y0": 726, + "y1": 1452, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 727, + "mark": null, + "x": 727, + "y0": 727, + "y1": 1454, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 728, + "mark": null, + "x": 728, + "y0": 728, + "y1": 1456, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 729, + "mark": null, + "x": 729, + "y0": 729, + "y1": 1458, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 730, + "mark": null, + "x": 730, + "y0": 730, + "y1": 1460, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 731, + "mark": null, + "x": 731, + "y0": 731, + "y1": 1462, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 732, + "mark": null, + "x": 732, + "y0": 732, + "y1": 1464, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 733, + "mark": null, + "x": 733, + "y0": 733, + "y1": 1466, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 734, + "mark": null, + "x": 734, + "y0": 734, + "y1": 1468, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 735, + "mark": null, + "x": 735, + "y0": 735, + "y1": 1470, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 736, + "mark": null, + "x": 736, + "y0": 736, + "y1": 1472, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 737, + "mark": null, + "x": 737, + "y0": 737, + "y1": 1474, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 738, + "mark": null, + "x": 738, + "y0": 738, + "y1": 1476, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 739, + "mark": null, + "x": 739, + "y0": 739, + "y1": 1478, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 740, + "mark": null, + "x": 740, + "y0": 740, + "y1": 1480, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 741, + "mark": null, + "x": 741, + "y0": 741, + "y1": 1482, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 742, + "mark": null, + "x": 742, + "y0": 742, + "y1": 1484, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 743, + "mark": null, + "x": 743, + "y0": 743, + "y1": 1486, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 744, + "mark": null, + "x": 744, + "y0": 744, + "y1": 1488, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 745, + "mark": null, + "x": 745, + "y0": 745, + "y1": 1490, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 746, + "mark": null, + "x": 746, + "y0": 746, + "y1": 1492, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 747, + "mark": null, + "x": 747, + "y0": 747, + "y1": 1494, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 748, + "mark": null, + "x": 748, + "y0": 748, + "y1": 1496, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 749, + "mark": null, + "x": 749, + "y0": 749, + "y1": 1498, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 750, + "mark": null, + "x": 750, + "y0": 750, + "y1": 1500, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 751, + "mark": null, + "x": 751, + "y0": 751, + "y1": 1502, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 752, + "mark": null, + "x": 752, + "y0": 752, + "y1": 1504, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 753, + "mark": null, + "x": 753, + "y0": 753, + "y1": 1506, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 754, + "mark": null, + "x": 754, + "y0": 754, + "y1": 1508, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 755, + "mark": null, + "x": 755, + "y0": 755, + "y1": 1510, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 756, + "mark": null, + "x": 756, + "y0": 756, + "y1": 1512, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 757, + "mark": null, + "x": 757, + "y0": 757, + "y1": 1514, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 758, + "mark": null, + "x": 758, + "y0": 758, + "y1": 1516, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 759, + "mark": null, + "x": 759, + "y0": 759, + "y1": 1518, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 760, + "mark": null, + "x": 760, + "y0": 760, + "y1": 1520, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 761, + "mark": null, + "x": 761, + "y0": 761, + "y1": 1522, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 762, + "mark": null, + "x": 762, + "y0": 762, + "y1": 1524, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 763, + "mark": null, + "x": 763, + "y0": 763, + "y1": 1526, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 764, + "mark": null, + "x": 764, + "y0": 764, + "y1": 1528, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 765, + "mark": null, + "x": 765, + "y0": 765, + "y1": 1530, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 766, + "mark": null, + "x": 766, + "y0": 766, + "y1": 1532, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 767, + "mark": null, + "x": 767, + "y0": 767, + "y1": 1534, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 768, + "mark": null, + "x": 768, + "y0": 768, + "y1": 1536, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 769, + "mark": null, + "x": 769, + "y0": 769, + "y1": 1538, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 770, + "mark": null, + "x": 770, + "y0": 770, + "y1": 1540, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 771, + "mark": null, + "x": 771, + "y0": 771, + "y1": 1542, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 772, + "mark": null, + "x": 772, + "y0": 772, + "y1": 1544, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 773, + "mark": null, + "x": 773, + "y0": 773, + "y1": 1546, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 774, + "mark": null, + "x": 774, + "y0": 774, + "y1": 1548, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 775, + "mark": null, + "x": 775, + "y0": 775, + "y1": 1550, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 776, + "mark": null, + "x": 776, + "y0": 776, + "y1": 1552, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 777, + "mark": null, + "x": 777, + "y0": 777, + "y1": 1554, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 778, + "mark": null, + "x": 778, + "y0": 778, + "y1": 1556, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 779, + "mark": null, + "x": 779, + "y0": 779, + "y1": 1558, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 780, + "mark": null, + "x": 780, + "y0": 780, + "y1": 1560, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 781, + "mark": null, + "x": 781, + "y0": 781, + "y1": 1562, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 782, + "mark": null, + "x": 782, + "y0": 782, + "y1": 1564, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 783, + "mark": null, + "x": 783, + "y0": 783, + "y1": 1566, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 784, + "mark": null, + "x": 784, + "y0": 784, + "y1": 1568, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 785, + "mark": null, + "x": 785, + "y0": 785, + "y1": 1570, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 786, + "mark": null, + "x": 786, + "y0": 786, + "y1": 1572, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 787, + "mark": null, + "x": 787, + "y0": 787, + "y1": 1574, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 788, + "mark": null, + "x": 788, + "y0": 788, + "y1": 1576, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 789, + "mark": null, + "x": 789, + "y0": 789, + "y1": 1578, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 790, + "mark": null, + "x": 790, + "y0": 790, + "y1": 1580, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 791, + "mark": null, + "x": 791, + "y0": 791, + "y1": 1582, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 792, + "mark": null, + "x": 792, + "y0": 792, + "y1": 1584, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 793, + "mark": null, + "x": 793, + "y0": 793, + "y1": 1586, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 794, + "mark": null, + "x": 794, + "y0": 794, + "y1": 1588, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 795, + "mark": null, + "x": 795, + "y0": 795, + "y1": 1590, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 796, + "mark": null, + "x": 796, + "y0": 796, + "y1": 1592, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 797, + "mark": null, + "x": 797, + "y0": 797, + "y1": 1594, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 798, + "mark": null, + "x": 798, + "y0": 798, + "y1": 1596, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 799, + "mark": null, + "x": 799, + "y0": 799, + "y1": 1598, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 800, + "mark": null, + "x": 800, + "y0": 800, + "y1": 1600, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 801, + "mark": null, + "x": 801, + "y0": 801, + "y1": 1602, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 802, + "mark": null, + "x": 802, + "y0": 802, + "y1": 1604, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 803, + "mark": null, + "x": 803, + "y0": 803, + "y1": 1606, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 804, + "mark": null, + "x": 804, + "y0": 804, + "y1": 1608, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 805, + "mark": null, + "x": 805, + "y0": 805, + "y1": 1610, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 806, + "mark": null, + "x": 806, + "y0": 806, + "y1": 1612, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 807, + "mark": null, + "x": 807, + "y0": 807, + "y1": 1614, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 808, + "mark": null, + "x": 808, + "y0": 808, + "y1": 1616, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 809, + "mark": null, + "x": 809, + "y0": 809, + "y1": 1618, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 810, + "mark": null, + "x": 810, + "y0": 810, + "y1": 1620, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 811, + "mark": null, + "x": 811, + "y0": 811, + "y1": 1622, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 812, + "mark": null, + "x": 812, + "y0": 812, + "y1": 1624, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 813, + "mark": null, + "x": 813, + "y0": 813, + "y1": 1626, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 814, + "mark": null, + "x": 814, + "y0": 814, + "y1": 1628, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 815, + "mark": null, + "x": 815, + "y0": 815, + "y1": 1630, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 816, + "mark": null, + "x": 816, + "y0": 816, + "y1": 1632, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 817, + "mark": null, + "x": 817, + "y0": 817, + "y1": 1634, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 818, + "mark": null, + "x": 818, + "y0": 818, + "y1": 1636, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 819, + "mark": null, + "x": 819, + "y0": 819, + "y1": 1638, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 820, + "mark": null, + "x": 820, + "y0": 820, + "y1": 1640, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 821, + "mark": null, + "x": 821, + "y0": 821, + "y1": 1642, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 822, + "mark": null, + "x": 822, + "y0": 822, + "y1": 1644, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 823, + "mark": null, + "x": 823, + "y0": 823, + "y1": 1646, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 824, + "mark": null, + "x": 824, + "y0": 824, + "y1": 1648, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 825, + "mark": null, + "x": 825, + "y0": 825, + "y1": 1650, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 826, + "mark": null, + "x": 826, + "y0": 826, + "y1": 1652, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 827, + "mark": null, + "x": 827, + "y0": 827, + "y1": 1654, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 828, + "mark": null, + "x": 828, + "y0": 828, + "y1": 1656, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 829, + "mark": null, + "x": 829, + "y0": 829, + "y1": 1658, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 830, + "mark": null, + "x": 830, + "y0": 830, + "y1": 1660, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 831, + "mark": null, + "x": 831, + "y0": 831, + "y1": 1662, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 832, + "mark": null, + "x": 832, + "y0": 832, + "y1": 1664, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 833, + "mark": null, + "x": 833, + "y0": 833, + "y1": 1666, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 834, + "mark": null, + "x": 834, + "y0": 834, + "y1": 1668, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 835, + "mark": null, + "x": 835, + "y0": 835, + "y1": 1670, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 836, + "mark": null, + "x": 836, + "y0": 836, + "y1": 1672, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 837, + "mark": null, + "x": 837, + "y0": 837, + "y1": 1674, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 838, + "mark": null, + "x": 838, + "y0": 838, + "y1": 1676, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 839, + "mark": null, + "x": 839, + "y0": 839, + "y1": 1678, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 840, + "mark": null, + "x": 840, + "y0": 840, + "y1": 1680, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 841, + "mark": null, + "x": 841, + "y0": 841, + "y1": 1682, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 842, + "mark": null, + "x": 842, + "y0": 842, + "y1": 1684, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 843, + "mark": null, + "x": 843, + "y0": 843, + "y1": 1686, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 844, + "mark": null, + "x": 844, + "y0": 844, + "y1": 1688, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 845, + "mark": null, + "x": 845, + "y0": 845, + "y1": 1690, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 846, + "mark": null, + "x": 846, + "y0": 846, + "y1": 1692, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 847, + "mark": null, + "x": 847, + "y0": 847, + "y1": 1694, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 848, + "mark": null, + "x": 848, + "y0": 848, + "y1": 1696, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 849, + "mark": null, + "x": 849, + "y0": 849, + "y1": 1698, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 850, + "mark": null, + "x": 850, + "y0": 850, + "y1": 1700, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 851, + "mark": null, + "x": 851, + "y0": 851, + "y1": 1702, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 852, + "mark": null, + "x": 852, + "y0": 852, + "y1": 1704, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 853, + "mark": null, + "x": 853, + "y0": 853, + "y1": 1706, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 854, + "mark": null, + "x": 854, + "y0": 854, + "y1": 1708, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 855, + "mark": null, + "x": 855, + "y0": 855, + "y1": 1710, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 856, + "mark": null, + "x": 856, + "y0": 856, + "y1": 1712, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 857, + "mark": null, + "x": 857, + "y0": 857, + "y1": 1714, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 858, + "mark": null, + "x": 858, + "y0": 858, + "y1": 1716, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 859, + "mark": null, + "x": 859, + "y0": 859, + "y1": 1718, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 860, + "mark": null, + "x": 860, + "y0": 860, + "y1": 1720, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 861, + "mark": null, + "x": 861, + "y0": 861, + "y1": 1722, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 862, + "mark": null, + "x": 862, + "y0": 862, + "y1": 1724, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 863, + "mark": null, + "x": 863, + "y0": 863, + "y1": 1726, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 864, + "mark": null, + "x": 864, + "y0": 864, + "y1": 1728, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 865, + "mark": null, + "x": 865, + "y0": 865, + "y1": 1730, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 866, + "mark": null, + "x": 866, + "y0": 866, + "y1": 1732, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 867, + "mark": null, + "x": 867, + "y0": 867, + "y1": 1734, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 868, + "mark": null, + "x": 868, + "y0": 868, + "y1": 1736, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 869, + "mark": null, + "x": 869, + "y0": 869, + "y1": 1738, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 870, + "mark": null, + "x": 870, + "y0": 870, + "y1": 1740, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 871, + "mark": null, + "x": 871, + "y0": 871, + "y1": 1742, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 872, + "mark": null, + "x": 872, + "y0": 872, + "y1": 1744, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 873, + "mark": null, + "x": 873, + "y0": 873, + "y1": 1746, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 874, + "mark": null, + "x": 874, + "y0": 874, + "y1": 1748, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 875, + "mark": null, + "x": 875, + "y0": 875, + "y1": 1750, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 876, + "mark": null, + "x": 876, + "y0": 876, + "y1": 1752, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 877, + "mark": null, + "x": 877, + "y0": 877, + "y1": 1754, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 878, + "mark": null, + "x": 878, + "y0": 878, + "y1": 1756, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 879, + "mark": null, + "x": 879, + "y0": 879, + "y1": 1758, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 880, + "mark": null, + "x": 880, + "y0": 880, + "y1": 1760, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 881, + "mark": null, + "x": 881, + "y0": 881, + "y1": 1762, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 882, + "mark": null, + "x": 882, + "y0": 882, + "y1": 1764, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 883, + "mark": null, + "x": 883, + "y0": 883, + "y1": 1766, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 884, + "mark": null, + "x": 884, + "y0": 884, + "y1": 1768, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 885, + "mark": null, + "x": 885, + "y0": 885, + "y1": 1770, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 886, + "mark": null, + "x": 886, + "y0": 886, + "y1": 1772, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 887, + "mark": null, + "x": 887, + "y0": 887, + "y1": 1774, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 888, + "mark": null, + "x": 888, + "y0": 888, + "y1": 1776, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 889, + "mark": null, + "x": 889, + "y0": 889, + "y1": 1778, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 890, + "mark": null, + "x": 890, + "y0": 890, + "y1": 1780, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 891, + "mark": null, + "x": 891, + "y0": 891, + "y1": 1782, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 892, + "mark": null, + "x": 892, + "y0": 892, + "y1": 1784, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 893, + "mark": null, + "x": 893, + "y0": 893, + "y1": 1786, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 894, + "mark": null, + "x": 894, + "y0": 894, + "y1": 1788, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 895, + "mark": null, + "x": 895, + "y0": 895, + "y1": 1790, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 896, + "mark": null, + "x": 896, + "y0": 896, + "y1": 1792, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 897, + "mark": null, + "x": 897, + "y0": 897, + "y1": 1794, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 898, + "mark": null, + "x": 898, + "y0": 898, + "y1": 1796, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 899, + "mark": null, + "x": 899, + "y0": 899, + "y1": 1798, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 900, + "mark": null, + "x": 900, + "y0": 900, + "y1": 1800, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 901, + "mark": null, + "x": 901, + "y0": 901, + "y1": 1802, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 902, + "mark": null, + "x": 902, + "y0": 902, + "y1": 1804, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 903, + "mark": null, + "x": 903, + "y0": 903, + "y1": 1806, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 904, + "mark": null, + "x": 904, + "y0": 904, + "y1": 1808, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 905, + "mark": null, + "x": 905, + "y0": 905, + "y1": 1810, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 906, + "mark": null, + "x": 906, + "y0": 906, + "y1": 1812, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 907, + "mark": null, + "x": 907, + "y0": 907, + "y1": 1814, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 908, + "mark": null, + "x": 908, + "y0": 908, + "y1": 1816, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 909, + "mark": null, + "x": 909, + "y0": 909, + "y1": 1818, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 910, + "mark": null, + "x": 910, + "y0": 910, + "y1": 1820, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 911, + "mark": null, + "x": 911, + "y0": 911, + "y1": 1822, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 912, + "mark": null, + "x": 912, + "y0": 912, + "y1": 1824, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 913, + "mark": null, + "x": 913, + "y0": 913, + "y1": 1826, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 914, + "mark": null, + "x": 914, + "y0": 914, + "y1": 1828, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 915, + "mark": null, + "x": 915, + "y0": 915, + "y1": 1830, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 916, + "mark": null, + "x": 916, + "y0": 916, + "y1": 1832, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 917, + "mark": null, + "x": 917, + "y0": 917, + "y1": 1834, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 918, + "mark": null, + "x": 918, + "y0": 918, + "y1": 1836, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 919, + "mark": null, + "x": 919, + "y0": 919, + "y1": 1838, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 920, + "mark": null, + "x": 920, + "y0": 920, + "y1": 1840, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 921, + "mark": null, + "x": 921, + "y0": 921, + "y1": 1842, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 922, + "mark": null, + "x": 922, + "y0": 922, + "y1": 1844, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 923, + "mark": null, + "x": 923, + "y0": 923, + "y1": 1846, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 924, + "mark": null, + "x": 924, + "y0": 924, + "y1": 1848, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 925, + "mark": null, + "x": 925, + "y0": 925, + "y1": 1850, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 926, + "mark": null, + "x": 926, + "y0": 926, + "y1": 1852, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 927, + "mark": null, + "x": 927, + "y0": 927, + "y1": 1854, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 928, + "mark": null, + "x": 928, + "y0": 928, + "y1": 1856, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 929, + "mark": null, + "x": 929, + "y0": 929, + "y1": 1858, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 930, + "mark": null, + "x": 930, + "y0": 930, + "y1": 1860, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 931, + "mark": null, + "x": 931, + "y0": 931, + "y1": 1862, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 932, + "mark": null, + "x": 932, + "y0": 932, + "y1": 1864, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 933, + "mark": null, + "x": 933, + "y0": 933, + "y1": 1866, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 934, + "mark": null, + "x": 934, + "y0": 934, + "y1": 1868, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 935, + "mark": null, + "x": 935, + "y0": 935, + "y1": 1870, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 936, + "mark": null, + "x": 936, + "y0": 936, + "y1": 1872, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 937, + "mark": null, + "x": 937, + "y0": 937, + "y1": 1874, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 938, + "mark": null, + "x": 938, + "y0": 938, + "y1": 1876, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 939, + "mark": null, + "x": 939, + "y0": 939, + "y1": 1878, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 940, + "mark": null, + "x": 940, + "y0": 940, + "y1": 1880, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 941, + "mark": null, + "x": 941, + "y0": 941, + "y1": 1882, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 942, + "mark": null, + "x": 942, + "y0": 942, + "y1": 1884, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 943, + "mark": null, + "x": 943, + "y0": 943, + "y1": 1886, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 944, + "mark": null, + "x": 944, + "y0": 944, + "y1": 1888, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 945, + "mark": null, + "x": 945, + "y0": 945, + "y1": 1890, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 946, + "mark": null, + "x": 946, + "y0": 946, + "y1": 1892, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 947, + "mark": null, + "x": 947, + "y0": 947, + "y1": 1894, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 948, + "mark": null, + "x": 948, + "y0": 948, + "y1": 1896, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 949, + "mark": null, + "x": 949, + "y0": 949, + "y1": 1898, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 950, + "mark": null, + "x": 950, + "y0": 950, + "y1": 1900, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 951, + "mark": null, + "x": 951, + "y0": 951, + "y1": 1902, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 952, + "mark": null, + "x": 952, + "y0": 952, + "y1": 1904, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 953, + "mark": null, + "x": 953, + "y0": 953, + "y1": 1906, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 954, + "mark": null, + "x": 954, + "y0": 954, + "y1": 1908, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 955, + "mark": null, + "x": 955, + "y0": 955, + "y1": 1910, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 956, + "mark": null, + "x": 956, + "y0": 956, + "y1": 1912, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 957, + "mark": null, + "x": 957, + "y0": 957, + "y1": 1914, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 958, + "mark": null, + "x": 958, + "y0": 958, + "y1": 1916, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 959, + "mark": null, + "x": 959, + "y0": 959, + "y1": 1918, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 960, + "mark": null, + "x": 960, + "y0": 960, + "y1": 1920, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 961, + "mark": null, + "x": 961, + "y0": 961, + "y1": 1922, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 962, + "mark": null, + "x": 962, + "y0": 962, + "y1": 1924, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 963, + "mark": null, + "x": 963, + "y0": 963, + "y1": 1926, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 964, + "mark": null, + "x": 964, + "y0": 964, + "y1": 1928, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 965, + "mark": null, + "x": 965, + "y0": 965, + "y1": 1930, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 966, + "mark": null, + "x": 966, + "y0": 966, + "y1": 1932, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 967, + "mark": null, + "x": 967, + "y0": 967, + "y1": 1934, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 968, + "mark": null, + "x": 968, + "y0": 968, + "y1": 1936, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 969, + "mark": null, + "x": 969, + "y0": 969, + "y1": 1938, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 970, + "mark": null, + "x": 970, + "y0": 970, + "y1": 1940, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 971, + "mark": null, + "x": 971, + "y0": 971, + "y1": 1942, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 972, + "mark": null, + "x": 972, + "y0": 972, + "y1": 1944, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 973, + "mark": null, + "x": 973, + "y0": 973, + "y1": 1946, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 974, + "mark": null, + "x": 974, + "y0": 974, + "y1": 1948, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 975, + "mark": null, + "x": 975, + "y0": 975, + "y1": 1950, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 976, + "mark": null, + "x": 976, + "y0": 976, + "y1": 1952, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 977, + "mark": null, + "x": 977, + "y0": 977, + "y1": 1954, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 978, + "mark": null, + "x": 978, + "y0": 978, + "y1": 1956, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 979, + "mark": null, + "x": 979, + "y0": 979, + "y1": 1958, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 980, + "mark": null, + "x": 980, + "y0": 980, + "y1": 1960, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 981, + "mark": null, + "x": 981, + "y0": 981, + "y1": 1962, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 982, + "mark": null, + "x": 982, + "y0": 982, + "y1": 1964, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 983, + "mark": null, + "x": 983, + "y0": 983, + "y1": 1966, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 984, + "mark": null, + "x": 984, + "y0": 984, + "y1": 1968, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 985, + "mark": null, + "x": 985, + "y0": 985, + "y1": 1970, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 986, + "mark": null, + "x": 986, + "y0": 986, + "y1": 1972, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 987, + "mark": null, + "x": 987, + "y0": 987, + "y1": 1974, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 988, + "mark": null, + "x": 988, + "y0": 988, + "y1": 1976, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 989, + "mark": null, + "x": 989, + "y0": 989, + "y1": 1978, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 990, + "mark": null, + "x": 990, + "y0": 990, + "y1": 1980, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 991, + "mark": null, + "x": 991, + "y0": 991, + "y1": 1982, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 992, + "mark": null, + "x": 992, + "y0": 992, + "y1": 1984, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 993, + "mark": null, + "x": 993, + "y0": 993, + "y1": 1986, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 994, + "mark": null, + "x": 994, + "y0": 994, + "y1": 1988, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 995, + "mark": null, + "x": 995, + "y0": 995, + "y1": 1990, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 996, + "mark": null, + "x": 996, + "y0": 996, + "y1": 1992, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 997, + "mark": null, + "x": 997, + "y0": 997, + "y1": 1994, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 998, + "mark": null, + "x": 998, + "y0": 998, + "y1": 1996, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 999, + "mark": null, + "x": 999, + "y0": 999, + "y1": 1998, + }, + ], + "key": "b", + "seriesKeys": Array [ + "b", + ], + "specId": "spec1", + "splitAccessors": Map {}, + "yAccessor": "y1", + }, +] +`; + +exports[`Series Can stack multiple dataseries 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 1, + "mark": null, + "x": 1, + "y0": 0, + "y1": 1, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 2, + "y0": 0, + "y1": 2, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 3, + "mark": null, + "x": 3, + "y0": 0, + "y1": 3, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 4, + "mark": null, + "x": 4, + "y0": 0, + "y1": 4, + }, + ], + "key": "a", + "seriesKeys": Array [ + "a", + ], + "specId": "spec1", + "splitAccessors": Map {}, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 1, + "mark": null, + "x": 1, + "y0": 1, + "y1": 2, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 2, + "y0": 2, + "y1": 4, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 3, + "mark": null, + "x": 3, + "y0": 3, + "y1": 6, + }, + Object { + "datum": undefined, + "filled": undefined, + "initialY0": null, + "initialY1": 4, + "mark": null, + "x": 4, + "y0": 4, + "y1": 8, + }, + ], + "key": "b", + "seriesKeys": Array [ + "b", + ], + "specId": "spec1", + "splitAccessors": Map {}, + "yAccessor": "y1", + }, +] +`; + +exports[`Series Can stack multiple dataseries with scale to extent 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g": "a", + "x": 1, + "y1": 1, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 1, + "mark": null, + "x": 1, + "y0": 0, + "y1": 1, + }, + Object { + "datum": Object { + "g": "a", + "x": 2, + "y1": 2, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 2, + "y0": 0, + "y1": 2, + }, + Object { + "datum": Object { + "g": "a", + "x": 3, + "y1": 3, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 3, + "mark": null, + "x": 3, + "y0": 0, + "y1": 3, + }, + Object { + "datum": Object { + "g": "a", + "x": 4, + "y1": 4, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 4, + "mark": null, + "x": 4, + "y0": 0, + "y1": 4, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-a}", + "seriesKeys": Array [ + "a", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "a", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g": "b", + "x": 1, + "y1": 1, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 1, + "mark": null, + "x": 1, + "y0": 1, + "y1": 2, + }, + Object { + "datum": Object { + "g": "b", + "x": 2, + "y1": 2, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 2, + "y0": 2, + "y1": 4, + }, + Object { + "datum": Object { + "g": "b", + "x": 3, + "y1": 3, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 3, + "mark": null, + "x": 3, + "y0": 3, + "y1": 6, + }, + Object { + "datum": Object { + "g": "b", + "x": 4, + "y1": 4, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 4, + "mark": null, + "x": 4, + "y0": 4, + "y1": 8, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-b}", + "seriesKeys": Array [ + "b", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "b", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g": "c", + "x": 1, + "y1": 1, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 1, + "mark": null, + "x": 1, + "y0": 2, + "y1": 3, + }, + Object { + "datum": Object { + "g": "c", + "x": 2, + "y1": 2, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 2, + "y0": 4, + "y1": 6, + }, + Object { + "datum": Object { + "g": "c", + "x": 3, + "y1": 3, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 3, + "mark": null, + "x": 3, + "y0": 6, + "y1": 9, + }, + Object { + "datum": Object { + "g": "c", + "x": 4, + "y1": 4, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 4, + "mark": null, + "x": 4, + "y0": 8, + "y1": 12, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-c}", + "seriesKeys": Array [ + "c", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "c", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g": "d", + "x": 1, + "y1": 1, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 1, + "mark": null, + "x": 1, + "y0": 3, + "y1": 4, + }, + Object { + "datum": Object { + "g": "d", + "x": 2, + "y1": 2, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 2, + "y0": 6, + "y1": 8, + }, + Object { + "datum": Object { + "g": "d", + "x": 3, + "y1": 3, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 3, + "mark": null, + "x": 3, + "y0": 9, + "y1": 12, + }, + Object { + "datum": Object { + "g": "d", + "x": 4, + "y1": 4, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 4, + "mark": null, + "x": 4, + "y0": 12, + "y1": 16, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-d}", + "seriesKeys": Array [ + "d", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "d", + }, + "yAccessor": "y1", + }, +] +`; + +exports[`Series Can stack simple dataseries 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g": "a", + "x": 1, + "y1": 1, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 1, + "mark": null, + "x": 1, + "y0": 0, + "y1": 1, + }, + Object { + "datum": Object { + "g": "a", + "x": 2, + "y1": 2, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 2, + "y0": 0, + "y1": 2, + }, + Object { + "datum": undefined, + "filled": Object { + "x": 3, + }, + "initialY0": null, + "initialY1": null, + "mark": null, + "x": 3, + "y0": 0, + "y1": 0, + }, + Object { + "datum": Object { + "g": "a", + "x": 4, + "y1": 4, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 4, + "mark": null, + "x": 4, + "y0": 0, + "y1": 4, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-a}", + "seriesKeys": Array [ + "a", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "a", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g": "b", + "x": 1, + "y1": 21, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 21, + "mark": null, + "x": 1, + "y0": 1, + "y1": 22, + }, + Object { + "datum": undefined, + "filled": Object { + "x": 2, + }, + "initialY0": null, + "initialY1": null, + "mark": null, + "x": 2, + "y0": 2, + "y1": 2, + }, + Object { + "datum": Object { + "g": "b", + "x": 3, + "y1": 23, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 23, + "mark": null, + "x": 3, + "y0": 0, + "y1": 23, + }, + Object { + "datum": undefined, + "filled": Object { + "x": 4, + }, + "initialY0": null, + "initialY1": null, + "mark": null, + "x": 4, + "y0": 4, + "y1": 4, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-b}", + "seriesKeys": Array [ + "b", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "b", + }, + "yAccessor": "y1", + }, +] +`; + +exports[`Series Can stack simple dataseries with scale to extent 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g": "a", + "x": 1, + "y1": 1, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 1, + "mark": null, + "x": 1, + "y0": 0, + "y1": 1, + }, + Object { + "datum": Object { + "g": "a", + "x": 2, + "y1": 2, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 2, + "y0": 0, + "y1": 2, + }, + Object { + "datum": undefined, + "filled": Object { + "x": 3, + }, + "initialY0": null, + "initialY1": null, + "mark": null, + "x": 3, + "y0": 0, + "y1": 0, + }, + Object { + "datum": Object { + "g": "a", + "x": 4, + "y1": 4, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 4, + "mark": null, + "x": 4, + "y0": 0, + "y1": 4, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-a}", + "seriesKeys": Array [ + "a", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "a", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g": "b", + "x": 1, + "y1": 21, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 21, + "mark": null, + "x": 1, + "y0": 1, + "y1": 22, + }, + Object { + "datum": undefined, + "filled": Object { + "x": 2, + }, + "initialY0": null, + "initialY1": null, + "mark": null, + "x": 2, + "y0": 2, + "y1": 2, + }, + Object { + "datum": Object { + "g": "b", + "x": 3, + "y1": 23, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 23, + "mark": null, + "x": 3, + "y0": 0, + "y1": 23, + }, + Object { + "datum": undefined, + "filled": Object { + "x": 4, + }, + "initialY0": null, + "initialY1": null, + "mark": null, + "x": 4, + "y0": 4, + "y1": 4, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-b}", + "seriesKeys": Array [ + "b", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "b", + }, + "yAccessor": "y1", + }, +] +`; + +exports[`Series Can stack simple dataseries with scale to extent with y0 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g": "a", + "x": 1, + "y0": 1, + "y1": 3, + }, + "filled": undefined, + "initialY0": 1, + "initialY1": 3, + "mark": null, + "x": 1, + "y0": 1, + "y1": 3, + }, + Object { + "datum": Object { + "g": "a", + "x": 2, + "y0": 2, + "y1": 3, + }, + "filled": undefined, + "initialY0": 2, + "initialY1": 3, + "mark": null, + "x": 2, + "y0": 2, + "y1": 3, + }, + Object { + "datum": undefined, + "filled": Object { + "x": 3, + }, + "initialY0": null, + "initialY1": null, + "mark": null, + "x": 3, + "y0": 0, + "y1": 0, + }, + Object { + "datum": Object { + "g": "a", + "x": 4, + "y0": 3, + "y1": 4, + }, + "filled": undefined, + "initialY0": 3, + "initialY1": 4, + "mark": null, + "x": 4, + "y0": 3, + "y1": 4, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-a}", + "seriesKeys": Array [ + "a", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "a", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g": "b", + "x": 1, + "y0": 1, + "y1": 2, + }, + "filled": undefined, + "initialY0": 1, + "initialY1": 2, + "mark": null, + "x": 1, + "y0": 4, + "y1": 5, + }, + Object { + "datum": Object { + "g": "b", + "x": 2, + "y0": 1, + "y1": 3, + }, + "filled": undefined, + "initialY0": 1, + "initialY1": 3, + "mark": null, + "x": 2, + "y0": 4, + "y1": 6, + }, + Object { + "datum": Object { + "g": "b", + "x": 3, + "y0": 4, + "y1": 23, + }, + "filled": undefined, + "initialY0": 4, + "initialY1": 23, + "mark": null, + "x": 3, + "y0": 4, + "y1": 23, + }, + Object { + "datum": Object { + "g": "b", + "x": 4, + "y0": 1, + "y1": 4, + }, + "filled": undefined, + "initialY0": 1, + "initialY1": 4, + "mark": null, + "x": 4, + "y0": 5, + "y1": 8, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-b}", + "seriesKeys": Array [ + "b", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "b", + }, + "yAccessor": "y1", + }, +] +`; + +exports[`Series Can stack simple dataseries with y0 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g": "a", + "x": 1, + "y0": 1, + "y1": 3, + }, + "filled": undefined, + "initialY0": 1, + "initialY1": 3, + "mark": null, + "x": 1, + "y0": 1, + "y1": 3, + }, + Object { + "datum": Object { + "g": "a", + "x": 2, + "y0": 2, + "y1": 3, + }, + "filled": undefined, + "initialY0": 2, + "initialY1": 3, + "mark": null, + "x": 2, + "y0": 2, + "y1": 3, + }, + Object { + "datum": undefined, + "filled": Object { + "x": 3, + }, + "initialY0": null, + "initialY1": null, + "mark": null, + "x": 3, + "y0": 0, + "y1": 0, + }, + Object { + "datum": Object { + "g": "a", + "x": 4, + "y0": 3, + "y1": 4, + }, + "filled": undefined, + "initialY0": 3, + "initialY1": 4, + "mark": null, + "x": 4, + "y0": 3, + "y1": 4, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-a}", + "seriesKeys": Array [ + "a", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "a", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g": "b", + "x": 1, + "y0": 1, + "y1": 2, + }, + "filled": undefined, + "initialY0": 1, + "initialY1": 2, + "mark": null, + "x": 1, + "y0": 4, + "y1": 5, + }, + Object { + "datum": Object { + "g": "b", + "x": 2, + "y0": 1, + "y1": 3, + }, + "filled": undefined, + "initialY0": 1, + "initialY1": 3, + "mark": null, + "x": 2, + "y0": 4, + "y1": 6, + }, + Object { + "datum": Object { + "g": "b", + "x": 3, + "y0": 4, + "y1": 23, + }, + "filled": undefined, + "initialY0": 4, + "initialY1": 23, + "mark": null, + "x": 3, + "y0": 4, + "y1": 23, + }, + Object { + "datum": Object { + "g": "b", + "x": 4, + "y0": 1, + "y1": 4, + }, + "filled": undefined, + "initialY0": 1, + "initialY1": 4, + "mark": null, + "x": 4, + "y0": 5, + "y1": 8, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-b}", + "seriesKeys": Array [ + "b", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "b", + }, + "yAccessor": "y1", + }, +] +`; + +exports[`Series Can stack unsorted dataseries 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g": "a", + "x": 1, + "y1": 1, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 1, + "mark": null, + "x": 1, + "y0": 0, + "y1": 1, + }, + Object { + "datum": Object { + "g": "a", + "x": 2, + "y1": 2, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 2, + "y0": 0, + "y1": 2, + }, + Object { + "datum": undefined, + "filled": Object { + "x": 3, + }, + "initialY0": null, + "initialY1": null, + "mark": null, + "x": 3, + "y0": 0, + "y1": 0, + }, + Object { + "datum": Object { + "g": "a", + "x": 4, + "y1": 4, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 4, + "mark": null, + "x": 4, + "y0": 0, + "y1": 4, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-a}", + "seriesKeys": Array [ + "a", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "a", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g": "b", + "x": 1, + "y1": 21, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 21, + "mark": null, + "x": 1, + "y0": 1, + "y1": 22, + }, + Object { + "datum": undefined, + "filled": Object { + "x": 2, + }, + "initialY0": null, + "initialY1": null, + "mark": null, + "x": 2, + "y0": 2, + "y1": 2, + }, + Object { + "datum": Object { + "g": "b", + "x": 3, + "y1": 23, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 23, + "mark": null, + "x": 3, + "y0": 0, + "y1": 23, + }, + Object { + "datum": undefined, + "filled": Object { + "x": 4, + }, + "initialY0": null, + "initialY1": null, + "mark": null, + "x": 4, + "y0": 4, + "y1": 4, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-b}", + "seriesKeys": Array [ + "b", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "b", + }, + "yAccessor": "y1", + }, +] +`; + +exports[`Series functional accessors Can use default custom xAccessor 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 7, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g1-cdn.google.com}", + "seriesKeys": Array [ + "cdn.google.com", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cdn.google.com", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": "_all", + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g1-cloudflare.com}", + "seriesKeys": Array [ + "cloudflare.com", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cloudflare.com", + }, + "yAccessor": "y1", + }, +] +`; + +exports[`Series functional accessors Can use functional splitSeriesAccessor 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 7, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{(index:0)-cdn.google.com}", + "seriesKeys": Array [ + "cdn.google.com", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "(index:0)" => "cdn.google.com", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{(index:0)-cloudflare.com}", + "seriesKeys": Array [ + "cloudflare.com", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "(index:0)" => "cloudflare.com", + }, + "yAccessor": "y1", + }, +] +`; + +exports[`Series functional accessors Can use functional splitSeriesAccessor with fieldName 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 7, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{custom name-cdn.google.com}", + "seriesKeys": Array [ + "cdn.google.com", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "custom name" => "cdn.google.com", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{custom name-cloudflare.com}", + "seriesKeys": Array [ + "cloudflare.com", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "custom name" => "cloudflare.com", + }, + "yAccessor": "y1", + }, +] +`; + +exports[`Series functional accessors Can use functional xAccessor 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 7, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g1-cdn.google.com}", + "seriesKeys": Array [ + "cdn.google.com", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cdn.google.com", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 5, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 5, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 5, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 5, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 3, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g1-cdn.google.com}", + "seriesKeys": Array [ + "cdn.google.com", + "y2", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cdn.google.com", + }, + "yAccessor": "y2", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g1-cloudflare.com}", + "seriesKeys": Array [ + "cloudflare.com", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cloudflare.com", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 5, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 5, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 5, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 5, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 4, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g1-cloudflare.com}", + "seriesKeys": Array [ + "cloudflare.com", + "y2", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cloudflare.com", + }, + "yAccessor": "y2", + }, +] +`; + +exports[`Series functional accessors Can use functional y0Accessor 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "max": 16.3203125, + "min": 0.3203125, + "x": 1551438000000, + }, + "initialY0": 0.3203125, + "initialY1": 16.3203125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438000000, + "y0": 0.3203125, + "y1": 16.3203125, + }, + Object { + "datum": Object { + "max": 11.9140625, + "min": -0.0859375, + "x": 1551438030000, + }, + "initialY0": -0.0859375, + "initialY1": 11.9140625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438030000, + "y0": -0.0859375, + "y1": 11.9140625, + }, + Object { + "datum": Object { + "max": 11.8671875, + "min": -0.1328125, + "x": 1551438060000, + }, + "initialY0": -0.1328125, + "initialY1": 11.8671875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438060000, + "y0": -0.1328125, + "y1": 11.8671875, + }, + Object { + "datum": Object { + "max": 11.125, + "min": -0.875, + "x": 1551438090000, + }, + "initialY0": -0.875, + "initialY1": 11.125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438090000, + "y0": -0.875, + "y1": 11.125, + }, + Object { + "datum": Object { + "max": 12.765625, + "min": 0.765625, + "x": 1551438120000, + }, + "initialY0": 0.765625, + "initialY1": 12.765625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438120000, + "y0": 0.765625, + "y1": 12.765625, + }, + Object { + "datum": Object { + "max": 19.546875, + "min": 7.546875, + "x": 1551438150000, + }, + "initialY0": 7.546875, + "initialY1": 19.546875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438150000, + "y0": 7.546875, + "y1": 19.546875, + }, + Object { + "datum": Object { + "max": 20.984375, + "min": 4.984375, + "x": 1551438180000, + }, + "initialY0": 4.984375, + "initialY1": 20.984375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438180000, + "y0": 4.984375, + "y1": 20.984375, + }, + Object { + "datum": Object { + "max": 21.546875, + "min": 5.546875, + "x": 1551438210000, + }, + "initialY0": 5.546875, + "initialY1": 21.546875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438210000, + "y0": 5.546875, + "y1": 21.546875, + }, + Object { + "datum": Object { + "max": 17.390625, + "min": 5.390625, + "x": 1551438240000, + }, + "initialY0": 5.390625, + "initialY1": 17.390625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438240000, + "y0": 5.390625, + "y1": 17.390625, + }, + Object { + "datum": Object { + "max": 19.5625, + "min": 3.5625, + "x": 1551438270000, + }, + "initialY0": 3.5625, + "initialY1": 19.5625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438270000, + "y0": 3.5625, + "y1": 19.5625, + }, + Object { + "datum": Object { + "max": 19.5859375, + "min": 3.5859375, + "x": 1551438300000, + }, + "initialY0": 3.5859375, + "initialY1": 19.5859375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438300000, + "y0": 3.5859375, + "y1": 19.5859375, + }, + Object { + "datum": Object { + "max": 14.0546875, + "min": 6.0546875, + "x": 1551438330000, + }, + "initialY0": 6.0546875, + "initialY1": 14.0546875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438330000, + "y0": 6.0546875, + "y1": 14.0546875, + }, + Object { + "datum": Object { + "max": 13.921875, + "min": 5.921875, + "x": 1551438360000, + }, + "initialY0": 5.921875, + "initialY1": 13.921875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438360000, + "y0": 5.921875, + "y1": 13.921875, + }, + Object { + "datum": Object { + "max": 13.4921875, + "min": 5.4921875, + "x": 1551438390000, + }, + "initialY0": 5.4921875, + "initialY1": 13.4921875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438390000, + "y0": 5.4921875, + "y1": 13.4921875, + }, + Object { + "datum": Object { + "max": 17.78125, + "min": 1.78125, + "x": 1551438420000, + }, + "initialY0": 1.78125, + "initialY1": 17.78125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438420000, + "y0": 1.78125, + "y1": 17.78125, + }, + Object { + "datum": Object { + "max": 18.046875, + "min": 2.046875, + "x": 1551438450000, + }, + "initialY0": 2.046875, + "initialY1": 18.046875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438450000, + "y0": 2.046875, + "y1": 18.046875, + }, + Object { + "datum": Object { + "max": 22.0546875, + "min": 6.0546875, + "x": 1551438480000, + }, + "initialY0": 6.0546875, + "initialY1": 22.0546875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438480000, + "y0": 6.0546875, + "y1": 22.0546875, + }, + Object { + "datum": Object { + "max": 18.640625, + "min": 6.640625, + "x": 1551438510000, + }, + "initialY0": 6.640625, + "initialY1": 18.640625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438510000, + "y0": 6.640625, + "y1": 18.640625, + }, + Object { + "datum": Object { + "max": 16.2421875, + "min": 0.2421875, + "x": 1551438540000, + }, + "initialY0": 0.2421875, + "initialY1": 16.2421875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438540000, + "y0": 0.2421875, + "y1": 16.2421875, + }, + Object { + "datum": Object { + "max": 12.5, + "min": 4.5, + "x": 1551438570000, + }, + "initialY0": 4.5, + "initialY1": 12.5, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438570000, + "y0": 4.5, + "y1": 12.5, + }, + Object { + "datum": Object { + "max": 11.2578125, + "min": 3.2578125, + "x": 1551438600000, + }, + "initialY0": 3.2578125, + "initialY1": 11.2578125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438600000, + "y0": 3.2578125, + "y1": 11.2578125, + }, + Object { + "datum": Object { + "max": 16.515625, + "min": 4.515625, + "x": 1551438630000, + }, + "initialY0": 4.515625, + "initialY1": 16.515625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438630000, + "y0": 4.515625, + "y1": 16.515625, + }, + Object { + "datum": Object { + "max": 18.796875, + "min": 2.796875, + "x": 1551438660000, + }, + "initialY0": 2.796875, + "initialY1": 18.796875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438660000, + "y0": 2.796875, + "y1": 18.796875, + }, + Object { + "datum": Object { + "max": 19.125, + "min": 3.125, + "x": 1551438690000, + }, + "initialY0": 3.125, + "initialY1": 19.125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438690000, + "y0": 3.125, + "y1": 19.125, + }, + Object { + "datum": Object { + "max": 25.40625, + "min": 17.40625, + "x": 1551438720000, + }, + "initialY0": 17.40625, + "initialY1": 25.40625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438720000, + "y0": 17.40625, + "y1": 25.40625, + }, + Object { + "datum": Object { + "max": 25.921875, + "min": 13.921875, + "x": 1551438750000, + }, + "initialY0": 13.921875, + "initialY1": 25.921875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438750000, + "y0": 13.921875, + "y1": 25.921875, + }, + Object { + "datum": Object { + "max": 34.640625, + "min": 22.640625, + "x": 1551438780000, + }, + "initialY0": 22.640625, + "initialY1": 34.640625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438780000, + "y0": 22.640625, + "y1": 34.640625, + }, + Object { + "datum": Object { + "max": 35.390625, + "min": 23.390625, + "x": 1551438810000, + }, + "initialY0": 23.390625, + "initialY1": 35.390625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438810000, + "y0": 23.390625, + "y1": 35.390625, + }, + Object { + "datum": Object { + "max": 27.953125, + "min": 15.953125, + "x": 1551438840000, + }, + "initialY0": 15.953125, + "initialY1": 27.953125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438840000, + "y0": 15.953125, + "y1": 27.953125, + }, + Object { + "datum": Object { + "max": 24, + "min": 12, + "x": 1551438870000, + }, + "initialY0": 12, + "initialY1": 24, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438870000, + "y0": 12, + "y1": 24, + }, + Object { + "datum": Object { + "max": 15.9765625, + "min": 7.9765625, + "x": 1551438900000, + }, + "initialY0": 7.9765625, + "initialY1": 15.9765625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438900000, + "y0": 7.9765625, + "y1": 15.9765625, + }, + Object { + "datum": Object { + "max": 17.1640625, + "min": 5.1640625, + "x": 1551438930000, + }, + "initialY0": 5.1640625, + "initialY1": 17.1640625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438930000, + "y0": 5.1640625, + "y1": 17.1640625, + }, + Object { + "datum": Object { + "max": 11.98046875, + "min": 3.98046875, + "x": 1551438960000, + }, + "initialY0": 3.98046875, + "initialY1": 11.98046875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438960000, + "y0": 3.98046875, + "y1": 11.98046875, + }, + Object { + "datum": Object { + "max": 15.1640625, + "min": 3.1640625, + "x": 1551438990000, + }, + "initialY0": 3.1640625, + "initialY1": 15.1640625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551438990000, + "y0": 3.1640625, + "y1": 15.1640625, + }, + Object { + "datum": Object { + "max": 11.39453125, + "min": -0.60546875, + "x": 1551439020000, + }, + "initialY0": -0.60546875, + "initialY1": 11.39453125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439020000, + "y0": -0.60546875, + "y1": 11.39453125, + }, + Object { + "datum": Object { + "max": 9.68359375, + "min": -2.31640625, + "x": 1551439050000, + }, + "initialY0": -2.31640625, + "initialY1": 9.68359375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439050000, + "y0": -2.31640625, + "y1": 9.68359375, + }, + Object { + "datum": Object { + "max": 8.95703125, + "min": -3.04296875, + "x": 1551439080000, + }, + "initialY0": -3.04296875, + "initialY1": 8.95703125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439080000, + "y0": -3.04296875, + "y1": 8.95703125, + }, + Object { + "datum": Object { + "max": 12.26171875, + "min": 0.26171875, + "x": 1551439110000, + }, + "initialY0": 0.26171875, + "initialY1": 12.26171875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439110000, + "y0": 0.26171875, + "y1": 12.26171875, + }, + Object { + "datum": Object { + "max": 15.1171875, + "min": 3.1171875, + "x": 1551439140000, + }, + "initialY0": 3.1171875, + "initialY1": 15.1171875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439140000, + "y0": 3.1171875, + "y1": 15.1171875, + }, + Object { + "datum": Object { + "max": 18.8515625, + "min": 6.8515625, + "x": 1551439170000, + }, + "initialY0": 6.8515625, + "initialY1": 18.8515625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439170000, + "y0": 6.8515625, + "y1": 18.8515625, + }, + Object { + "datum": Object { + "max": 20.6171875, + "min": 4.6171875, + "x": 1551439200000, + }, + "initialY0": 4.6171875, + "initialY1": 20.6171875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439200000, + "y0": 4.6171875, + "y1": 20.6171875, + }, + Object { + "datum": Object { + "max": 15.1171875, + "min": 7.1171875, + "x": 1551439230000, + }, + "initialY0": 7.1171875, + "initialY1": 15.1171875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439230000, + "y0": 7.1171875, + "y1": 15.1171875, + }, + Object { + "datum": Object { + "max": 19.6640625, + "min": 7.6640625, + "x": 1551439260000, + }, + "initialY0": 7.6640625, + "initialY1": 19.6640625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439260000, + "y0": 7.6640625, + "y1": 19.6640625, + }, + Object { + "datum": Object { + "max": 19.109375, + "min": 7.109375, + "x": 1551439290000, + }, + "initialY0": 7.109375, + "initialY1": 19.109375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439290000, + "y0": 7.109375, + "y1": 19.109375, + }, + Object { + "datum": Object { + "max": 18.6015625, + "min": 6.6015625, + "x": 1551439320000, + }, + "initialY0": 6.6015625, + "initialY1": 18.6015625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439320000, + "y0": 6.6015625, + "y1": 18.6015625, + }, + Object { + "datum": Object { + "max": 19.21875, + "min": 3.21875, + "x": 1551439350000, + }, + "initialY0": 3.21875, + "initialY1": 19.21875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439350000, + "y0": 3.21875, + "y1": 19.21875, + }, + Object { + "datum": Object { + "max": 21.53125, + "min": 9.53125, + "x": 1551439380000, + }, + "initialY0": 9.53125, + "initialY1": 21.53125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439380000, + "y0": 9.53125, + "y1": 21.53125, + }, + Object { + "datum": Object { + "max": 23.4609375, + "min": 11.4609375, + "x": 1551439410000, + }, + "initialY0": 11.4609375, + "initialY1": 23.4609375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439410000, + "y0": 11.4609375, + "y1": 23.4609375, + }, + Object { + "datum": Object { + "max": 19.1796875, + "min": 11.1796875, + "x": 1551439440000, + }, + "initialY0": 11.1796875, + "initialY1": 19.1796875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439440000, + "y0": 11.1796875, + "y1": 19.1796875, + }, + Object { + "datum": Object { + "max": 15.984375, + "min": 7.984375, + "x": 1551439470000, + }, + "initialY0": 7.984375, + "initialY1": 15.984375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439470000, + "y0": 7.984375, + "y1": 15.984375, + }, + Object { + "datum": Object { + "max": 32.8125, + "min": 20.8125, + "x": 1551439500000, + }, + "initialY0": 20.8125, + "initialY1": 32.8125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439500000, + "y0": 20.8125, + "y1": 32.8125, + }, + Object { + "datum": Object { + "max": 25.46875, + "min": 17.46875, + "x": 1551439530000, + }, + "initialY0": 17.46875, + "initialY1": 25.46875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439530000, + "y0": 17.46875, + "y1": 25.46875, + }, + Object { + "datum": Object { + "max": 22.484375, + "min": 6.484375, + "x": 1551439560000, + }, + "initialY0": 6.484375, + "initialY1": 22.484375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439560000, + "y0": 6.484375, + "y1": 22.484375, + }, + Object { + "datum": Object { + "max": 17.9609375, + "min": 5.9609375, + "x": 1551439590000, + }, + "initialY0": 5.9609375, + "initialY1": 17.9609375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439590000, + "y0": 5.9609375, + "y1": 17.9609375, + }, + Object { + "datum": Object { + "max": 14.8515625, + "min": 2.8515625, + "x": 1551439620000, + }, + "initialY0": 2.8515625, + "initialY1": 14.8515625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439620000, + "y0": 2.8515625, + "y1": 14.8515625, + }, + Object { + "datum": Object { + "max": 16.1171875, + "min": 8.1171875, + "x": 1551439650000, + }, + "initialY0": 8.1171875, + "initialY1": 16.1171875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439650000, + "y0": 8.1171875, + "y1": 16.1171875, + }, + Object { + "datum": Object { + "max": 23.375, + "min": 15.375, + "x": 1551439680000, + }, + "initialY0": 15.375, + "initialY1": 23.375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439680000, + "y0": 15.375, + "y1": 23.375, + }, + Object { + "datum": Object { + "max": 24.609375, + "min": 16.609375, + "x": 1551439710000, + }, + "initialY0": 16.609375, + "initialY1": 24.609375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439710000, + "y0": 16.609375, + "y1": 24.609375, + }, + Object { + "datum": Object { + "max": 20.484375, + "min": 8.484375, + "x": 1551439740000, + }, + "initialY0": 8.484375, + "initialY1": 20.484375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439740000, + "y0": 8.484375, + "y1": 20.484375, + }, + Object { + "datum": Object { + "max": 19.515625, + "min": 11.515625, + "x": 1551439770000, + }, + "initialY0": 11.515625, + "initialY1": 19.515625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439770000, + "y0": 11.515625, + "y1": 19.515625, + }, + Object { + "datum": Object { + "max": 18.9140625, + "min": 10.9140625, + "x": 1551439800000, + }, + "initialY0": 10.9140625, + "initialY1": 18.9140625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439800000, + "y0": 10.9140625, + "y1": 18.9140625, + }, + Object { + "datum": Object { + "max": 14.8828125, + "min": 2.8828125, + "x": 1551439830000, + }, + "initialY0": 2.8828125, + "initialY1": 14.8828125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439830000, + "y0": 2.8828125, + "y1": 14.8828125, + }, + Object { + "datum": Object { + "max": 13.7578125, + "min": 5.7578125, + "x": 1551439860000, + }, + "initialY0": 5.7578125, + "initialY1": 13.7578125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439860000, + "y0": 5.7578125, + "y1": 13.7578125, + }, + Object { + "datum": Object { + "max": 12.625, + "min": 0.625, + "x": 1551439890000, + }, + "initialY0": 0.625, + "initialY1": 12.625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439890000, + "y0": 0.625, + "y1": 12.625, + }, + Object { + "datum": Object { + "max": 13.21875, + "min": 5.21875, + "x": 1551439920000, + }, + "initialY0": 5.21875, + "initialY1": 13.21875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439920000, + "y0": 5.21875, + "y1": 13.21875, + }, + Object { + "datum": Object { + "max": 12.5390625, + "min": 4.5390625, + "x": 1551439950000, + }, + "initialY0": 4.5390625, + "initialY1": 12.5390625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439950000, + "y0": 4.5390625, + "y1": 12.5390625, + }, + Object { + "datum": Object { + "max": 12.40625, + "min": 4.40625, + "x": 1551439980000, + }, + "initialY0": 4.40625, + "initialY1": 12.40625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551439980000, + "y0": 4.40625, + "y1": 12.40625, + }, + Object { + "datum": Object { + "max": 10.671875, + "min": 2.671875, + "x": 1551440010000, + }, + "initialY0": 2.671875, + "initialY1": 10.671875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440010000, + "y0": 2.671875, + "y1": 10.671875, + }, + Object { + "datum": Object { + "max": 15.24609375, + "min": 3.24609375, + "x": 1551440040000, + }, + "initialY0": 3.24609375, + "initialY1": 15.24609375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440040000, + "y0": 3.24609375, + "y1": 15.24609375, + }, + Object { + "datum": Object { + "max": 15.1015625, + "min": -0.8984375, + "x": 1551440070000, + }, + "initialY0": -0.8984375, + "initialY1": 15.1015625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440070000, + "y0": -0.8984375, + "y1": 15.1015625, + }, + Object { + "datum": Object { + "max": 15.09375, + "min": 3.09375, + "x": 1551440100000, + }, + "initialY0": 3.09375, + "initialY1": 15.09375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440100000, + "y0": 3.09375, + "y1": 15.09375, + }, + Object { + "datum": Object { + "max": 14.8125, + "min": 2.8125, + "x": 1551440130000, + }, + "initialY0": 2.8125, + "initialY1": 14.8125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440130000, + "y0": 2.8125, + "y1": 14.8125, + }, + Object { + "datum": Object { + "max": 14.90625, + "min": 2.90625, + "x": 1551440160000, + }, + "initialY0": 2.90625, + "initialY1": 14.90625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440160000, + "y0": 2.90625, + "y1": 14.90625, + }, + Object { + "datum": Object { + "max": 16.453125, + "min": 4.453125, + "x": 1551440190000, + }, + "initialY0": 4.453125, + "initialY1": 16.453125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440190000, + "y0": 4.453125, + "y1": 16.453125, + }, + Object { + "datum": Object { + "max": 19.8984375, + "min": 7.8984375, + "x": 1551440220000, + }, + "initialY0": 7.8984375, + "initialY1": 19.8984375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440220000, + "y0": 7.8984375, + "y1": 19.8984375, + }, + Object { + "datum": Object { + "max": 14.875, + "min": 6.875, + "x": 1551440250000, + }, + "initialY0": 6.875, + "initialY1": 14.875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440250000, + "y0": 6.875, + "y1": 14.875, + }, + Object { + "datum": Object { + "max": 20.4140625, + "min": 4.4140625, + "x": 1551440280000, + }, + "initialY0": 4.4140625, + "initialY1": 20.4140625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440280000, + "y0": 4.4140625, + "y1": 20.4140625, + }, + Object { + "datum": Object { + "max": 20.78125, + "min": 8.78125, + "x": 1551440310000, + }, + "initialY0": 8.78125, + "initialY1": 20.78125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440310000, + "y0": 8.78125, + "y1": 20.78125, + }, + Object { + "datum": Object { + "max": 42.28125, + "min": 26.28125, + "x": 1551440340000, + }, + "initialY0": 26.28125, + "initialY1": 42.28125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440340000, + "y0": 26.28125, + "y1": 42.28125, + }, + Object { + "datum": Object { + "max": 33.84375, + "min": 25.84375, + "x": 1551440370000, + }, + "initialY0": 25.84375, + "initialY1": 33.84375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440370000, + "y0": 25.84375, + "y1": 33.84375, + }, + Object { + "datum": Object { + "max": 26.40625, + "min": 14.40625, + "x": 1551440400000, + }, + "initialY0": 14.40625, + "initialY1": 26.40625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440400000, + "y0": 14.40625, + "y1": 26.40625, + }, + Object { + "datum": Object { + "max": 24.046875, + "min": 12.046875, + "x": 1551440430000, + }, + "initialY0": 12.046875, + "initialY1": 24.046875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440430000, + "y0": 12.046875, + "y1": 24.046875, + }, + Object { + "datum": Object { + "max": 20.6328125, + "min": 8.6328125, + "x": 1551440460000, + }, + "initialY0": 8.6328125, + "initialY1": 20.6328125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440460000, + "y0": 8.6328125, + "y1": 20.6328125, + }, + Object { + "datum": Object { + "max": 16.8125, + "min": 4.8125, + "x": 1551440490000, + }, + "initialY0": 4.8125, + "initialY1": 16.8125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440490000, + "y0": 4.8125, + "y1": 16.8125, + }, + Object { + "datum": Object { + "max": 14.93359375, + "min": -1.06640625, + "x": 1551440520000, + }, + "initialY0": -1.06640625, + "initialY1": 14.93359375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440520000, + "y0": -1.06640625, + "y1": 14.93359375, + }, + Object { + "datum": Object { + "max": 14.12890625, + "min": 2.12890625, + "x": 1551440550000, + }, + "initialY0": 2.12890625, + "initialY1": 14.12890625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440550000, + "y0": 2.12890625, + "y1": 14.12890625, + }, + Object { + "datum": Object { + "max": 13.69921875, + "min": 1.69921875, + "x": 1551440580000, + }, + "initialY0": 1.69921875, + "initialY1": 13.69921875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440580000, + "y0": 1.69921875, + "y1": 13.69921875, + }, + Object { + "datum": Object { + "max": 9.48828125, + "min": -2.51171875, + "x": 1551440610000, + }, + "initialY0": -2.51171875, + "initialY1": 9.48828125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440610000, + "y0": -2.51171875, + "y1": 9.48828125, + }, + Object { + "datum": Object { + "max": 16.0234375, + "min": 4.0234375, + "x": 1551440640000, + }, + "initialY0": 4.0234375, + "initialY1": 16.0234375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440640000, + "y0": 4.0234375, + "y1": 16.0234375, + }, + Object { + "datum": Object { + "max": 18.484375, + "min": 6.484375, + "x": 1551440670000, + }, + "initialY0": 6.484375, + "initialY1": 18.484375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440670000, + "y0": 6.484375, + "y1": 18.484375, + }, + Object { + "datum": Object { + "max": 16.890625, + "min": 4.890625, + "x": 1551440700000, + }, + "initialY0": 4.890625, + "initialY1": 16.890625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440700000, + "y0": 4.890625, + "y1": 16.890625, + }, + Object { + "datum": Object { + "max": 19.578125, + "min": 7.578125, + "x": 1551440730000, + }, + "initialY0": 7.578125, + "initialY1": 19.578125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440730000, + "y0": 7.578125, + "y1": 19.578125, + }, + Object { + "datum": Object { + "max": 14.7578125, + "min": 2.7578125, + "x": 1551440760000, + }, + "initialY0": 2.7578125, + "initialY1": 14.7578125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440760000, + "y0": 2.7578125, + "y1": 14.7578125, + }, + Object { + "datum": Object { + "max": 13.921875, + "min": 1.921875, + "x": 1551440790000, + }, + "initialY0": 1.921875, + "initialY1": 13.921875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440790000, + "y0": 1.921875, + "y1": 13.921875, + }, + Object { + "datum": Object { + "max": 14.5078125, + "min": 2.5078125, + "x": 1551440820000, + }, + "initialY0": 2.5078125, + "initialY1": 14.5078125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440820000, + "y0": 2.5078125, + "y1": 14.5078125, + }, + Object { + "datum": Object { + "max": 15.375, + "min": 3.375, + "x": 1551440850000, + }, + "initialY0": 3.375, + "initialY1": 15.375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440850000, + "y0": 3.375, + "y1": 15.375, + }, + Object { + "datum": Object { + "max": 19.890625, + "min": 7.890625, + "x": 1551440880000, + }, + "initialY0": 7.890625, + "initialY1": 19.890625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440880000, + "y0": 7.890625, + "y1": 19.890625, + }, + Object { + "datum": Object { + "max": 22.1953125, + "min": 6.1953125, + "x": 1551440910000, + }, + "initialY0": 6.1953125, + "initialY1": 22.1953125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440910000, + "y0": 6.1953125, + "y1": 22.1953125, + }, + Object { + "datum": Object { + "max": 19.625, + "min": 7.625, + "x": 1551440940000, + }, + "initialY0": 7.625, + "initialY1": 19.625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440940000, + "y0": 7.625, + "y1": 19.625, + }, + Object { + "datum": Object { + "max": 15.734375, + "min": 7.734375, + "x": 1551440970000, + }, + "initialY0": 7.734375, + "initialY1": 15.734375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551440970000, + "y0": 7.734375, + "y1": 15.734375, + }, + Object { + "datum": Object { + "max": 14.1640625, + "min": 2.1640625, + "x": 1551441000000, + }, + "initialY0": 2.1640625, + "initialY1": 14.1640625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441000000, + "y0": 2.1640625, + "y1": 14.1640625, + }, + Object { + "datum": Object { + "max": 13.296875, + "min": 5.296875, + "x": 1551441030000, + }, + "initialY0": 5.296875, + "initialY1": 13.296875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441030000, + "y0": 5.296875, + "y1": 13.296875, + }, + Object { + "datum": Object { + "max": 11.5546875, + "min": 3.5546875, + "x": 1551441060000, + }, + "initialY0": 3.5546875, + "initialY1": 11.5546875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441060000, + "y0": 3.5546875, + "y1": 11.5546875, + }, + Object { + "datum": Object { + "max": 11.17578125, + "min": 3.17578125, + "x": 1551441090000, + }, + "initialY0": 3.17578125, + "initialY1": 11.17578125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441090000, + "y0": 3.17578125, + "y1": 11.17578125, + }, + Object { + "datum": Object { + "max": 9.8671875, + "min": 1.8671875, + "x": 1551441120000, + }, + "initialY0": 1.8671875, + "initialY1": 9.8671875, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441120000, + "y0": 1.8671875, + "y1": 9.8671875, + }, + Object { + "datum": Object { + "max": 14.828125, + "min": -1.171875, + "x": 1551441150000, + }, + "initialY0": -1.171875, + "initialY1": 14.828125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441150000, + "y0": -1.171875, + "y1": 14.828125, + }, + Object { + "datum": Object { + "max": 18.578125, + "min": 2.578125, + "x": 1551441180000, + }, + "initialY0": 2.578125, + "initialY1": 18.578125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441180000, + "y0": 2.578125, + "y1": 18.578125, + }, + Object { + "datum": Object { + "max": 20.140625, + "min": 12.140625, + "x": 1551441210000, + }, + "initialY0": 12.140625, + "initialY1": 20.140625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441210000, + "y0": 12.140625, + "y1": 20.140625, + }, + Object { + "datum": Object { + "max": 19.640625, + "min": 11.640625, + "x": 1551441240000, + }, + "initialY0": 11.640625, + "initialY1": 19.640625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441240000, + "y0": 11.640625, + "y1": 19.640625, + }, + Object { + "datum": Object { + "max": 21.1484375, + "min": 5.1484375, + "x": 1551441270000, + }, + "initialY0": 5.1484375, + "initialY1": 21.1484375, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441270000, + "y0": 5.1484375, + "y1": 21.1484375, + }, + Object { + "datum": Object { + "max": 15.9140625, + "min": 3.9140625, + "x": 1551441300000, + }, + "initialY0": 3.9140625, + "initialY1": 15.9140625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441300000, + "y0": 3.9140625, + "y1": 15.9140625, + }, + Object { + "datum": Object { + "max": 14.0625, + "min": 6.0625, + "x": 1551441330000, + }, + "initialY0": 6.0625, + "initialY1": 14.0625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441330000, + "y0": 6.0625, + "y1": 14.0625, + }, + Object { + "datum": Object { + "max": 11.66015625, + "min": 3.66015625, + "x": 1551441360000, + }, + "initialY0": 3.66015625, + "initialY1": 11.66015625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441360000, + "y0": 3.66015625, + "y1": 11.66015625, + }, + Object { + "datum": Object { + "max": 17.0078125, + "min": 1.0078125, + "x": 1551441390000, + }, + "initialY0": 1.0078125, + "initialY1": 17.0078125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441390000, + "y0": 1.0078125, + "y1": 17.0078125, + }, + Object { + "datum": Object { + "max": 12.78125, + "min": 4.78125, + "x": 1551441420000, + }, + "initialY0": 4.78125, + "initialY1": 12.78125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441420000, + "y0": 4.78125, + "y1": 12.78125, + }, + Object { + "datum": Object { + "max": 16.0390625, + "min": 0.0390625, + "x": 1551441450000, + }, + "initialY0": 0.0390625, + "initialY1": 16.0390625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441450000, + "y0": 0.0390625, + "y1": 16.0390625, + }, + Object { + "datum": Object { + "max": 29.515625, + "min": 17.515625, + "x": 1551441480000, + }, + "initialY0": 17.515625, + "initialY1": 29.515625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441480000, + "y0": 17.515625, + "y1": 29.515625, + }, + Object { + "datum": Object { + "max": 22.640625, + "min": 14.640625, + "x": 1551441510000, + }, + "initialY0": 14.640625, + "initialY1": 22.640625, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441510000, + "y0": 14.640625, + "y1": 22.640625, + }, + Object { + "datum": Object { + "max": 17.1953125, + "min": 5.1953125, + "x": 1551441540000, + }, + "initialY0": 5.1953125, + "initialY1": 17.1953125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441540000, + "y0": 5.1953125, + "y1": 17.1953125, + }, + Object { + "datum": Object { + "max": 14.1953125, + "min": 6.1953125, + "x": 1551441570000, + }, + "initialY0": 6.1953125, + "initialY1": 14.1953125, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1551441570000, + "y0": 6.1953125, + "y1": 14.1953125, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{(index:0)}splitAccessors{}", + "seriesKeys": Array [ + "(index:0)", + ], + "specId": "spec1", + "splitAccessors": Map {}, + "yAccessor": "(index:0)", + }, +] +`; + +exports[`Series functional accessors Can use functional yAccessor 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 7, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{(index:0)}splitAccessors{g1-cdn.google.com}", + "seriesKeys": Array [ + "cdn.google.com", + "(index:0)", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cdn.google.com", + }, + "yAccessor": "(index:0)", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{(index:0)}splitAccessors{g1-cloudflare.com}", + "seriesKeys": Array [ + "cloudflare.com", + "(index:0)", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cloudflare.com", + }, + "yAccessor": "(index:0)", + }, +] +`; + +exports[`Series functional accessors Can use functional yAccessor with fieldName 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 7, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{custom name}splitAccessors{g1-cdn.google.com}", + "seriesKeys": Array [ + "cdn.google.com", + "custom name", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cdn.google.com", + }, + "yAccessor": "custom name", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{custom name}splitAccessors{g1-cloudflare.com}", + "seriesKeys": Array [ + "cloudflare.com", + "custom name", + ], + "specId": "spec1", + "splitAccessors": Map { + "g1" => "cloudflare.com", + }, + "yAccessor": "custom name", + }, +] +`; + +exports[`Series functional accessors Can use multiple functional/static accessors 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 10, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 7, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 7, + "y0": null, + "y1": 7, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{(index:0)-cdn.google.com|g2-direct-cdn}", + "seriesKeys": Array [ + "cdn.google.com", + "direct-cdn", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "(index:0)" => "cdn.google.com", + "g2" => "direct-cdn", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 5, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 10, + "y0": null, + "y1": 5, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 7, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "direct-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 7, + "y0": null, + "y1": 3, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{(index:1)}splitAccessors{(index:0)-cdn.google.com|g2-direct-cdn}", + "seriesKeys": Array [ + "cdn.google.com", + "direct-cdn", + "(index:1)", + ], + "specId": "spec1", + "splitAccessors": Map { + "(index:0)" => "cdn.google.com", + "g2" => "direct-cdn", + }, + "yAccessor": "(index:1)", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 10, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 7, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 7, + "y0": null, + "y1": 7, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{(index:0)-cdn.google.com|g2-indirect-cdn}", + "seriesKeys": Array [ + "cdn.google.com", + "indirect-cdn", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "(index:0)" => "cdn.google.com", + "g2" => "indirect-cdn", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 1, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 10, + "y2": 5, + }, + "initialY0": null, + "initialY1": 5, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 10, + "y0": null, + "y1": 5, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 7, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cdn.google.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 7, + "y2": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 7, + "y0": null, + "y1": 3, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{(index:1)}splitAccessors{(index:0)-cdn.google.com|g2-indirect-cdn}", + "seriesKeys": Array [ + "cdn.google.com", + "indirect-cdn", + "(index:1)", + ], + "specId": "spec1", + "splitAccessors": Map { + "(index:0)" => "cdn.google.com", + "g2" => "indirect-cdn", + }, + "yAccessor": "(index:1)", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{(index:0)-cloudflare.com|g2-direct-cdn}", + "seriesKeys": Array [ + "cloudflare.com", + "direct-cdn", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "(index:0)" => "cloudflare.com", + "g2" => "direct-cdn", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 5, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 5, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "direct-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 4, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{(index:1)}splitAccessors{(index:0)-cloudflare.com|g2-direct-cdn}", + "seriesKeys": Array [ + "cloudflare.com", + "direct-cdn", + "(index:1)", + ], + "specId": "spec1", + "splitAccessors": Map { + "(index:0)" => "cloudflare.com", + "g2" => "direct-cdn", + }, + "yAccessor": "(index:1)", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{(index:0)-cloudflare.com|g2-indirect-cdn}", + "seriesKeys": Array [ + "cloudflare.com", + "indirect-cdn", + "y1", + ], + "specId": "spec1", + "splitAccessors": Map { + "(index:0)" => "cloudflare.com", + "g2" => "indirect-cdn", + }, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 0, + "y1": 3, + "y2": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 1, + "y1": 2, + "y2": 5, + }, + "initialY0": null, + "initialY1": 5, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 5, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 2, + "y1": 3, + "y2": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 3, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 4, + }, + Object { + "datum": Object { + "g1": "cloudflare.com", + "g2": "indirect-cdn", + "x": 6, + "y1": 6, + "y2": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 6, + "y0": null, + "y1": 4, + }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{(index:1)}splitAccessors{(index:0)-cloudflare.com|g2-indirect-cdn}", + "seriesKeys": Array [ + "cloudflare.com", + "indirect-cdn", + "(index:1)", + ], + "specId": "spec1", + "splitAccessors": Map { + "(index:0)" => "cloudflare.com", + "g2" => "indirect-cdn", + }, + "yAccessor": "(index:1)", + }, +] +`; + +exports[`Series should compute data series for stacked specs 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "x": 0, + "y1": 1, + "y2": 3, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 1, + "mark": null, + "x": 0, + "y0": 0, + "y1": 1, + }, + Object { + "datum": Object { + "x": 1, + "y1": 2, + "y2": 7, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 1, + "y0": 0, + "y1": 2, + }, + Object { + "datum": Object { + "x": 2, + "y1": 1, + "y2": 2, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 1, + "mark": null, + "x": 2, + "y0": 0, + "y1": 1, + }, + Object { + "datum": Object { + "x": 3, + "y1": 6, + "y2": 10, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 6, + "mark": null, + "x": 3, + "y0": 0, + "y1": 6, + }, + ], + "key": "groupId{group2}spec{spec2}yAccessor{y1}splitAccessors{}", + "seriesKeys": Array [ + "y1", + ], + "specId": "spec2", + "splitAccessors": Map {}, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "x": 0, + "y1": 1, + "y2": 3, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 3, + "mark": null, + "x": 0, + "y0": 1, + "y1": 4, + }, + Object { + "datum": Object { + "x": 1, + "y1": 2, + "y2": 7, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 7, + "mark": null, + "x": 1, + "y0": 2, + "y1": 9, + }, + Object { + "datum": Object { + "x": 2, + "y1": 1, + "y2": 2, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 2, + "y0": 1, + "y1": 3, + }, + Object { + "datum": Object { + "x": 3, + "y1": 6, + "y2": 10, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 10, + "mark": null, + "x": 3, + "y0": 6, + "y1": 16, + }, + ], + "key": "groupId{group2}spec{spec2}yAccessor{y2}splitAccessors{}", + "seriesKeys": Array [ + "y2", + ], + "specId": "spec2", + "splitAccessors": Map {}, + "yAccessor": "y2", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "x": 0, + "y": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "x": 1, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "x": 2, + "y": 10, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "x": 3, + "y": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{group}spec{spec1}yAccessor{y}splitAccessors{}", + "seriesKeys": Array [ + "y", + ], + "specId": "spec1", + "splitAccessors": Map {}, + "yAccessor": "y", + }, +] +`; + +exports[`Series should split an array of specs into data series 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "x": 0, + "y": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "x": 1, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "x": 2, + "y": 10, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "x": 3, + "y": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + ], + "groupId": "group", + "insertIndex": 0, + "isFiltered": false, + "isStacked": false, + "key": "groupId{group}spec{spec1}yAccessor{y}splitAccessors{}", + "seriesKeys": Array [ + "y", + ], + "seriesType": "line", + "spec": Object { + "chartType": "xy_axis", + "data": Array [ + Object { + "x": 0, + "y": 1, + }, + Object { + "x": 1, + "y": 2, + }, + Object { + "x": 2, + "y": 10, + }, + Object { + "x": 3, + "y": 6, + }, + ], + "groupId": "group", + "hideInLegend": false, + "id": "spec1", + "seriesType": "line", + "specType": "series", + "xAccessor": "x", + "xScaleType": "linear", + "yAccessors": Array [ + "y", + ], + "yScaleType": "log", + }, + "specId": "spec1", + "splitAccessors": Map {}, + "stackMode": undefined, + "yAccessor": "y", + }, +] +`; + +exports[`Series should split an array of specs into data series 2`] = ` +Array [ + Object { + "data": Array [ + Object { + "datum": Object { + "x": 0, + "y1": 1, + "y2": 3, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "x": 1, + "y1": 2, + "y2": 7, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "x": 2, + "y1": 1, + "y2": 2, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "x": 3, + "y1": 6, + "y2": 10, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 6, + }, + ], + "groupId": "group2", + "insertIndex": 1, + "isFiltered": false, + "isStacked": true, + "key": "groupId{group2}spec{spec2}yAccessor{y1}splitAccessors{}", + "seriesKeys": Array [ + "y1", + ], + "seriesType": "line", + "spec": Object { + "chartType": "xy_axis", + "data": Array [ + Object { + "x": 0, + "y1": 1, + "y2": 3, + }, + Object { + "x": 1, + "y1": 2, + "y2": 7, + }, + Object { + "x": 2, + "y1": 1, + "y2": 2, + }, + Object { + "x": 3, + "y1": 6, + "y2": 10, + }, + ], + "groupId": "group2", + "hideInLegend": false, + "id": "spec2", + "seriesType": "line", + "specType": "series", + "stackAccessors": Array [ + "x", + ], + "xAccessor": "x", + "xScaleType": "linear", + "yAccessors": Array [ + "y1", + "y2", + ], + "yScaleType": "log", + }, + "specId": "spec2", + "splitAccessors": Map {}, + "stackMode": undefined, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "x": 0, + "y1": 1, + "y2": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 0, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "x": 1, + "y1": 2, + "y2": 7, + }, + "initialY0": null, + "initialY1": 7, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 1, + "y0": null, + "y1": 7, + }, + Object { + "datum": Object { + "x": 2, + "y1": 1, + "y2": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 2, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "x": 3, + "y1": 6, + "y2": 10, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": undefined, + "smV": undefined, + "x": 3, + "y0": null, + "y1": 10, + }, + ], + "groupId": "group2", + "insertIndex": 2, + "isFiltered": false, + "isStacked": true, + "key": "groupId{group2}spec{spec2}yAccessor{y2}splitAccessors{}", + "seriesKeys": Array [ + "y2", + ], + "seriesType": "line", + "spec": Object { + "chartType": "xy_axis", + "data": Array [ + Object { + "x": 0, + "y1": 1, + "y2": 3, + }, + Object { + "x": 1, + "y1": 2, + "y2": 7, + }, + Object { + "x": 2, + "y1": 1, + "y2": 2, + }, + Object { + "x": 3, + "y1": 6, + "y2": 10, + }, + ], + "groupId": "group2", + "hideInLegend": false, + "id": "spec2", + "seriesType": "line", + "specType": "series", + "stackAccessors": Array [ + "x", + ], + "xAccessor": "x", + "xScaleType": "linear", + "yAccessors": Array [ + "y1", + "y2", + ], + "yScaleType": "log", + }, + "specId": "spec2", + "splitAccessors": Map {}, + "stackMode": undefined, + "yAccessor": "y2", + }, +] +`; diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/axis_type_utils.test.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/axis_type_utils.test.ts new file mode 100644 index 000000000000..2dcc7543bb71 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/axis_type_utils.test.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Position } from '../../../utils/common'; +import { isBounded, isVerticalAxis, isHorizontalAxis, isVerticalGrid, isHorizontalGrid } from './axis_type_utils'; + +describe('Axis type utils', () => { + test('should determine orientation of axis position', () => { + expect(isVerticalAxis(Position.Left)).toBe(true); + expect(isVerticalAxis(Position.Right)).toBe(true); + expect(isVerticalAxis(Position.Top)).toBe(false); + expect(isVerticalAxis(Position.Bottom)).toBe(false); + + expect(isHorizontalAxis(Position.Left)).toBe(false); + expect(isHorizontalAxis(Position.Right)).toBe(false); + expect(isHorizontalAxis(Position.Top)).toBe(true); + expect(isHorizontalAxis(Position.Bottom)).toBe(true); + }); + + test('should determine orientation of gridlines from axis position', () => { + expect(isVerticalGrid(Position.Left)).toBe(false); + expect(isVerticalGrid(Position.Right)).toBe(false); + expect(isVerticalGrid(Position.Top)).toBe(true); + expect(isVerticalGrid(Position.Bottom)).toBe(true); + + expect(isHorizontalGrid(Position.Left)).toBe(true); + expect(isHorizontalGrid(Position.Right)).toBe(true); + expect(isHorizontalGrid(Position.Top)).toBe(false); + expect(isHorizontalGrid(Position.Bottom)).toBe(false); + }); + + test('should determine that a domain has at least one bound', () => { + const lowerBounded = { + min: 0, + }; + + const upperBounded = { + max: 0, + }; + + expect(isBounded(lowerBounded)).toBe(true); + expect(isBounded(upperBounded)).toBe(true); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/axis_type_utils.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/axis_type_utils.ts new file mode 100644 index 000000000000..32c41cc430db --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/axis_type_utils.ts @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Position } from '../../../utils/common'; +import { CompleteBoundedDomain, LowerBoundedDomain, UpperBoundedDomain, DomainRange } from './specs'; + +/** @internal */ +export function isLowerBound(domain: Partial): domain is LowerBoundedDomain { + return domain.min != null; +} + +/** @internal */ +export function isUpperBound(domain: Partial): domain is UpperBoundedDomain { + return domain.max != null; +} + +/** @internal */ +export function isCompleteBound(domain: Partial): domain is CompleteBoundedDomain { + return domain.max != null && domain.min != null; +} + +/** @internal */ +export function isBounded(domain: Partial): domain is DomainRange { + return domain.max != null || domain.min != null; +} + +/** @internal */ +export function isVerticalAxis(axisPosition: Position): axisPosition is Extract { + return axisPosition === Position.Left || axisPosition === Position.Right; +} + +/** @internal */ +export function isHorizontalAxis(axisPosition: Position): axisPosition is Extract { + return axisPosition === Position.Top || axisPosition === Position.Bottom; +} + +/** @internal */ +export function isVerticalGrid(axisPosition: Position) { + return isHorizontalAxis(axisPosition); +} + +/** @internal */ +export function isHorizontalGrid(axisPosition: Position) { + return isVerticalAxis(axisPosition); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/axis_utils.test.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/axis_utils.test.ts new file mode 100644 index 000000000000..04dafeed58b6 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/axis_utils.test.ts @@ -0,0 +1,1896 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DateTime } from 'luxon'; +import moment from 'moment-timezone'; + +import { ChartType } from '../..'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs/specs'; +import { MockStore } from '../../../mocks/store/store'; +import { MockXDomain, MockYDomain } from '../../../mocks/xy/domains'; +import { Scale } from '../../../scales'; +import { ScaleType } from '../../../scales/constants'; +import { SpecType } from '../../../specs/constants'; +import { CanvasTextBBoxCalculator } from '../../../utils/bbox/canvas_text_bbox_calculator'; +import { SvgTextBBoxCalculator } from '../../../utils/bbox/svg_text_bbox_calculator'; +import { Position, mergePartial } from '../../../utils/common'; +import { niceTimeFormatter } from '../../../utils/data/formatters'; +import { OrdinalDomain } from '../../../utils/domain'; +import { AxisId, GroupId } from '../../../utils/ids'; +import { LIGHT_THEME } from '../../../utils/themes/light_theme'; +import { AxisStyle, TextOffset } from '../../../utils/themes/theme'; +import { computeAxesGeometriesSelector } from '../state/selectors/compute_axes_geometries'; +import { computeAxisTicksDimensionsSelector } from '../state/selectors/compute_axis_ticks_dimensions'; +import { getScale, SmallMultipleScales } from '../state/selectors/compute_small_multiple_scales'; +import { getAxesStylesSelector } from '../state/selectors/get_axis_styles'; +import { computeGridLinesSelector } from '../state/selectors/get_grid_lines'; +import { mergeYCustomDomainsByGroupId } from '../state/selectors/merge_y_custom_domains'; +import { + AxisTick, + AxisTicksDimensions, + computeAxisTicksDimensions, + computeRotatedLabelDimensions, + getAvailableTicks, + getAxisPosition, + getAxesGeometries, + getHorizontalAxisTickLineProps, + getMaxLabelDimensions, + getMinMaxRange, + getScaleForAxisSpec, + getTickLabelProps, + getVerticalAxisTickLineProps, + getVisibleTicks, + isYDomain, + enableDuplicatedTicks, + defaultTickFormatter, +} from './axis_utils'; +import { computeXScale } from './scales'; +import { AxisSpec, DomainRange, DEFAULT_GLOBAL_ID } from './specs'; + +const NO_ROTATION = 0; + +const getCustomStyle = (rotation = 0, padding = 10): AxisStyle => + mergePartial(LIGHT_THEME.axes, { + tickLine: { + size: 10, + padding, + }, + tickLabel: { + fontSize: 16, + fontFamily: 'Arial', + rotation, + }, + }); +const style = getCustomStyle(); + +describe('Axis computational utils', () => { + const mockedRect = { + x: 0, + y: 0, + bottom: 0, + left: 0, + right: 0, + top: 0, + width: 10, + height: 10, + toJSON: () => '', + }; + const originalGetBBox = SVGElement.prototype.getBoundingClientRect; + + beforeEach( + () => + (SVGElement.prototype.getBoundingClientRect = function () { + const text = this.textContent || 0; + return { ...mockedRect, width: Number(text) * 10, height: Number(text) * 10 }; + }), + ); + afterEach(() => (SVGElement.prototype.getBoundingClientRect = originalGetBBox)); + + const chartDim = { + width: 100, + height: 100, + top: 0, + left: 0, + }; + const axis1Dims = { + tickValues: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1], + tickLabels: ['0', '0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1'], + maxLabelBboxWidth: 10, + maxLabelBboxHeight: 10, + maxLabelTextWidth: 10, + maxLabelTextHeight: 10, + isHidden: false, + }; + const verticalAxisSpec = MockGlobalSpec.axis({ + chartType: ChartType.XYAxis, + specType: SpecType.Axis, + id: 'axis_1', + title: 'Axis 1', + groupId: 'group_1', + hide: false, + showOverlappingTicks: false, + showOverlappingLabels: false, + position: Position.Left, + style, + showGridLines: true, + integersOnly: false, + }); + + const horizontalAxisSpec = MockGlobalSpec.axis({ + chartType: ChartType.XYAxis, + specType: SpecType.Axis, + id: 'axis_2', + title: 'Axis 2', + groupId: 'group_1', + hide: false, + showOverlappingTicks: false, + showOverlappingLabels: false, + position: Position.Top, + style, + integersOnly: false, + }); + + const verticalAxisSpecWTitle = MockGlobalSpec.axis({ + chartType: ChartType.XYAxis, + specType: SpecType.Axis, + id: 'axis_1', + groupId: 'group_1', + title: 'v axis', + hide: false, + showOverlappingTicks: false, + showOverlappingLabels: false, + position: Position.Left, + style, + showGridLines: true, + integersOnly: false, + }); + const xAxisWithTime = MockGlobalSpec.axis({ + chartType: ChartType.XYAxis, + specType: SpecType.Axis, + id: 'axis_1', + groupId: 'group_1', + title: 'v axis', + hide: false, + showOverlappingTicks: false, + showOverlappingLabels: false, + position: Position.Bottom, + style, + tickFormat: niceTimeFormatter([1551438000000, 1551441300000]), + showGridLines: true, + integersOnly: false, + }); + + const lineSeriesSpec = MockSeriesSpec.line({ + id: 'line', + groupId: 'group_1', + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + data: [ + [0, 0], + [0.5, 0.5], + [1, 1], + ], + }); + const xDomain = MockXDomain.fromScaleType(ScaleType.Linear, { + domain: [0, 1], + isBandScale: false, + minInterval: 0, + }); + + const yDomain = MockYDomain.fromScaleType(ScaleType.Linear, { + groupId: 'group_1', + domain: [0, 1], + isBandScale: false, + }); + + const getSmScales = (smHDomain: OrdinalDomain = [], smVDomain: OrdinalDomain = []): SmallMultipleScales => ({ + horizontal: getScale(smHDomain, chartDim.width), + vertical: getScale(smVDomain, chartDim.height), + }); + + const emptySmScales = getSmScales(); + + const axisTitleStyles = (titleHeight: number, panelTitleHeight?: number) => + mergePartial(LIGHT_THEME.axes, { + axisTitle: { + fontSize: titleHeight, + padding: { + inner: 0, + outer: 10, + }, + }, + axisPanelTitle: { + fontSize: panelTitleHeight, + }, + }); + + const { axes } = LIGHT_THEME; + + test('should compute axis dimensions', () => { + const bboxCalculator = new SvgTextBBoxCalculator(); + const axisDimensions = computeAxisTicksDimensions( + verticalAxisSpec, + xDomain, + [yDomain], + 1, + bboxCalculator, + NO_ROTATION, + axes, + (v) => `${v}`, + ); + expect(axisDimensions).toEqual(axis1Dims); + + const ungroupedAxisSpec = { ...verticalAxisSpec, groupId: 'foo' }; + const result = computeAxisTicksDimensions( + ungroupedAxisSpec, + xDomain, + [yDomain], + 1, + bboxCalculator, + NO_ROTATION, + axes, + (v) => `${v}`, + undefined, + false, + ); + + expect(result).toBeNull(); + + bboxCalculator.destroy(); + }); + + test('should not compute axis dimensions when spec is configured to hide', () => { + const bboxCalculator = new CanvasTextBBoxCalculator(); + verticalAxisSpec.hide = true; + const axisDimensions = computeAxisTicksDimensions( + verticalAxisSpec, + xDomain, + [yDomain], + 1, + bboxCalculator, + NO_ROTATION, + axes, + (v) => `${v}`, + ); + expect(axisDimensions).toBe(null); + }); + + test('should compute axis dimensions with timeZone', () => { + const bboxCalculator = new SvgTextBBoxCalculator(); + const xDomain = MockXDomain.fromScaleType(ScaleType.Time, { + domain: [1551438000000, 1551441300000], + isBandScale: false, + minInterval: 0, + timeZone: 'utc', + }); + let axisDimensions = computeAxisTicksDimensions( + xAxisWithTime, + xDomain, + [yDomain], + 1, + bboxCalculator, + NO_ROTATION, + axes, + (v) => `${v}`, + ); + expect(axisDimensions).not.toBeNull(); + expect(axisDimensions?.tickLabels[0]).toBe('11:00:00'); + expect(axisDimensions?.tickLabels[11]).toBe('11:55:00'); + + axisDimensions = computeAxisTicksDimensions( + xAxisWithTime, + { + ...xDomain, + timeZone: 'utc+3', + }, + [yDomain], + 1, + bboxCalculator, + NO_ROTATION, + axes, + (v) => `${v}`, + ); + expect(axisDimensions).not.toBeNull(); + expect(axisDimensions?.tickLabels[0]).toBe('14:00:00'); + expect(axisDimensions?.tickLabels[11]).toBe('14:55:00'); + + axisDimensions = computeAxisTicksDimensions( + xAxisWithTime, + { + ...xDomain, + timeZone: 'utc-3', + }, + [yDomain], + 1, + bboxCalculator, + NO_ROTATION, + axes, + (v) => `${v}`, + ); + expect(axisDimensions).not.toBeNull(); + expect(axisDimensions?.tickLabels[0]).toBe('08:00:00'); + expect(axisDimensions?.tickLabels[11]).toBe('08:55:00'); + + bboxCalculator.destroy(); + }); + + test('should compute dimensions for the bounding box containing a rotated label', () => { + expect(computeRotatedLabelDimensions({ width: 1, height: 2 }, 0)).toEqual({ + width: 1, + height: 2, + }); + + const dims90 = computeRotatedLabelDimensions({ width: 1, height: 2 }, 90); + expect(dims90.width).toBeCloseTo(2); + expect(dims90.height).toBeCloseTo(1); + + const dims45 = computeRotatedLabelDimensions({ width: 1, height: 1 }, 45); + expect(dims45.width).toBeCloseTo(Math.sqrt(2)); + expect(dims45.height).toBeCloseTo(Math.sqrt(2)); + }); + + test('should generate a valid scale', () => { + const yScale = getScaleForAxisSpec(verticalAxisSpec, xDomain, [yDomain], 0, 0, 100, 0); + expect(yScale).toBeDefined(); + expect(yScale?.bandwidth).toBe(0); + expect(yScale?.domain).toEqual([0, 1]); + expect(yScale?.range).toEqual([100, 0]); + expect(yScale?.ticks()).toEqual([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]); + + const ungroupedAxisSpec = { ...verticalAxisSpec, groupId: 'foo' }; + const nullYScale = getScaleForAxisSpec(ungroupedAxisSpec, xDomain, [yDomain], 0, 0, 100, 0); + expect(nullYScale).toBe(null); + + const xScale = getScaleForAxisSpec(horizontalAxisSpec, xDomain, [yDomain], 0, 0, 100, 0); + expect(xScale).toBeDefined(); + }); + + const axisDimensions: AxisTicksDimensions = { + maxLabelBboxWidth: 100, + maxLabelBboxHeight: 100, + maxLabelTextHeight: 100, + maxLabelTextWidth: 100, + tickLabels: [], + tickValues: [], + isHidden: false, + }; + const offset: TextOffset = { + x: 0, + y: 0, + reference: 'global', + }; + + describe('getAvailableTicks', () => { + test('should compute to end of domain when histogram mode not enabled', () => { + const scale = getScaleForAxisSpec(verticalAxisSpec, xDomain, [yDomain], 0, 0, 100, 0); + const axisPositions = getAvailableTicks(verticalAxisSpec, scale!, 0, false, (v) => `${v}`, 0); + const expectedAxisPositions = [ + { label: '0', position: 100, value: 0 }, + { label: '0.1', position: 90, value: 0.1 }, + { label: '0.2', position: 80, value: 0.2 }, + { label: '0.3', position: 70, value: 0.3 }, + { label: '0.4', position: 60, value: 0.4 }, + { label: '0.5', position: 50, value: 0.5 }, + { label: '0.6', position: 40, value: 0.6 }, + { label: '0.7', position: 30, value: 0.7 }, + { label: '0.8', position: 20, value: 0.8 }, + { label: '0.9', position: 10, value: 0.9 }, + { label: '1', position: 0, value: 1 }, + ]; + expect(axisPositions).toEqual(expectedAxisPositions); + }); + + test('should compute positions with rotational offset', () => { + const rotationalOffset = 2; + const scale = getScaleForAxisSpec(verticalAxisSpec, xDomain, [yDomain], 0, 0, 100, 0); + const axisPositions = getAvailableTicks(verticalAxisSpec, scale!, 0, false, (v) => `${v}`, rotationalOffset); + const expectedAxisPositions = [ + { label: '0', position: 100 + rotationalOffset, value: 0 }, + { label: '0.1', position: 90 + rotationalOffset, value: 0.1 }, + { label: '0.2', position: 80 + rotationalOffset, value: 0.2 }, + { label: '0.3', position: 70 + rotationalOffset, value: 0.3 }, + { label: '0.4', position: 60 + rotationalOffset, value: 0.4 }, + { label: '0.5', position: 50 + rotationalOffset, value: 0.5 }, + { label: '0.6', position: 40 + rotationalOffset, value: 0.6 }, + { label: '0.7', position: 30 + rotationalOffset, value: 0.7 }, + { label: '0.8', position: 20 + rotationalOffset, value: 0.8 }, + { label: '0.9', position: 10 + rotationalOffset, value: 0.9 }, + { label: '1', position: rotationalOffset, value: 1 }, + ]; + expect(axisPositions).toEqual(expectedAxisPositions); + }); + + test('should extend ticks to domain + minInterval in histogram mode for linear scale', () => { + const enableHistogramMode = true; + const xBandDomain = MockXDomain.fromScaleType(ScaleType.Linear, { + domain: [0, 100], + isBandScale: true, + minInterval: 10, + }); + const xScale = getScaleForAxisSpec(horizontalAxisSpec, xBandDomain, [yDomain], 1, 0, 100, 0); + const histogramAxisPositions = getAvailableTicks( + horizontalAxisSpec, + xScale!, + 1, + enableHistogramMode, + (v) => `${v}`, + 0, + ); + const histogramTickLabels = histogramAxisPositions.map(({ label }: AxisTick) => label); + expect(histogramTickLabels).toEqual(['0', '10', '20', '30', '40', '50', '60', '70', '80', '90', '100', '110']); + }); + + test('should extend ticks to domain + minInterval in histogram mode for time scale', () => { + const enableHistogramMode = true; + const xBandDomain = MockXDomain.fromScaleType(ScaleType.Time, { + domain: [1560438420000, 1560438510000], + isBandScale: true, + minInterval: 90000, + }); + const xScale = getScaleForAxisSpec(horizontalAxisSpec, xBandDomain, [yDomain], 1, 0, 100, 0); + const histogramAxisPositions = getAvailableTicks( + horizontalAxisSpec, + xScale!, + 1, + enableHistogramMode, + (v) => `${v}`, + 0, + ); + const histogramTickValues = histogramAxisPositions.map(({ value }: AxisTick) => value); + + const expectedTickValues = [ + 1560438420000, + 1560438435000, + 1560438450000, + 1560438465000, + 1560438480000, + 1560438495000, + 1560438510000, + 1560438525000, + 1560438540000, + 1560438555000, + 1560438570000, + 1560438585000, + 1560438600000, + ]; + + expect(histogramTickValues).toEqual(expectedTickValues); + }); + + test('should extend ticks to domain + minInterval in histogram mode for a scale with single datum', () => { + const enableHistogramMode = true; + const xBandDomain = MockXDomain.fromScaleType(ScaleType.Time, { + domain: [1560438420000, 1560438420000], // a single datum scale will have the same value for domain start & end + isBandScale: true, + minInterval: 90000, + }); + const xScale = getScaleForAxisSpec(horizontalAxisSpec, xBandDomain, [yDomain], 1, 0, 100, 0); + const histogramAxisPositions = getAvailableTicks( + horizontalAxisSpec, + xScale!, + 1, + enableHistogramMode, + (v) => `${v}`, + 0, + ); + const histogramTickValues = histogramAxisPositions.map(({ value }: AxisTick) => value); + const expectedTickValues = [1560438420000, 1560438510000]; + + expect(histogramTickValues).toEqual(expectedTickValues); + }); + }); + test('should compute visible ticks for a vertical axis', () => { + const allTicks = [ + { label: '0', position: 100, value: 0 }, + { label: '0.1', position: 90, value: 0.1 }, + { label: '0.2', position: 80, value: 0.2 }, + { label: '0.3', position: 70, value: 0.3 }, + { label: '0.4', position: 60, value: 0.4 }, + { label: '0.5', position: 50, value: 0.5 }, + { label: '0.6', position: 40, value: 0.6 }, + { label: '0.7', position: 30, value: 0.7 }, + { label: '0.8', position: 20, value: 0.8 }, + { label: '0.9', position: 10, value: 0.9 }, + { label: '1', position: 0, value: 1 }, + ]; + const visibleTicks = getVisibleTicks(allTicks, verticalAxisSpec, axis1Dims); + const expectedVisibleTicks = [ + { label: '1', position: 0, value: 1 }, + { label: '0.9', position: 10, value: 0.9 }, + { label: '0.8', position: 20, value: 0.8 }, + { label: '0.7', position: 30, value: 0.7 }, + { label: '0.6', position: 40, value: 0.6 }, + { label: '0.5', position: 50, value: 0.5 }, + { label: '0.4', position: 60, value: 0.4 }, + { label: '0.3', position: 70, value: 0.3 }, + { label: '0.2', position: 80, value: 0.2 }, + { label: '0.1', position: 90, value: 0.1 }, + { label: '0', position: 100, value: 0 }, + ]; + expect(visibleTicks).toEqual(expectedVisibleTicks); + }); + test('should compute visible ticks for a horizontal axis', () => { + const allTicks = [ + { label: '0', position: 100, value: 0 }, + { label: '0.1', position: 90, value: 0.1 }, + { label: '0.2', position: 80, value: 0.2 }, + { label: '0.3', position: 70, value: 0.3 }, + { label: '0.4', position: 60, value: 0.4 }, + { label: '0.5', position: 50, value: 0.5 }, + { label: '0.6', position: 40, value: 0.6 }, + { label: '0.7', position: 30, value: 0.7 }, + { label: '0.8', position: 20, value: 0.8 }, + { label: '0.9', position: 10, value: 0.9 }, + { label: '1', position: 0, value: 1 }, + ]; + const visibleTicks = getVisibleTicks(allTicks, horizontalAxisSpec, axis1Dims); + const expectedVisibleTicks = [ + { label: '1', position: 0, value: 1 }, + { label: '0.9', position: 10, value: 0.9 }, + { label: '0.8', position: 20, value: 0.8 }, + { label: '0.7', position: 30, value: 0.7 }, + { label: '0.6', position: 40, value: 0.6 }, + { label: '0.5', position: 50, value: 0.5 }, + { label: '0.4', position: 60, value: 0.4 }, + { label: '0.3', position: 70, value: 0.3 }, + { label: '0.2', position: 80, value: 0.2 }, + { label: '0.1', position: 90, value: 0.1 }, + { label: '0', position: 100, value: 0 }, + ]; + + expect(visibleTicks).toEqual(expectedVisibleTicks); + }); + test('should hide some ticks', () => { + const allTicks = [ + { label: '0', position: 100, value: 0 }, + { label: '0.1', position: 90, value: 0.1 }, + { label: '0.2', position: 80, value: 0.2 }, + { label: '0.3', position: 70, value: 0.3 }, + { label: '0.4', position: 60, value: 0.4 }, + { label: '0.5', position: 50, value: 0.5 }, + { label: '0.6', position: 40, value: 0.6 }, + { label: '0.7', position: 30, value: 0.7 }, + { label: '0.8', position: 20, value: 0.8 }, + { label: '0.9', position: 10, value: 0.9 }, + { label: '1', position: 0, value: 1 }, + ]; + const axis2Dims = { + axisScaleType: ScaleType.Linear, + axisScaleDomain: [0, 1], + tickValues: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1], + tickLabels: ['0', '0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1'], + maxLabelBboxWidth: 10, + maxLabelBboxHeight: 20, + maxLabelTextWidth: 10, + maxLabelTextHeight: 20, + isHidden: false, + }; + const visibleTicks = getVisibleTicks(allTicks, verticalAxisSpec, axis2Dims); + const expectedVisibleTicks = [ + { label: '1', position: 0, value: 1 }, + { label: '0.8', position: 20, value: 0.8 }, + { label: '0.6', position: 40, value: 0.6 }, + { label: '0.4', position: 60, value: 0.4 }, + { label: '0.2', position: 80, value: 0.2 }, + { label: '0', position: 100, value: 0 }, + ]; + expect(visibleTicks).toEqual(expectedVisibleTicks); + }); + test('should show all overlapping ticks and labels if configured to', () => { + const allTicks = [ + { label: '0', position: 100, value: 0 }, + { label: '0.1', position: 90, value: 0.1 }, + { label: '0.2', position: 80, value: 0.2 }, + { label: '0.3', position: 70, value: 0.3 }, + { label: '0.4', position: 60, value: 0.4 }, + { label: '0.5', position: 50, value: 0.5 }, + { label: '0.6', position: 40, value: 0.6 }, + { label: '0.7', position: 30, value: 0.7 }, + { label: '0.8', position: 20, value: 0.8 }, + { label: '0.9', position: 10, value: 0.9 }, + { label: '1', position: 0, value: 1 }, + ]; + const axis2Dims = { + axisScaleType: ScaleType.Linear, + axisScaleDomain: [0, 1], + tickValues: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1], + tickLabels: ['0', '0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1'], + maxLabelBboxWidth: 10, + maxLabelBboxHeight: 20, + maxLabelTextWidth: 10, + maxLabelTextHeight: 20, + isHidden: false, + }; + + verticalAxisSpec.showOverlappingTicks = true; + verticalAxisSpec.showOverlappingLabels = true; + const visibleOverlappingTicks = getVisibleTicks(allTicks, verticalAxisSpec, axis2Dims); + const expectedVisibleOverlappingTicks = [ + { label: '1', position: 0, value: 1 }, + { label: '0.9', position: 10, value: 0.9 }, + { label: '0.8', position: 20, value: 0.8 }, + { label: '0.7', position: 30, value: 0.7 }, + { label: '0.6', position: 40, value: 0.6 }, + { label: '0.5', position: 50, value: 0.5 }, + { label: '0.4', position: 60, value: 0.4 }, + { label: '0.3', position: 70, value: 0.3 }, + { label: '0.2', position: 80, value: 0.2 }, + { label: '0.1', position: 90, value: 0.1 }, + { label: '0', position: 100, value: 0 }, + ]; + expect(visibleOverlappingTicks).toEqual(expectedVisibleOverlappingTicks); + + verticalAxisSpec.showOverlappingTicks = true; + verticalAxisSpec.showOverlappingLabels = false; + const visibleOverlappingTicksAndLabels = getVisibleTicks(allTicks, verticalAxisSpec, axis2Dims); + const expectedVisibleOverlappingTicksAndLabels = [ + { label: '1', position: 0, value: 1 }, + { label: '', position: 10, value: 0.9 }, + { label: '0.8', position: 20, value: 0.8 }, + { label: '', position: 30, value: 0.7 }, + { label: '0.6', position: 40, value: 0.6 }, + { label: '', position: 50, value: 0.5 }, + { label: '0.4', position: 60, value: 0.4 }, + { label: '', position: 70, value: 0.3 }, + { label: '0.2', position: 80, value: 0.2 }, + { label: '', position: 90, value: 0.1 }, + { label: '0', position: 100, value: 0 }, + ]; + expect(visibleOverlappingTicksAndLabels).toEqual(expectedVisibleOverlappingTicksAndLabels); + }); + test('should compute min max range for on 0 deg bottom', () => { + const minMax = getMinMaxRange(Position.Bottom, 0, { + width: 100, + height: 50, + }); + expect(minMax).toEqual({ minRange: 0, maxRange: 100 }); + }); + test('should compute min max range for on 90 deg bottom', () => { + const minMax = getMinMaxRange(Position.Bottom, 90, { + width: 100, + height: 50, + }); + expect(minMax).toEqual({ minRange: 0, maxRange: 100 }); + }); + test('should compute min max range for on 180 deg bottom', () => { + const minMax = getMinMaxRange(Position.Bottom, 180, { + width: 100, + height: 50, + }); + expect(minMax).toEqual({ minRange: 100, maxRange: 0 }); + }); + test('should compute min max range for on -90 deg bottom', () => { + const minMax = getMinMaxRange(Position.Bottom, -90, { + width: 100, + height: 50, + }); + expect(minMax).toEqual({ minRange: 100, maxRange: 0 }); + }); + test('should compute min max range for on 90 deg Left', () => { + const minMax = getMinMaxRange(Position.Left, 90, { + width: 100, + height: 50, + }); + expect(minMax).toEqual({ minRange: 0, maxRange: 50 }); + }); + test('should compute min max range for on 180 deg Left', () => { + const minMax = getMinMaxRange(Position.Left, 180, { + width: 100, + height: 50, + }); + expect(minMax).toEqual({ minRange: 0, maxRange: 50 }); + }); + test('should compute min max range for on -90 deg Right', () => { + const minMax = getMinMaxRange(Position.Right, -90, { + width: 100, + height: 50, + }); + expect(minMax).toEqual({ minRange: 50, maxRange: 0 }); + }); + test('should get max bbox dimensions for a tick in comparison to previous values', () => { + const bboxCalculator = new CanvasTextBBoxCalculator(); + const reducer = getMaxLabelDimensions(bboxCalculator, LIGHT_THEME.axes.tickLabel); + + const accWithGreaterValues = { + maxLabelBboxWidth: 100, + maxLabelBboxHeight: 100, + maxLabelTextWidth: 100, + maxLabelTextHeight: 100, + }; + expect(reducer(accWithGreaterValues, 'foo')).toEqual(accWithGreaterValues); + }); + + test('should compute positions and alignment of tick labels along a vertical axis', () => { + const tickPosition = 0; + const axisPosition = { + top: 0, + left: 0, + width: 100, + height: 10, + }; + const unrotatedLabelProps = getTickLabelProps( + getCustomStyle(0, 5), + tickPosition, + Position.Left, + 0, + axisPosition, + axisDimensions, + true, + offset, + ); + + expect(unrotatedLabelProps).toEqual({ + offsetX: -50, + offsetY: 0, + textOffsetX: 50, + textOffsetY: 0, + x: 85, + y: 0, + horizontalAlign: 'right', + verticalAlign: 'middle', + }); + + const rotatedLabelProps = getTickLabelProps( + getCustomStyle(90), + tickPosition, + Position.Left, + 90, + axisPosition, + axisDimensions, + true, + offset, + { + vertical: 'middle', + horizontal: 'center', + }, + ); + + expect(rotatedLabelProps).toEqual({ + offsetX: -50, + offsetY: 0, + textOffsetX: 0, + textOffsetY: 0, + x: 80, + y: 0, + horizontalAlign: 'center', + verticalAlign: 'middle', + }); + + const rightRotatedLabelProps = getTickLabelProps( + getCustomStyle(90), + tickPosition, + Position.Right, + 90, + axisPosition, + axisDimensions, + true, + offset, + { + horizontal: 'center', + vertical: 'middle', + }, + ); + + expect(rightRotatedLabelProps).toEqual({ + offsetX: 50, + offsetY: 0, + textOffsetX: 0, + textOffsetY: 0, + x: 20, + y: 0, + horizontalAlign: 'center', + verticalAlign: 'middle', + }); + + const rightUnrotatedLabelProps = getTickLabelProps( + getCustomStyle(), + tickPosition, + Position.Right, + 0, + axisPosition, + axisDimensions, + true, + offset, + ); + + expect(rightUnrotatedLabelProps).toEqual({ + offsetX: 50, + offsetY: 0, + textOffsetX: -50, + textOffsetY: 0, + x: 20, + y: 0, + horizontalAlign: 'left', + verticalAlign: 'middle', + }); + }); + + test('should compute positions and alignment of tick labels along a horizontal axis', () => { + const tickPosition = 0; + const axisPosition = { + top: 0, + left: 0, + width: 100, + height: 10, + }; + const unrotatedLabelProps = getTickLabelProps( + getCustomStyle(0, 5), + tickPosition, + Position.Top, + 0, + axisPosition, + axisDimensions, + true, + offset, + { + horizontal: 'center', + vertical: 'bottom', + }, + ); + + expect(unrotatedLabelProps).toEqual({ + offsetX: 0, + offsetY: -50, + textOffsetY: 50, + textOffsetX: 0, + x: 0, + y: -5, + horizontalAlign: 'center', + verticalAlign: 'bottom', + }); + + const rotatedLabelProps = getTickLabelProps( + getCustomStyle(90), + tickPosition, + Position.Top, + 90, + axisPosition, + axisDimensions, + true, + offset, + ); + + expect(rotatedLabelProps).toEqual({ + offsetX: 0, + offsetY: -50, + textOffsetX: 50, + textOffsetY: 0, + x: 0, + y: -10, + horizontalAlign: 'right', + verticalAlign: 'middle', + }); + + const bottomRotatedLabelProps = getTickLabelProps( + getCustomStyle(90), + tickPosition, + Position.Bottom, + 90, + axisPosition, + axisDimensions, + true, + offset, + ); + + expect(bottomRotatedLabelProps).toEqual({ + offsetX: 0, + offsetY: 50, + textOffsetX: -50, + textOffsetY: 0, + x: 0, + y: 20, + horizontalAlign: 'left', + verticalAlign: 'middle', + }); + + const bottomUnrotatedLabelProps = getTickLabelProps( + getCustomStyle(90), + tickPosition, + Position.Bottom, + 90, + axisPosition, + axisDimensions, + true, + offset, + { + horizontal: 'center', + vertical: 'top', + }, + ); + + expect(bottomUnrotatedLabelProps).toEqual({ + offsetX: 0, + offsetY: 50, + textOffsetX: 0, + textOffsetY: -50, + x: 0, + y: 20, + horizontalAlign: 'center', + verticalAlign: 'top', + }); + }); + + test('should compute axis tick line positions', () => { + const tickPadding = 5; + const tickSize = 10; + const tickPosition = 10; + const axisHeight = 20; + + const leftAxisTickLinePositions = getVerticalAxisTickLineProps(Position.Left, tickPadding, tickSize, tickPosition); + + expect(leftAxisTickLinePositions).toEqual({ x1: 5, y1: 10, x2: -5, y2: 10 }); + + const rightAxisTickLinePositions = getVerticalAxisTickLineProps( + Position.Right, + tickPadding, + tickSize, + tickPosition, + ); + + expect(rightAxisTickLinePositions).toEqual({ x1: 0, y1: 10, x2: 10, y2: 10 }); + + const topAxisTickLinePositions = getHorizontalAxisTickLineProps(Position.Top, axisHeight, tickSize, tickPosition); + + expect(topAxisTickLinePositions).toEqual({ x1: 10, y1: 10, x2: 10, y2: 20 }); + + const bottomAxisTickLinePositions = getHorizontalAxisTickLineProps( + Position.Bottom, + axisHeight, + tickSize, + tickPosition, + ); + + expect(bottomAxisTickLinePositions).toEqual({ x1: 10, y1: 0, x2: 10, y2: 10 }); + }); + + test('should compute axis ticks positions with title', () => { + // validate assumptions for test + expect(verticalAxisSpec.id).toEqual(verticalAxisSpecWTitle.id); + + const axisSpecs = [verticalAxisSpecWTitle]; + const axesStyles = new Map(); + const axisDims = new Map(); + axisDims.set(verticalAxisSpecWTitle.id, axis1Dims); + + let axisTicksPosition = getAxesGeometries( + { + chartDimensions: chartDim, + leftMargin: 0, + }, + LIGHT_THEME, + NO_ROTATION, + axisSpecs, + axisDims, + axesStyles, + xDomain, + [yDomain], + emptySmScales, + 1, + false, + (v) => `${v}`, + ); + + const verticalAxisGeoms = axisTicksPosition.find(({ axis: { id } }) => id === verticalAxisSpecWTitle.id); + expect(verticalAxisGeoms?.anchorPoint).toEqual({ + y: 0, + x: 10, + }); + expect(verticalAxisGeoms?.size).toEqual({ + width: 50, + height: 100, + }); + + axisSpecs[0] = verticalAxisSpec; + + axisDims.set(verticalAxisSpec.id, axis1Dims); + + axisTicksPosition = getAxesGeometries( + { + chartDimensions: chartDim, + leftMargin: 0, + }, + LIGHT_THEME, + NO_ROTATION, + axisSpecs, + axisDims, + axesStyles, + xDomain, + [yDomain], + emptySmScales, + 1, + false, + (v) => `${v}`, + ); + const verticalAxisSpecWTitleGeoms = axisTicksPosition.find(({ axis: { id } }) => id === verticalAxisSpecWTitle.id); + expect(verticalAxisSpecWTitleGeoms?.anchorPoint).toEqual({ + y: 0, + x: 10, + }); + expect(verticalAxisSpecWTitleGeoms?.size).toEqual({ + width: 30, + height: 100, + }); + }); + + test('should compute left axis position', () => { + const axisTitleHeight = 10; + const cumTopSum = 10; + const cumBottomSum = 10; + const cumLeftSum = 10; + const cumRightSum = 10; + + const leftAxisPosition = getAxisPosition( + chartDim, + LIGHT_THEME.chartMargins, + axisTitleStyles(axisTitleHeight), + verticalAxisSpec, + axis1Dims, + emptySmScales, + cumTopSum, + cumBottomSum, + cumLeftSum, + cumRightSum, + 10, + 0, + true, + ); + + const expectedLeftAxisPosition = { + dimensions: { + height: 100, + width: 40, + left: 20, + top: 0, + }, + topIncrement: 0, + bottomIncrement: 0, + leftIncrement: 50, + rightIncrement: 0, + }; + + expect(leftAxisPosition).toEqual(expectedLeftAxisPosition); + }); + + test('should compute right axis position', () => { + const axisTitleHeight = 10; + const cumTopSum = 10; + const cumBottomSum = 10; + const cumLeftSum = 10; + const cumRightSum = 10; + + verticalAxisSpec.position = Position.Right; + const rightAxisPosition = getAxisPosition( + chartDim, + LIGHT_THEME.chartMargins, + axisTitleStyles(axisTitleHeight), + verticalAxisSpec, + axis1Dims, + emptySmScales, + cumTopSum, + cumBottomSum, + cumLeftSum, + cumRightSum, + 10, + 0, + true, + ); + + const expectedRightAxisPosition = { + dimensions: { + height: 100, + width: 40, + left: 110, + top: 0, + }, + topIncrement: 0, + bottomIncrement: 0, + leftIncrement: 0, + rightIncrement: 50, + }; + + expect(rightAxisPosition).toEqual(expectedRightAxisPosition); + }); + + test('should compute top axis position', () => { + const axisTitleHeight = 10; + const cumTopSum = 10; + const cumBottomSum = 10; + const cumLeftSum = 10; + const cumRightSum = 10; + + horizontalAxisSpec.position = Position.Top; + const topAxisPosition = getAxisPosition( + chartDim, + LIGHT_THEME.chartMargins, + axisTitleStyles(axisTitleHeight), + horizontalAxisSpec, + axis1Dims, + emptySmScales, + cumTopSum, + cumBottomSum, + cumLeftSum, + cumRightSum, + 10, + 0, + true, + ); + const { size: tickSize, padding: tickPadding } = LIGHT_THEME.axes.tickLine; + + const expectedTopAxisPosition = { + dimensions: { + height: axis1Dims.maxLabelBboxHeight + axisTitleHeight + tickSize + tickPadding, + width: 100, + left: 0, + top: cumTopSum + LIGHT_THEME.chartMargins.top, + }, + topIncrement: 50, + bottomIncrement: 0, + leftIncrement: 0, + rightIncrement: 0, + }; + + expect(topAxisPosition).toEqual(expectedTopAxisPosition); + }); + + test('should compute bottom axis position', () => { + const axisTitleHeight = 10; + const cumTopSum = 10; + const cumBottomSum = 10; + const cumLeftSum = 10; + const cumRightSum = 10; + + horizontalAxisSpec.position = Position.Bottom; + const bottomAxisPosition = getAxisPosition( + chartDim, + LIGHT_THEME.chartMargins, + axisTitleStyles(axisTitleHeight), + horizontalAxisSpec, + axis1Dims, + emptySmScales, + cumTopSum, + cumBottomSum, + cumLeftSum, + cumRightSum, + 10, + 0, + true, + ); + + const expectedBottomAxisPosition = { + dimensions: { + height: 40, + width: 100, + left: 0, + top: 110, + }, + topIncrement: 0, + bottomIncrement: 50, + leftIncrement: 0, + rightIncrement: 0, + }; + + expect(bottomAxisPosition).toEqual(expectedBottomAxisPosition); + }); + + test('should not compute axis ticks positions if missaligned specs', () => { + const axisSpecs = [verticalAxisSpec]; + const axisStyles = new Map(); + const axisDims = new Map(); + axisDims.set('not_a_mapped_one', axis1Dims); + + const axisTicksPosition = getAxesGeometries( + { + chartDimensions: chartDim, + leftMargin: 0, + }, + LIGHT_THEME, + NO_ROTATION, + axisSpecs, + axisDims, + axisStyles, + xDomain, + [yDomain], + emptySmScales, + 1, + false, + (v) => `${v}`, + ); + expect(axisTicksPosition).toHaveLength(0); + // expect(axisTicksPosition.axisTicks.size).toBe(0); + // expect(axisTicksPosition.axisGridLinesPositions.size).toBe(0); + // expect(axisTicksPosition.axisVisibleTicks.size).toBe(0); + }); + + test('should compute axis ticks positions', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockGlobalSpec.settingsNoMargins(), + lineSeriesSpec, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + hide: true, + gridLine: { + visible: true, + }, + }), + ], + store, + ); + const gridLines = computeGridLinesSelector(store.getState()); + + const expectedVerticalAxisGridLines = [ + [0, 0, 100, 0], + [0, 10, 100, 10], + [0, 20, 100, 20], + [0, 30, 100, 30], + [0, 40, 100, 40], + [0, 50, 100, 50], + [0, 60, 100, 60], + [0, 70, 100, 70], + [0, 80, 100, 80], + [0, 90, 100, 90], + [0, 100, 100, 100], + ]; + + const [{ lines }] = gridLines[0].lineGroups; + + expect(lines.map(({ x1, y1, x2, y2 }) => [x1, y1, x2, y2])).toEqual(expectedVerticalAxisGridLines); + + const axisTicksPositionWithTopLegend = computeAxesGeometriesSelector(store.getState()); + + const verticalAxisWithTopLegendPosition = axisTicksPositionWithTopLegend.find( + ({ axis: { id } }) => id === verticalAxisSpec.id, + ); + // TODO check the root cause of having with at 10 on previous implementation + expect(verticalAxisWithTopLegendPosition?.size).toEqual({ height: 0, width: 0 }); + expect(verticalAxisWithTopLegendPosition?.anchorPoint).toEqual({ x: 100, y: 0 }); + + const ungroupedAxisSpec = { ...verticalAxisSpec, groupId: 'foo' }; + const invalidSpecs = [ungroupedAxisSpec]; + const computeScalelessSpec = () => { + const axisDims = computeAxisTicksDimensionsSelector(store.getState()); + const axisStyles = getAxesStylesSelector(store.getState()); + getAxesGeometries( + { + chartDimensions: chartDim, + leftMargin: 0, + }, + LIGHT_THEME, + NO_ROTATION, + invalidSpecs, + axisDims, + axisStyles, + xDomain, + [yDomain], + emptySmScales, + 1, + false, + (v) => `${v}`, + ); + }; + + expect(computeScalelessSpec).toThrowError('Cannot compute scale for axis spec axis_1'); + }); + + test('should determine if axis belongs to yDomain', () => { + const verticalY = isYDomain(Position.Left, 0); + expect(verticalY).toBe(true); + + const verticalX = isYDomain(Position.Left, 90); + expect(verticalX).toBe(false); + + const horizontalX = isYDomain(Position.Top, 0); + expect(horizontalX).toBe(false); + + const horizontalY = isYDomain(Position.Top, 90); + expect(horizontalY).toBe(true); + }); + + test('should merge axis domains by group id', () => { + const groupId = 'group_1'; + const domainRange1 = { + min: 2, + max: 9, + }; + + verticalAxisSpec.domain = domainRange1; + + const axesSpecs = [verticalAxisSpec]; + + // Base case + const expectedSimpleMap = new Map(); + expectedSimpleMap.set(groupId, { min: 2, max: 9 }); + + const simpleDomainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, 0); + expect(simpleDomainsByGroupId).toEqual(expectedSimpleMap); + + // Multiple definitions for the same group + const domainRange2 = { + min: 0, + max: 7, + }; + + const altVerticalAxisSpec = { ...verticalAxisSpec, id: 'axis2' }; + + altVerticalAxisSpec.domain = domainRange2; + axesSpecs.push(altVerticalAxisSpec); + + const expectedMergedMap = new Map(); + expectedMergedMap.set(groupId, { min: 0, max: 9 }); + + const mergedDomainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, 0); + expect(mergedDomainsByGroupId).toEqual(expectedMergedMap); + + // xDomain limit (bad config) + horizontalAxisSpec.domain = { + min: 5, + max: 15, + }; + axesSpecs.push(horizontalAxisSpec); + + const attemptToMerge = () => { + mergeYCustomDomainsByGroupId(axesSpecs, 0); + }; + + expect(attemptToMerge).toThrowError('[Axis axis_2]: custom domain for xDomain should be defined in Settings'); + }); + + test('should merge axis domains by group id: partial upper bounded prevDomain with complete domain', () => { + const groupId = 'group_1'; + const domainRange1 = { + max: 9, + }; + + const domainRange2 = { + min: 0, + max: 7, + }; + + verticalAxisSpec.domain = domainRange1; + + const axis2 = { ...verticalAxisSpec, id: 'axis2' }; + + axis2.domain = domainRange2; + const axesSpecs = [verticalAxisSpec, axis2]; + + const expectedMergedMap = new Map(); + expectedMergedMap.set(groupId, { min: 0, max: 9 }); + + const mergedDomainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, 0); + expect(mergedDomainsByGroupId).toEqual(expectedMergedMap); + }); + + test('should merge axis domains by group id: partial lower bounded prevDomain with complete domain', () => { + const groupId = 'group_1'; + const domainRange1 = { + min: -1, + }; + + const domainRange2 = { + min: 0, + max: 7, + }; + + verticalAxisSpec.domain = domainRange1; + const axis2 = { ...verticalAxisSpec, id: 'axis2' }; + + const axesSpecs = [verticalAxisSpec, axis2]; + + axis2.domain = domainRange2; + + const expectedMergedMap = new Map(); + expectedMergedMap.set(groupId, { min: -1, max: 7 }); + + const mergedDomainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, 0); + expect(mergedDomainsByGroupId).toEqual(expectedMergedMap); + }); + + test('should merge axis domains by group id: partial upper bounded prevDomain with lower bounded domain', () => { + const groupId = 'group_1'; + const domainRange1 = { + max: 9, + }; + + const domainRange2 = { + min: 0, + }; + + const domainRange3 = { + min: -1, + }; + + verticalAxisSpec.domain = domainRange1; + + const axesSpecs = []; + axesSpecs.push(verticalAxisSpec); + + const axis2 = { ...verticalAxisSpec, id: 'axis2' }; + + axis2.domain = domainRange2; + axesSpecs.push(axis2); + + const axis3 = { ...verticalAxisSpec, id: 'axis3' }; + + axis3.domain = domainRange3; + axesSpecs.push(axis3); + + const expectedMergedMap = new Map(); + expectedMergedMap.set(groupId, { min: -1, max: 9 }); + + const mergedDomainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, 0); + expect(mergedDomainsByGroupId).toEqual(expectedMergedMap); + }); + + test('should merge axis domains by group id: partial lower bounded prevDomain with upper bounded domain', () => { + const groupId = 'group_1'; + const domainRange1 = { + min: 2, + }; + + const domainRange2 = { + max: 7, + }; + + const domainRange3 = { + max: 9, + }; + + verticalAxisSpec.domain = domainRange1; + + const axesSpecs = []; + axesSpecs.push(verticalAxisSpec); + + const axis2 = { ...verticalAxisSpec, id: 'axis2' }; + + axis2.domain = domainRange2; + axesSpecs.push(axis2); + + const axis3 = { ...verticalAxisSpec, id: 'axis3' }; + + axis3.domain = domainRange3; + axesSpecs.push(axis3); + + const expectedMergedMap = new Map(); + expectedMergedMap.set(groupId, { min: 2, max: 9 }); + + const mergedDomainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, 0); + expect(mergedDomainsByGroupId).toEqual(expectedMergedMap); + }); + + test('should throw on invalid domain', () => { + const domainRange1 = { + min: 9, + max: 2, + }; + + verticalAxisSpec.domain = domainRange1; + + const axesSpecs = [verticalAxisSpec]; + + const attemptToMerge = () => { + mergeYCustomDomainsByGroupId(axesSpecs, 0); + }; + const expectedError = '[Axis axis_1]: custom domain is invalid, min is greater than max'; + + expect(attemptToMerge).toThrowError(expectedError); + }); + + test('should show unique tick labels if duplicateTicks is set to false', () => { + const now = DateTime.fromISO('2019-01-11T00:00:00.000').setZone('utc+1').toMillis(); + const oneDay = moment.duration(1, 'day'); + const formatter = niceTimeFormatter([now, oneDay.add(now).asMilliseconds() * 31]); + const axisSpec: AxisSpec = { + id: 'bottom', + position: 'bottom', + showDuplicatedTicks: false, + chartType: 'xy_axis', + specType: 'axis', + groupId: DEFAULT_GLOBAL_ID, + hide: false, + showOverlappingLabels: false, + showOverlappingTicks: false, + style, + tickFormat: formatter, + }; + const xDomainTime = MockXDomain.fromScaleType(ScaleType.Time, { + isBandScale: false, + domain: [1547190000000, 1547622000000], + minInterval: 86400000, + }); + const scale: Scale = computeXScale({ xDomain: xDomainTime, totalBarsInCluster: 0, range: [0, 603.5] }); + const offset = 0; + const tickFormatOption = { timeZone: 'utc+1' }; + expect(enableDuplicatedTicks(axisSpec, scale, offset, (v) => `${v}`, tickFormatOption)).toEqual([ + { value: 1547208000000, label: '2019-01-11', position: 25.145833333333332 }, + { value: 1547251200000, label: '2019-01-12', position: 85.49583333333334 }, + { value: 1547337600000, label: '2019-01-13', position: 206.19583333333333 }, + { value: 1547424000000, label: '2019-01-14', position: 326.8958333333333 }, + { value: 1547510400000, label: '2019-01-15', position: 447.59583333333336 }, + { value: 1547596800000, label: '2019-01-16', position: 568.2958333333333 }, + ]); + }); + test('should show unique consecutive ticks if duplicateTicks is set to false', () => { + const axisSpec: AxisSpec = { + id: 'bottom', + position: 'bottom', + showDuplicatedTicks: false, + chartType: 'xy_axis', + specType: 'axis', + groupId: DEFAULT_GLOBAL_ID, + hide: false, + showOverlappingLabels: false, + showOverlappingTicks: false, + style, + tickFormat: (d, options) => + DateTime.fromMillis(d, { setZone: true, zone: options?.timeZone ?? 'utc+1' }).toFormat('HH:mm'), + }; + const xDomainTime = MockXDomain.fromScaleType(ScaleType.Time, { + isBandScale: false, + timeZone: 'utc+1', + domain: [1547190000000, 1547622000000], + minInterval: 86400000, + }); + const scale: Scale = computeXScale({ xDomain: xDomainTime, totalBarsInCluster: 0, range: [0, 603.5] }); + const offset = 0; + const tickFormatOption = { timeZone: xDomainTime.timeZone }; + const ticks = enableDuplicatedTicks(axisSpec, scale, offset, (v) => `${v}`, tickFormatOption); + const tickLabels = ticks.map(({ label }) => ({ label })); + expect(tickLabels).toEqual([ + { label: '12:00' }, + { label: '00:00' }, + { label: '12:00' }, + { label: '00:00' }, + { label: '12:00' }, + { label: '00:00' }, + { label: '12:00' }, + { label: '00:00' }, + { label: '12:00' }, + { label: '00:00' }, + ]); + }); + test('should show duplicate tick labels if duplicateTicks is set to true', () => { + const now = DateTime.fromISO('2019-01-11T00:00:00.000').setZone('utc+1').toMillis(); + const oneDay = moment.duration(1, 'day'); + const formatter = niceTimeFormatter([now, oneDay.add(now).asMilliseconds() * 31]); + const axisSpec: AxisSpec = { + id: 'bottom', + position: 'bottom', + showDuplicatedTicks: true, + chartType: 'xy_axis', + specType: 'axis', + groupId: DEFAULT_GLOBAL_ID, + hide: false, + showOverlappingLabels: false, + showOverlappingTicks: false, + style, + tickFormat: formatter, + }; + const xDomainTime = MockXDomain.fromScaleType(ScaleType.Time, { + isBandScale: false, + domain: [1547190000000, 1547622000000], + minInterval: 86400000, + }); + const scale: Scale = computeXScale({ xDomain: xDomainTime, totalBarsInCluster: 0, range: [0, 603.5] }); + const offset = 0; + const tickFormatOption = { timeZone: 'utc+1' }; + expect(enableDuplicatedTicks(axisSpec, scale, offset, (v) => `${v}`, tickFormatOption)).toEqual([ + { value: 1547208000000, label: '2019-01-11', position: 25.145833333333332 }, + { value: 1547251200000, label: '2019-01-12', position: 85.49583333333334 }, + { value: 1547294400000, label: '2019-01-12', position: 145.84583333333333 }, + { value: 1547337600000, label: '2019-01-13', position: 206.19583333333333 }, + { value: 1547380800000, label: '2019-01-13', position: 266.54583333333335 }, + { value: 1547424000000, label: '2019-01-14', position: 326.8958333333333 }, + { value: 1547467200000, label: '2019-01-14', position: 387.24583333333334 }, + { value: 1547510400000, label: '2019-01-15', position: 447.59583333333336 }, + { value: 1547553600000, label: '2019-01-15', position: 507.9458333333333 }, + { value: 1547596800000, label: '2019-01-16', position: 568.2958333333333 }, + ]); + }); + test('should use custom tick formatter', () => { + const now = DateTime.fromISO('2019-01-11T00:00:00.000').setZone('utc+1').toMillis(); + const oneDay = moment.duration(1, 'day'); + const formatter = niceTimeFormatter([now, oneDay.add(now).asMilliseconds() * 31]); + const axisSpec: AxisSpec = { + id: 'bottom', + position: 'bottom', + showDuplicatedTicks: true, + chartType: 'xy_axis', + specType: 'axis', + groupId: DEFAULT_GLOBAL_ID, + hide: false, + showOverlappingLabels: false, + showOverlappingTicks: false, + style, + tickFormat: formatter, + }; + const xDomainTime = MockXDomain.fromScaleType(ScaleType.Time, { + isBandScale: false, + domain: [1547190000000, 1547622000000], + minInterval: 86400000, + }); + const scale: Scale = computeXScale({ xDomain: xDomainTime, totalBarsInCluster: 0, range: [0, 603.5] }); + const offset = 0; + const tickFormatOption = { timeZone: 'utc+1' }; + expect(enableDuplicatedTicks(axisSpec, scale, offset, (v) => `${v}`, tickFormatOption)).toEqual([ + { value: 1547208000000, label: '2019-01-11', position: 25.145833333333332 }, + { value: 1547251200000, label: '2019-01-12', position: 85.49583333333334 }, + { value: 1547294400000, label: '2019-01-12', position: 145.84583333333333 }, + { value: 1547337600000, label: '2019-01-13', position: 206.19583333333333 }, + { value: 1547380800000, label: '2019-01-13', position: 266.54583333333335 }, + { value: 1547424000000, label: '2019-01-14', position: 326.8958333333333 }, + { value: 1547467200000, label: '2019-01-14', position: 387.24583333333334 }, + { value: 1547510400000, label: '2019-01-15', position: 447.59583333333336 }, + { value: 1547553600000, label: '2019-01-15', position: 507.9458333333333 }, + { value: 1547596800000, label: '2019-01-16', position: 568.2958333333333 }, + ]); + }); + + describe('Custom formatting', () => { + it('should get custom labels for y axis', () => { + const customFormatter = (v: any) => `${v} custom`; + const axisSpecs = [verticalAxisSpec]; + const axesStyles = new Map(); + const axisDims = new Map(); + axisDims.set(verticalAxisSpec.id, axis1Dims); + + const axisTicksPosition = getAxesGeometries( + { + chartDimensions: chartDim, + leftMargin: 0, + }, + LIGHT_THEME, + NO_ROTATION, + axisSpecs, + axisDims, + axesStyles, + xDomain, + [yDomain], + emptySmScales, + 1, + false, + customFormatter, + ); + + const expected = axis1Dims.tickValues.slice().reverse().map(customFormatter); + const axisPos = axisTicksPosition.find(({ axis: { id } }) => id === verticalAxisSpec.id); + expect(axisPos?.ticks.map(({ label }) => label)).toEqual(expected); + }); + + it('should not use custom formatter with x axis', () => { + const customFotmatter = (v: any) => `${v} custom`; + const axisSpecs = [horizontalAxisSpec]; + const axesStyles = new Map(); + const axisDims = new Map(); + axisDims.set(horizontalAxisSpec.id, axis1Dims); + + const axisTicksPosition = getAxesGeometries( + { + chartDimensions: chartDim, + leftMargin: 0, + }, + LIGHT_THEME, + NO_ROTATION, + axisSpecs, + axisDims, + axesStyles, + xDomain, + [yDomain], + emptySmScales, + 1, + false, + customFotmatter, + ); + + const expected = axis1Dims.tickValues.slice().map(defaultTickFormatter); + expect( + axisTicksPosition.find(({ axis: { id } }) => id === horizontalAxisSpec.id)!.ticks.map(({ label }) => label), + ).toEqual(expected); + }); + + it('should use custom axis tick formatter to get labels for x axis', () => { + const customFotmatter = (v: any) => `${v} custom`; + const customAxisFotmatter = (v: any) => `${v} custom`; + const spec: AxisSpec = { + ...horizontalAxisSpec, + tickFormat: customAxisFotmatter, + }; + const axisSpecs = [spec]; + const axesStyles = new Map(); + const axisDims = new Map(); + axisDims.set(spec.id, axis1Dims); + + const axisTicksPosition = getAxesGeometries( + { + chartDimensions: chartDim, + leftMargin: 0, + }, + LIGHT_THEME, + NO_ROTATION, + axisSpecs, + axisDims, + axesStyles, + xDomain, + [yDomain], + emptySmScales, + 1, + false, + customFotmatter, + ); + + const expected = axis1Dims.tickValues.slice().map(customAxisFotmatter); + expect(axisTicksPosition.find(({ axis: { id } }) => id === spec.id)!.ticks.map(({ label }) => label)).toEqual( + expected, + ); + }); + }); + + describe('Small multiples', () => { + const axisStyles = axisTitleStyles(10, 8); + const cumTopSum = 10; + const cumBottomSum = 10; + const cumLeftSum = 10; + const cumRightSum = 10; + const smScales = getSmScales(['a'], [0]); + + describe.each(['test', ''])('Axes title positions - title is "%s"', (title) => { + test('should compute left axis position', () => { + const leftAxisPosition = getAxisPosition( + chartDim, + LIGHT_THEME.chartMargins, + axisStyles, + { ...verticalAxisSpec, title }, + axis1Dims, + smScales, + cumTopSum, + cumBottomSum, + cumLeftSum, + cumRightSum, + 10, + 0, + true, + ); + + const expectedLeftAxisPosition = { + dimensions: { + height: 100, + width: title ? 56 : 36, + left: 110, + top: 0, + }, + topIncrement: 0, + bottomIncrement: 0, + leftIncrement: 0, + rightIncrement: title ? 66 : 46, + }; + + expect(leftAxisPosition).toEqual(expectedLeftAxisPosition); + }); + + test('should compute right axis position', () => { + verticalAxisSpec.position = Position.Right; + const rightAxisPosition = getAxisPosition( + chartDim, + LIGHT_THEME.chartMargins, + axisStyles, + { ...verticalAxisSpec, title }, + axis1Dims, + smScales, + cumTopSum, + cumBottomSum, + cumLeftSum, + cumRightSum, + 10, + 0, + true, + ); + + const expectedRightAxisPosition = { + dimensions: { + height: 100, + width: title ? 56 : 36, + left: 110, + top: 0, + }, + topIncrement: 0, + bottomIncrement: 0, + leftIncrement: 0, + rightIncrement: title ? 66 : 46, + }; + + expect(rightAxisPosition).toEqual(expectedRightAxisPosition); + }); + + test('should compute top axis position', () => { + horizontalAxisSpec.position = Position.Top; + const topAxisPosition = getAxisPosition( + chartDim, + LIGHT_THEME.chartMargins, + axisStyles, + { ...horizontalAxisSpec, title }, + axis1Dims, + smScales, + cumTopSum, + cumBottomSum, + cumLeftSum, + cumRightSum, + 10, + 0, + true, + ); + + const expectedTopAxisPosition = { + dimensions: { + height: title ? 56 : 36, + width: 100, + left: 0, + top: 20, + }, + topIncrement: title ? 66 : 46, + bottomIncrement: 0, + leftIncrement: 0, + rightIncrement: 0, + }; + + expect(topAxisPosition).toEqual(expectedTopAxisPosition); + }); + + test('should compute bottom axis position', () => { + horizontalAxisSpec.position = Position.Bottom; + const bottomAxisPosition = getAxisPosition( + chartDim, + LIGHT_THEME.chartMargins, + axisStyles, + { ...horizontalAxisSpec, title }, + axis1Dims, + smScales, + cumTopSum, + cumBottomSum, + cumLeftSum, + cumRightSum, + 10, + 0, + true, + ); + + const expectedBottomAxisPosition = { + dimensions: { + height: title ? 56 : 36, + width: 100, + left: 0, + top: 110, + }, + topIncrement: 0, + bottomIncrement: title ? 66 : 46, + leftIncrement: 0, + rightIncrement: 0, + }; + expect(bottomAxisPosition).toEqual(expectedBottomAxisPosition); + }); + }); + }); +}); + +it.todo('Test alignment calculations'); +it.todo('Test text offsets calculations'); +it.todo('Test title padding calculations'); +it.todo('Test label padding calculations'); +it.todo('Test axis visibility'); diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/axis_utils.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/axis_utils.ts new file mode 100644 index 000000000000..8f83de2223e0 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/axis_utils.ts @@ -0,0 +1,940 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Line } from '../../../geoms/types'; +import { Scale } from '../../../scales'; +import { BBox, BBoxCalculator } from '../../../utils/bbox/bbox_calculator'; +import { + Position, + Rotation, + getUniqueValues, + VerticalAlignment, + HorizontalAlignment, + getPercentageValue, + getRadians, +} from '../../../utils/common'; +import { Dimensions, Margins, getSimplePadding, Size } from '../../../utils/dimensions'; +import { Range } from '../../../utils/domain'; +import { AxisId } from '../../../utils/ids'; +import { Logger } from '../../../utils/logger'; +import { Point } from '../../../utils/point'; +import { AxisStyle, Theme, TextAlignment, TextOffset } from '../../../utils/themes/theme'; +import { XDomain, YDomain } from '../domains/types'; +import { MIN_STROKE_WIDTH } from '../renderer/canvas/primitives/line'; +import { SmallMultipleScales } from '../state/selectors/compute_small_multiple_scales'; +import { getSpecsById } from '../state/utils/spec'; +import { isVerticalAxis } from './axis_type_utils'; +import { getPanelSize, hasSMDomain } from './panel'; +import { computeXScale, computeYScales } from './scales'; +import { AxisSpec, TickFormatterOptions, TickFormatter } from './specs'; + +/** @internal */ +export interface AxisTick { + value: number | string; + label: string; + position: number; +} + +/** @internal */ +export interface AxisTicksDimensions { + tickValues: string[] | number[]; + tickLabels: string[]; + maxLabelBboxWidth: number; + maxLabelBboxHeight: number; + maxLabelTextWidth: number; + maxLabelTextHeight: number; + isHidden: boolean; +} + +/** @internal */ +export interface TickLabelProps { + x: number; + y: number; + offsetX: number; + offsetY: number; + textOffsetX: number; + textOffsetY: number; + horizontalAlign: Extract< + HorizontalAlignment, + typeof HorizontalAlignment.Left | typeof HorizontalAlignment.Center | typeof HorizontalAlignment.Right + >; + verticalAlign: Extract< + VerticalAlignment, + typeof VerticalAlignment.Top | typeof VerticalAlignment.Middle | typeof VerticalAlignment.Bottom + >; +} + +/** @internal */ +export const defaultTickFormatter = (tick: any) => `${tick}`; + +/** + * Compute the ticks values and identify max width and height of the labels + * so we can compute the max space occupied by the axis component. + * @param axisSpec the spec of the axis + * @param xDomain the x domain associated + * @param yDomains the y domain array + * @param totalBarsInCluster the total number of grouped series + * @param bboxCalculator an instance of the boundingbox calculator + * @param chartRotation the rotation of the chart + * @param gridLine + * @param tickLabel + * @param fallBackTickFormatter + * @param barsPadding + * @param enableHistogramMode + * @internal + */ +export function computeAxisTicksDimensions( + axisSpec: AxisSpec, + xDomain: XDomain, + yDomains: YDomain[], + totalBarsInCluster: number, + bboxCalculator: BBoxCalculator, + chartRotation: Rotation, + { gridLine, tickLabel }: AxisStyle, + fallBackTickFormatter: TickFormatter, + barsPadding?: number, + enableHistogramMode?: boolean, +): AxisTicksDimensions | null { + const gridLineVisible = isVerticalAxis(axisSpec.position) ? gridLine.vertical.visible : gridLine.horizontal.visible; + + // don't compute anything on this axis if grid is hidden and axis is hidden + if (axisSpec.hide && !gridLineVisible) { + return null; + } + + const scale = getScaleForAxisSpec( + axisSpec, + xDomain, + yDomains, + totalBarsInCluster, + chartRotation, + 0, + 1, + barsPadding, + enableHistogramMode, + ); + + if (!scale) { + Logger.warn(`Cannot compute scale for axis spec ${axisSpec.id}. Axis will not be displayed.`); + + return null; + } + + const dimensions = computeTickDimensions( + scale, + axisSpec.labelFormat ?? axisSpec.tickFormat ?? fallBackTickFormatter, + bboxCalculator, + tickLabel, + { timeZone: xDomain.timeZone }, + ); + + return { + ...dimensions, + isHidden: axisSpec.hide && gridLineVisible, + }; +} + +/** @internal */ +export function isYDomain(position: Position, chartRotation: Rotation): boolean { + const isStraightRotation = chartRotation === 0 || chartRotation === 180; + if (isVerticalAxis(position)) { + return isStraightRotation; + } + + return !isStraightRotation; +} + +/** @internal */ +export function getScaleForAxisSpec( + axisSpec: AxisSpec, + xDomain: XDomain, + yDomains: YDomain[], + totalBarsInCluster: number, + chartRotation: Rotation, + minRange: number, + maxRange: number, + barsPadding?: number, + enableHistogramMode?: boolean, +): Scale | null { + const axisIsYDomain = isYDomain(axisSpec.position, chartRotation); + const range: Range = [minRange, maxRange]; + if (axisIsYDomain) { + const yScales = computeYScales({ + yDomains, + range, + integersOnly: axisSpec.integersOnly, + }); + if (yScales.has(axisSpec.groupId)) { + return yScales.get(axisSpec.groupId)!; + } + return null; + } + return computeXScale({ + xDomain, + totalBarsInCluster, + range, + barsPadding, + enableHistogramMode, + integersOnly: axisSpec.integersOnly, + }); +} + +/** @internal */ +export function computeRotatedLabelDimensions(unrotatedDims: BBox, degreesRotation: number): BBox { + const { width, height } = unrotatedDims; + const radians = getRadians(degreesRotation); + const rotatedHeight = Math.abs(width * Math.sin(radians)) + Math.abs(height * Math.cos(radians)); + const rotatedWidth = Math.abs(width * Math.cos(radians)) + Math.abs(height * Math.sin(radians)); + + return { + width: rotatedWidth, + height: rotatedHeight, + }; +} + +/** @internal */ +export const getMaxLabelDimensions = ( + bboxCalculator: BBoxCalculator, + { fontSize, fontFamily, rotation }: AxisStyle['tickLabel'], +) => ( + acc: Record, + tickLabel: string, +): { + maxLabelBboxWidth: number; + maxLabelBboxHeight: number; + maxLabelTextWidth: number; + maxLabelTextHeight: number; +} => { + const bbox = bboxCalculator.compute(tickLabel, 0, fontSize, fontFamily); + const rotatedBbox = computeRotatedLabelDimensions(bbox, rotation); + + const width = Math.ceil(rotatedBbox.width); + const height = Math.ceil(rotatedBbox.height); + const labelWidth = Math.ceil(bbox.width); + const labelHeight = Math.ceil(bbox.height); + + const prevWidth = acc.maxLabelBboxWidth; + const prevHeight = acc.maxLabelBboxHeight; + const prevLabelWidth = acc.maxLabelTextWidth; + const prevLabelHeight = acc.maxLabelTextHeight; + return { + maxLabelBboxWidth: prevWidth > width ? prevWidth : width, + maxLabelBboxHeight: prevHeight > height ? prevHeight : height, + maxLabelTextWidth: prevLabelWidth > labelWidth ? prevLabelWidth : labelWidth, + maxLabelTextHeight: prevLabelHeight > labelHeight ? prevLabelHeight : labelHeight, + }; +}; + +function computeTickDimensions( + scale: Scale, + tickFormat: TickFormatter, + bboxCalculator: BBoxCalculator, + tickLabelStyle: AxisStyle['tickLabel'], + tickFormatOptions?: TickFormatterOptions, +) { + const tickValues = scale.ticks(); + const tickLabels = tickValues.map((d) => tickFormat(d, tickFormatOptions)); + const defaultAcc = { maxLabelBboxWidth: 0, maxLabelBboxHeight: 0, maxLabelTextWidth: 0, maxLabelTextHeight: 0 }; + const dimensions = tickLabelStyle.visible + ? tickLabels.reduce(getMaxLabelDimensions(bboxCalculator, tickLabelStyle), defaultAcc) + : defaultAcc; + + return { + ...dimensions, + tickValues, + tickLabels, + }; +} + +function getUserTextOffsets(dimensions: AxisTicksDimensions, offset: TextOffset) { + const defaults = { + x: 0, + y: 0, + }; + + if (offset.reference === 'global') { + return { + local: defaults, + global: { + x: getPercentageValue(offset.x, dimensions.maxLabelBboxWidth, 0), + y: getPercentageValue(offset.y, dimensions.maxLabelBboxHeight, 0), + }, + }; + } + + return { + global: defaults, + local: { + x: getPercentageValue(offset.x, dimensions.maxLabelTextWidth, 0), + y: getPercentageValue(offset.y, dimensions.maxLabelTextHeight, 0), + }, + }; +} + +function getHorizontalTextOffset( + width: number, + alignment: Extract< + HorizontalAlignment, + typeof HorizontalAlignment.Left | typeof HorizontalAlignment.Center | typeof HorizontalAlignment.Right + >, +): number { + switch (alignment) { + case HorizontalAlignment.Left: + return -width / 2; + case HorizontalAlignment.Right: + return width / 2; + case HorizontalAlignment.Center: + default: + return 0; + } +} + +function getVerticalTextOffset( + height: number, + alignment: Extract< + VerticalAlignment, + typeof VerticalAlignment.Top | typeof VerticalAlignment.Middle | typeof VerticalAlignment.Bottom + >, +): number { + switch (alignment) { + case VerticalAlignment.Top: + return -height / 2; + case VerticalAlignment.Bottom: + return height / 2; + case VerticalAlignment.Middle: + default: + return 0; + } +} + +function getHorizontalAlign( + position: Position, + rotation: number, + alignment: HorizontalAlignment = HorizontalAlignment.Near, +): Exclude { + if ( + alignment === HorizontalAlignment.Center || + alignment === HorizontalAlignment.Right || + alignment === HorizontalAlignment.Left + ) { + return alignment; + } + + if ([-90, 90].includes(rotation)) { + if (position === Position.Left || position === Position.Right) { + return HorizontalAlignment.Center; + } + + if (position === Position.Top) { + return rotation === 90 ? HorizontalAlignment.Right : HorizontalAlignment.Left; + } + + return rotation === -90 ? HorizontalAlignment.Right : HorizontalAlignment.Left; + } + + if ([0, 180].includes(rotation) && (position === Position.Bottom || position === Position.Top)) { + return HorizontalAlignment.Center; + } + + if (position === Position.Left) { + return alignment === HorizontalAlignment.Near ? HorizontalAlignment.Right : HorizontalAlignment.Left; + } + + if (position === Position.Right) { + return alignment === HorizontalAlignment.Near ? HorizontalAlignment.Left : HorizontalAlignment.Right; + } + + // fallback for near/far on top/bottom axis + return HorizontalAlignment.Center; +} + +function getVerticalAlign( + position: Position, + rotation: number, + alignment: VerticalAlignment = VerticalAlignment.Middle, +): Exclude { + if ( + alignment === VerticalAlignment.Middle || + alignment === VerticalAlignment.Top || + alignment === VerticalAlignment.Bottom + ) { + return alignment; + } + + if ([0, 180].includes(rotation)) { + if (position === Position.Bottom || position === Position.Top) { + return VerticalAlignment.Middle; + } + + if (position === Position.Left) { + return rotation === 0 ? VerticalAlignment.Bottom : VerticalAlignment.Top; + } + + return rotation === 180 ? VerticalAlignment.Bottom : VerticalAlignment.Top; + } + + if (position === Position.Top) { + return alignment === VerticalAlignment.Near ? VerticalAlignment.Bottom : VerticalAlignment.Top; + } + + if (position === Position.Bottom) { + return alignment === VerticalAlignment.Near ? VerticalAlignment.Top : VerticalAlignment.Bottom; + } + + // fallback for near/far on left/right axis + return VerticalAlignment.Middle; +} + +/** + * Gets the computed x/y coordinates & alignment properties for an axis tick label. + * @internal + */ +export function getTickLabelProps( + { tickLine, tickLabel }: AxisStyle, + tickPosition: number, + position: Position, + rotation: number, + axisSize: Size, + tickDimensions: AxisTicksDimensions, + showTicks: boolean, + textOffset: TextOffset, + textAlignment?: TextAlignment, +): TickLabelProps { + const { maxLabelBboxWidth, maxLabelTextWidth, maxLabelBboxHeight, maxLabelTextHeight } = tickDimensions; + const tickDimension = showTicks ? tickLine.size + tickLine.padding : 0; + const labelPadding = getSimplePadding(tickLabel.padding); + const isLeftAxis = position === Position.Left; + const isAxisTop = position === Position.Top; + const horizontalAlign = getHorizontalAlign(position, rotation, textAlignment?.horizontal); + const verticalAlign = getVerticalAlign(position, rotation, textAlignment?.vertical); + + const userOffsets = getUserTextOffsets(tickDimensions, textOffset); + const textOffsetX = getHorizontalTextOffset(maxLabelTextWidth, horizontalAlign) + userOffsets.local.x; + const textOffsetY = getVerticalTextOffset(maxLabelTextHeight, verticalAlign) + userOffsets.local.y; + + if (isVerticalAxis(position)) { + const x = isLeftAxis ? axisSize.width - tickDimension - labelPadding.inner : tickDimension + labelPadding.inner; + const offsetX = (isLeftAxis ? -1 : 1) * (maxLabelBboxWidth / 2); + + return { + x, + y: tickPosition, + offsetX: offsetX + userOffsets.global.x, + offsetY: userOffsets.global.y, + textOffsetY, + textOffsetX, + horizontalAlign, + verticalAlign, + }; + } + + const offsetY = isAxisTop ? -maxLabelBboxHeight / 2 : maxLabelBboxHeight / 2; + + return { + x: tickPosition, + y: isAxisTop ? axisSize.height - tickDimension - labelPadding.inner : tickDimension + labelPadding.inner, + offsetX: userOffsets.global.x, + offsetY: offsetY + userOffsets.global.y, + textOffsetX, + textOffsetY, + horizontalAlign, + verticalAlign, + }; +} + +/** @internal */ +export function getVerticalAxisTickLineProps( + position: Position, + axisWidth: number, + tickSize: number, + tickPosition: number, +): Line { + const isLeftAxis = position === Position.Left; + const y = tickPosition; + const x1 = isLeftAxis ? axisWidth : 0; + const x2 = isLeftAxis ? axisWidth - tickSize : tickSize; + + return { x1, y1: y, x2, y2: y }; +} + +/** @internal */ +export function getHorizontalAxisTickLineProps( + position: Position, + axisHeight: number, + tickSize: number, + tickPosition: number, +): Line { + const isTopAxis = position === Position.Top; + const x = tickPosition; + const y1 = isTopAxis ? axisHeight - tickSize : 0; + const y2 = isTopAxis ? axisHeight : tickSize; + + return { x1: x, y1, x2: x, y2 }; +} + +/** @internal */ +export function getMinMaxRange( + axisPosition: Position, + chartRotation: Rotation, + { width, height }: Size, +): { + minRange: number; + maxRange: number; +} { + switch (axisPosition) { + case Position.Bottom: + case Position.Top: + return getBottomTopAxisMinMaxRange(chartRotation, width); + case Position.Left: + case Position.Right: + default: + return getLeftAxisMinMaxRange(chartRotation, height); + } +} + +function getBottomTopAxisMinMaxRange(chartRotation: Rotation, width: number) { + switch (chartRotation) { + case 90: + // dealing with y domain + return { minRange: 0, maxRange: width }; + case -90: + // dealing with y domain + return { minRange: width, maxRange: 0 }; + case 180: + // dealing with x domain + return { minRange: width, maxRange: 0 }; + case 0: + default: + // dealing with x domain + return { minRange: 0, maxRange: width }; + } +} +function getLeftAxisMinMaxRange(chartRotation: Rotation, height: number) { + switch (chartRotation) { + case 90: + // dealing with x domain + return { minRange: 0, maxRange: height }; + case -90: + // dealing with x domain + return { minRange: height, maxRange: 0 }; + case 180: + // dealing with y domain + return { minRange: 0, maxRange: height }; + case 0: + default: + // dealing with y domain + return { minRange: height, maxRange: 0 }; + } +} + +/** @internal */ +export function getAvailableTicks( + axisSpec: AxisSpec, + scale: Scale, + totalBarsInCluster: number, + enableHistogramMode: boolean, + fallBackTickFormatter: TickFormatter, + rotationOffset: number, + tickFormatOptions?: TickFormatterOptions, +): AxisTick[] { + const ticks = scale.ticks(); + const isSingleValueScale = scale.domain[0] - scale.domain[1] === 0; + const hasAdditionalTicks = enableHistogramMode && scale.bandwidth > 0; + + if (hasAdditionalTicks) { + const lastComputedTick = ticks[ticks.length - 1]; + + if (!isSingleValueScale) { + const penultimateComputedTick = ticks[ticks.length - 2]; + const computedTickDistance = lastComputedTick - penultimateComputedTick; + const numTicks = scale.minInterval / computedTickDistance; + + for (let i = 1; i <= numTicks; i++) { + ticks.push(i * computedTickDistance + lastComputedTick); + } + } + } + const shift = totalBarsInCluster > 0 ? totalBarsInCluster : 1; + + const band = scale.bandwidth / (1 - scale.barsPadding); + const halfPadding = (band - scale.bandwidth) / 2; + const offset = + (enableHistogramMode ? -halfPadding : (scale.bandwidth * shift) / 2) + (scale.isSingleValue() ? 0 : rotationOffset); + const tickFormatter = axisSpec.tickFormat ?? fallBackTickFormatter; + + if (isSingleValueScale && hasAdditionalTicks) { + const [firstTickValue] = ticks; + const firstTick = { + value: firstTickValue, + label: tickFormatter(firstTickValue, tickFormatOptions), + position: (scale.scale(firstTickValue) ?? 0) + offset, + }; + + const lastTickValue = firstTickValue + scale.minInterval; + const lastTick = { + value: lastTickValue, + label: tickFormatter(lastTickValue, tickFormatOptions), + position: scale.bandwidth + halfPadding * 2, + }; + + return [firstTick, lastTick]; + } + return enableDuplicatedTicks(axisSpec, scale, offset, fallBackTickFormatter, tickFormatOptions); +} + +/** @internal */ +export function enableDuplicatedTicks( + axisSpec: AxisSpec, + scale: Scale, + offset: number, + fallBackTickFormatter: TickFormatter, + tickFormatOptions?: TickFormatterOptions, +): AxisTick[] { + const ticks = scale.ticks(); + const allTicks: AxisTick[] = ticks.map((tick) => ({ + value: tick, + // TODO handle empty string tick formatting + label: (axisSpec.tickFormat ?? fallBackTickFormatter)(tick, tickFormatOptions), + position: (scale.scale(tick) ?? 0) + offset, + })); + + if (axisSpec.showDuplicatedTicks === true) { + return allTicks; + } + return getUniqueValues(allTicks, 'label', true); +} + +/** @internal */ +export function getVisibleTicks(allTicks: AxisTick[], axisSpec: AxisSpec, axisDim: AxisTicksDimensions): AxisTick[] { + // We sort the ticks by position so that we can incrementally compute previousOccupiedSpace + allTicks.sort((a: AxisTick, b: AxisTick) => a.position - b.position); + + const { showOverlappingTicks, showOverlappingLabels } = axisSpec; + const { maxLabelBboxHeight, maxLabelBboxWidth } = axisDim; + + const requiredSpace = isVerticalAxis(axisSpec.position) ? maxLabelBboxHeight / 2 : maxLabelBboxWidth / 2; + + let previousOccupiedSpace = 0; + const visibleTicks = []; + for (let i = 0; i < allTicks.length; i++) { + const { position } = allTicks[i]; + + if (i === 0) { + visibleTicks.push(allTicks[i]); + previousOccupiedSpace = position + requiredSpace; + } else if (position - requiredSpace >= previousOccupiedSpace) { + visibleTicks.push(allTicks[i]); + previousOccupiedSpace = position + requiredSpace; + } else { + // still add the tick but without a label + if (showOverlappingTicks || showOverlappingLabels) { + const overlappingTick = { + ...allTicks[i], + label: showOverlappingLabels ? allTicks[i].label : '', + }; + visibleTicks.push(overlappingTick); + } + } + } + return visibleTicks; +} + +/** @internal */ +export function getTitleDimension({ + visible, + fontSize, + padding, +}: AxisStyle['axisTitle'] | AxisStyle['axisPanelTitle']): number { + if (!visible || fontSize <= 0) return 0; + const { inner, outer } = getSimplePadding(padding); + return inner + fontSize + outer; +} + +/** @internal */ +export function getAxisPosition( + chartDimensions: Dimensions, + chartMargins: Margins, + { axisTitle, axisPanelTitle }: Pick, + axisSpec: AxisSpec, + axisDim: AxisTicksDimensions, + smScales: SmallMultipleScales, + cumTopSum: number, + cumBottomSum: number, + cumLeftSum: number, + cumRightSum: number, + tickDimension: number, + labelPaddingSum: number, + showLabels: boolean, +) { + const titleDimension = axisSpec.title ? getTitleDimension(axisTitle) : 0; + const { position } = axisSpec; + const { maxLabelBboxHeight, maxLabelBboxWidth } = axisDim; + const { top, left, height, width } = chartDimensions; + const dimensions = { + top, + left, + width, + height, + }; + let topIncrement = 0; + let bottomIncrement = 0; + let leftIncrement = 0; + let rightIncrement = 0; + + if (isVerticalAxis(position)) { + const panelTitleDimension = hasSMDomain(smScales.vertical) ? getTitleDimension(axisPanelTitle) : 0; + const dimWidth = + labelPaddingSum + (showLabels ? maxLabelBboxWidth : 0) + tickDimension + titleDimension + panelTitleDimension; + if (position === Position.Left) { + leftIncrement = chartMargins.left + dimWidth; + dimensions.left = cumLeftSum + chartMargins.left; + } else { + rightIncrement = dimWidth + chartMargins.right; + dimensions.left = left + width + cumRightSum; + } + dimensions.width = dimWidth; + } else { + const panelTitleDimension = hasSMDomain(smScales.horizontal) ? getTitleDimension(axisPanelTitle) : 0; + const dimHeight = + labelPaddingSum + (showLabels ? maxLabelBboxHeight : 0) + tickDimension + titleDimension + panelTitleDimension; + if (position === Position.Top) { + topIncrement = dimHeight + chartMargins.top; + dimensions.top = cumTopSum + chartMargins.top; + } else { + bottomIncrement = dimHeight + chartMargins.bottom; + dimensions.top = top + height + cumBottomSum; + } + dimensions.height = dimHeight; + } + + return { dimensions, topIncrement, bottomIncrement, leftIncrement, rightIncrement }; +} + +/** @internal */ +export function shouldShowTicks({ visible, strokeWidth, size }: AxisStyle['tickLine'], axisHidden: boolean): boolean { + return !axisHidden && visible && size > 0 && strokeWidth >= MIN_STROKE_WIDTH; +} + +/** @internal */ +export interface AxisGeometry { + anchorPoint: Point; + size: Size; + parentSize: Size; + axis: { + id: AxisId; + position: Position; + panelTitle?: string; // defined later per panel + secondary?: boolean; // defined later per panel + }; + dimension: AxisTicksDimensions; + ticks: AxisTick[]; + visibleTicks: AxisTick[]; +} + +/** @internal */ +export function getAxesGeometries( + computedChartDims: { + chartDimensions: Dimensions; + leftMargin: number; + }, + { chartPaddings, chartMargins, axes: sharedAxesStyle }: Theme, + chartRotation: Rotation, + axisSpecs: AxisSpec[], + axisDimensions: Map, + axesStyles: Map, + xDomain: XDomain, + yDomains: YDomain[], + smScales: SmallMultipleScales, + totalGroupsCount: number, + enableHistogramMode: boolean, + fallBackTickFormatter: TickFormatter, + barsPadding?: number, +): Array { + const axesGeometries: Array = []; + const { chartDimensions } = computedChartDims; + const panel = getPanelSize(smScales); + + const anchorPointByAxisGroups = [...axisDimensions.entries()].reduce( + (acc, [axisId, dimension]) => { + const axisSpec = getSpecsById(axisSpecs, axisId); + + if (!axisSpec) { + return acc; + } + + const { tickLine, tickLabel, ...otherStyles } = axesStyles.get(axisId) ?? sharedAxesStyle; + const labelPadding = getSimplePadding(tickLabel.padding); + const showTicks = shouldShowTicks(tickLine, axisSpec.hide); + const tickDimension = showTicks ? tickLine.size + tickLine.padding : 0; + const labelPaddingSum = tickLabel.visible ? labelPadding.inner + labelPadding.outer : 0; + + const { dimensions, topIncrement, bottomIncrement, leftIncrement, rightIncrement } = getAxisPosition( + chartDimensions, + chartMargins, + otherStyles, + axisSpec, + dimension, + smScales, + acc.top, + acc.bottom, + acc.left, + acc.right, + tickDimension, + labelPaddingSum, + tickLabel.visible, + ); + + acc.pos.set(axisId, { + anchor: { + top: acc.top, + left: acc.left, + right: acc.right, + bottom: acc.bottom, + }, + dimensions, + }); + return { + top: acc.top + topIncrement, + bottom: acc.bottom + bottomIncrement, + left: acc.left + leftIncrement, + right: acc.right + rightIncrement, + pos: acc.pos, + }; + }, + { + top: 0, + bottom: chartPaddings.bottom, + left: computedChartDims.leftMargin, + right: chartPaddings.right, + pos: new Map< + AxisId, + { + anchor: { left: number; right: number; top: number; bottom: number }; + dimensions: Dimensions; + } + >(), + }, + ).pos; + + axisDimensions.forEach((axisDim, id) => { + const axisSpec = getSpecsById(axisSpecs, id); + const anchorPoint = anchorPointByAxisGroups.get(id); + // Consider refactoring this so this condition can be tested + // Given some of the values that get passed around, maybe re-write as a reduce instead of forEach? + if (!axisSpec || !anchorPoint) { + return; + } + + const isVertical = isVerticalAxis(axisSpec.position); + const minMaxRanges = getMinMaxRange(axisSpec.position, chartRotation, panel); + + const scale = getScaleForAxisSpec( + axisSpec, + xDomain, + yDomains, + totalGroupsCount, + chartRotation, + minMaxRanges.minRange, + minMaxRanges.maxRange, + barsPadding, + enableHistogramMode, + ); + + if (!scale) { + throw new Error(`Cannot compute scale for axis spec ${axisSpec.id}`); + } + const tickFormatOptions = { + timeZone: xDomain.timeZone, + }; + + // TODO: Find the true cause of the this offset error + const rotationOffset = + enableHistogramMode && + ((isVertical && [-90].includes(chartRotation)) || (!isVertical && [180].includes(chartRotation))) + ? scale.step + : 0; + + const allTicks = getAvailableTicks( + axisSpec, + scale, + totalGroupsCount, + enableHistogramMode, + isVertical ? fallBackTickFormatter : defaultTickFormatter, + rotationOffset, + tickFormatOptions, + ); + const visibleTicks = getVisibleTicks(allTicks, axisSpec, axisDim); + + const size = axisDim.isHidden + ? { width: 0, height: 0 } + : { + width: isVertical ? anchorPoint.dimensions.width : panel.width, + height: isVertical ? panel.height : anchorPoint.dimensions.height, + }; + axesGeometries.push({ + axis: { + id: axisSpec.id, + position: axisSpec.position, + }, + anchorPoint: { + x: anchorPoint.dimensions.left, + y: anchorPoint.dimensions.top, + }, + size, + parentSize: { + height: anchorPoint.dimensions.height, + width: anchorPoint.dimensions.width, + }, + dimension: axisDim, + ticks: allTicks, + visibleTicks, + }); + }); + return axesGeometries; +} + +/** @internal */ +export const isDuplicateAxis = ( + { position, title }: AxisSpec, + { tickLabels }: AxisTicksDimensions, + tickMap: Map, + specs: AxisSpec[], +): boolean => { + const [firstTickLabel] = tickLabels; + const [lastTickLabel] = tickLabels.slice(-1); + + let hasDuplicate = false; + tickMap.forEach(({ tickLabels: axisTickLabels }, axisId) => { + if ( + !hasDuplicate && + axisTickLabels && + tickLabels.length === axisTickLabels.length && + firstTickLabel === axisTickLabels[0] && + lastTickLabel === axisTickLabels.slice(-1)[0] + ) { + const spec = getSpecsById(specs, axisId); + + if (spec && spec.position === position && title === spec.title) { + hasDuplicate = true; + } + } + }); + + return hasDuplicate; +}; diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/default_series_sort_fn.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/default_series_sort_fn.ts new file mode 100644 index 000000000000..795f1687aa60 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/default_series_sort_fn.ts @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DataSeries } from './series'; + +/** + * Return the default sorting used for XY series. + * Ordered by group insert order, then first stacked, after non stacked. + * @internal + */ +export function defaultXYSeriesSort(a: DataSeries, b: DataSeries) { + if (a.groupId !== b.groupId) { + return a.insertIndex - b.insertIndex; + } + + if (a.isStacked && !b.isStacked) { + return -1; // a first then b + } + if (!a.isStacked && b.isStacked) { + return 1; // b first then a + } + return a.insertIndex - b.insertIndex; +} + +/** + * Return the default sorting used for XY series. + * Ordered by group insert order, then first stacked, after non stacked. + * Stacked are sorted from from top to bottom to respect the rendering order + * @internal + */ +export function defaultXYLegendSeriesSort(a: DataSeries, b: DataSeries) { + if (a.groupId !== b.groupId) { + return a.insertIndex - b.insertIndex; + } + + if (a.isStacked && !b.isStacked) { + return -1; // a first then b + } + if (!a.isStacked && b.isStacked) { + return 1; // b first then a + } + if (a.isStacked && b.isStacked) { + return b.insertIndex - a.insertIndex; + } + return a.insertIndex - b.insertIndex; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/dimensions.test.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/dimensions.test.ts new file mode 100644 index 000000000000..d1bad05aeb3b --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/dimensions.test.ts @@ -0,0 +1,192 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartType } from '../..'; +import { SpecType } from '../../../specs/constants'; +import { Position } from '../../../utils/common'; +import { Margins } from '../../../utils/dimensions'; +import { AxisId } from '../../../utils/ids'; +import { LIGHT_THEME } from '../../../utils/themes/light_theme'; +import { LegendStyle } from '../../../utils/themes/theme'; +import { AxisTicksDimensions } from './axis_utils'; +import { computeChartDimensions } from './dimensions'; +import { AxisSpec } from './specs'; + +describe('Computed chart dimensions', () => { + const parentDim = { + width: 100, + height: 100, + top: 0, + left: 0, + }; + const chartMargins: Margins = { + left: 10, + right: 10, + top: 10, + bottom: 10, + }; + const chartPaddings: Margins = { + left: 10, + right: 10, + top: 10, + bottom: 10, + }; + + const axis1Dims: AxisTicksDimensions = { + tickValues: [0, 1], + tickLabels: ['first', 'second'], + maxLabelBboxWidth: 10, + maxLabelBboxHeight: 10, + maxLabelTextWidth: 10, + maxLabelTextHeight: 10, + isHidden: false, + }; + const axisLeftSpec: AxisSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Axis, + id: 'axis_1', + groupId: 'group_1', + hide: false, + showOverlappingTicks: false, + showOverlappingLabels: false, + position: Position.Left, + tickFormat: (value: any) => `${value}`, + }; + const legend: LegendStyle = { + verticalWidth: 10, + horizontalHeight: 10, + spacingBuffer: 10, + margin: 0, + }; + const defaultTheme = LIGHT_THEME; + const chartTheme = { + ...defaultTheme, + chartMargins, + chartPaddings, + axes: { + ...defaultTheme.axes, + }, + ...legend, + }; + chartTheme.axes.axisTitle.fontSize = 10; + chartTheme.axes.axisTitle.padding = 10; + test('should be equal to parent dimension with no axis minus margins', () => { + const axisDims = new Map(); + const axisStyles = new Map(); + const axisSpecs: AxisSpec[] = []; + const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisStyles, axisSpecs); + expect(chartDimensions.left + chartDimensions.width).toBeLessThanOrEqual(parentDim.width); + expect(chartDimensions.top + chartDimensions.height).toBeLessThanOrEqual(parentDim.height); + expect(chartDimensions).toMatchSnapshot(); + }); + test('should be padded by a left axis', () => { + // |margin|titleFontSize|titlePadding|maxLabelBboxWidth|tickPadding|tickSize|padding| + // \10|10|10|10|10|10|10| = 70px from left + const axisDims = new Map(); + const axisStyles = new Map(); + const axisSpecs = [axisLeftSpec]; + axisDims.set('axis_1', axis1Dims); + const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisStyles, axisSpecs); + expect(chartDimensions.left + chartDimensions.width).toBeLessThanOrEqual(parentDim.width); + expect(chartDimensions.top + chartDimensions.height).toBeLessThanOrEqual(parentDim.height); + expect(chartDimensions).toMatchSnapshot(); + }); + test('should be padded by a right axis', () => { + // |padding|tickSize|tickPadding|maxLabelBBoxWidth|titlePadding|titleFontSize\margin| + // \10|10|10|10|10|10|10| = 70px from right + const axisDims = new Map(); + const axisStyles = new Map(); + const axisSpecs = [{ ...axisLeftSpec, position: Position.Right }]; + axisDims.set('axis_1', axis1Dims); + const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisStyles, axisSpecs); + expect(chartDimensions.left + chartDimensions.width).toBeLessThanOrEqual(parentDim.width); + expect(chartDimensions.top + chartDimensions.height).toBeLessThanOrEqual(parentDim.height); + expect(chartDimensions).toMatchSnapshot(); + }); + test('should be padded by a top axis', () => { + // |margin|titleFontSize|titlePadding|maxLabelBboxHeight|tickPadding|tickSize|padding| + // \10|10|10|10|10|10|10| = 70px from top + const axisDims = new Map(); + const axisStyles = new Map(); + const axisSpecs = [ + { + ...axisLeftSpec, + position: Position.Top, + }, + ]; + axisDims.set('axis_1', axis1Dims); + const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisStyles, axisSpecs); + expect(chartDimensions.left + chartDimensions.width).toBeLessThanOrEqual(parentDim.width); + expect(chartDimensions.top + chartDimensions.height).toBeLessThanOrEqual(parentDim.height); + expect(chartDimensions).toMatchSnapshot(); + }); + test('should be padded by a bottom axis', () => { + // |margin|titleFontSize|titlePadding|maxLabelBboxHeight|tickPadding|tickSize|padding| + // \10|10|10|10|10|10|10| = 70px from bottom + const axisDims = new Map(); + const axisStyles = new Map(); + const axisSpecs = [ + { + ...axisLeftSpec, + position: Position.Bottom, + }, + ]; + axisDims.set('axis_1', axis1Dims); + const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisStyles, axisSpecs); + expect(chartDimensions.left + chartDimensions.width).toBeLessThanOrEqual(parentDim.width); + expect(chartDimensions.top + chartDimensions.height).toBeLessThanOrEqual(parentDim.height); + expect(chartDimensions).toMatchSnapshot(); + }); + test('should not add space for axis when no spec for axis dimensions or axis is hidden', () => { + const axisDims = new Map(); + const axisStyles = new Map(); + const axisSpecs = [ + { + ...axisLeftSpec, + position: Position.Bottom, + }, + ]; + axisDims.set('foo', axis1Dims); + const chartDimensions = computeChartDimensions(parentDim, chartTheme, axisDims, axisStyles, axisSpecs); + + const expectedDims = { + chartDimensions: { + height: 60, + width: 60, + left: 20, + top: 20, + }, + leftMargin: 10, + }; + + expect(chartDimensions).toEqual(expectedDims); + + const hiddenAxisDims = new Map(); + const hiddenAxisSpecs = new Map(); + hiddenAxisDims.set('axis_1', axis1Dims); + hiddenAxisSpecs.set('axis_1', { + ...axisLeftSpec, + hide: true, + position: Position.Bottom, + }); + const hiddenAxisChartDimensions = computeChartDimensions(parentDim, chartTheme, axisDims, axisStyles, axisSpecs); + + expect(hiddenAxisChartDimensions).toEqual(expectedDims); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/dimensions.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/dimensions.ts new file mode 100644 index 000000000000..7dc151de79b3 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/dimensions.ts @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SmallMultiplesSpec } from '../../../specs'; +import { Dimensions } from '../../../utils/dimensions'; +import { AxisId } from '../../../utils/ids'; +import { Theme, AxisStyle } from '../../../utils/themes/theme'; +import { computeAxesSizes } from '../axes/axes_sizes'; +import { AxisTicksDimensions } from './axis_utils'; +import { AxisSpec } from './specs'; + +/** + * @internal + */ +export interface ChartDimensions { + /** + * Dimensions relative to canvas element + */ + chartDimensions: Dimensions; + /** + * Margin to account for ending text overflow + */ + leftMargin: number; +} + +/** + * Compute the chart dimensions. It's computed removing from the parent dimensions + * the axis spaces, the legend and any other specified style margin and padding. + * @internal + */ +export function computeChartDimensions( + parentDimensions: Dimensions, + theme: Theme, + axisDimensions: Map, + axesStyles: Map, + axisSpecs: AxisSpec[], + smSpec?: SmallMultiplesSpec, +): ChartDimensions { + if (parentDimensions.width <= 0 || parentDimensions.height <= 0) { + return { + chartDimensions: { + width: 0, + height: 0, + left: 0, + top: 0, + }, + leftMargin: 0, + }; + } + + const axisSizes = computeAxesSizes(theme, axisDimensions, axesStyles, axisSpecs, smSpec); + const chartWidth = parentDimensions.width - axisSizes.left - axisSizes.right; + const chartHeight = parentDimensions.height - axisSizes.top - axisSizes.bottom; + const { chartPaddings } = theme; + const top = axisSizes.top + chartPaddings.top; + const left = axisSizes.left + chartPaddings.left; + + return { + leftMargin: axisSizes.margin.left, + chartDimensions: { + top, + left, + width: chartWidth - chartPaddings.left - chartPaddings.right, + height: chartHeight - chartPaddings.top - chartPaddings.bottom, + }, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/fill_series.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/fill_series.ts new file mode 100644 index 000000000000..847c3fbfb916 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/fill_series.ts @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ScaleType } from '../../../scales/constants'; +import { DataSeries } from './series'; +import { BasicSeriesSpec, isLineSeriesSpec, isAreaSeriesSpec } from './specs'; + +/** + * @internal + */ +export function fillSeries( + dataSeries: DataSeries[], + xValues: Set, + groupScaleType: ScaleType, +): DataSeries[] { + const sortedXValues = [...xValues.values()]; + return dataSeries.map((series) => { + const { spec, data, isStacked } = series; + + const noFillRequired = isXFillNotRequired(spec, groupScaleType, isStacked); + if (data.length === xValues.size || noFillRequired) { + return { + ...series, + data, + }; + } + const filledData: typeof data = []; + const missingValues = new Set(xValues); + for (let i = 0; i < data.length; i++) { + const { x } = data[i]; + filledData.push(data[i]); + missingValues.delete(x); + } + + const missingValuesArray = [...missingValues.values()]; + for (let i = 0; i < missingValuesArray.length; i++) { + const missingValue = missingValuesArray[i]; + const index = sortedXValues.indexOf(missingValue); + + filledData.splice(index, 0, { + x: missingValue, + y1: null, + y0: null, + initialY1: null, + initialY0: null, + mark: null, + datum: undefined, + filled: { + x: missingValue, + }, + }); + } + return { + ...series, + data: filledData, + }; + }); +} + +function isXFillNotRequired(spec: BasicSeriesSpec, groupScaleType: ScaleType, isStacked: boolean) { + const onlyNoFitAreaLine = (isAreaSeriesSpec(spec) || isLineSeriesSpec(spec)) && !spec.fit; + const onlyContinuous = groupScaleType === ScaleType.Linear || groupScaleType === ScaleType.Time; + return onlyNoFitAreaLine && onlyContinuous && !isStacked; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/fit_function.test.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/fit_function.test.ts new file mode 100644 index 000000000000..b21eb2699c02 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/fit_function.test.ts @@ -0,0 +1,1053 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + MockDataSeries, + getFilledNullData, + getFilledNonNullData, + getYResolvedData, + MockDataSeriesDatum, +} from '../../../mocks'; +import { ScaleType } from '../../../scales/constants'; +import * as testModule from './fit_function'; +import { DataSeries } from './series'; +import { Fit } from './specs'; +import * as seriesUtils from './stacked_series_utils'; + +describe('Fit Function', () => { + describe('getValue', () => { + describe('Non-Ordinal scale', () => { + it('should return current datum if next and previous are null with no endValue', () => { + const current = MockDataSeriesDatum.default(); + const actual = testModule.getValue(current, 0, null, null, Fit.Average); + + expect(actual).toBe(current); + expect(actual.filled).toBeUndefined(); + }); + + it('should return current datum with filled endValue if next and previous are null with endValue', () => { + const current = MockDataSeriesDatum.default(); + const actual = testModule.getValue(current, 0, null, null, Fit.Average, 100); + + expect(actual.filled?.y1).toBe(100); + }); + + describe('previous is not null and fit type is Carry', () => { + it('should return current datum with value from next when previous is null', () => { + const previous = MockDataSeriesDatum.full({ x: 0, y1: 20 }); + const current = MockDataSeriesDatum.simple({ x: 3 }); + const actual = testModule.getValue(current, 0, previous, null, Fit.Carry); + + expect(actual).toMatchObject({ + ...current, + y1: 20, + filled: { y1: 20 }, + }); + }); + }); + + describe('next is not null and fit type is Lookahead', () => { + it('should return current datum with value from next when previous is null', () => { + const current = MockDataSeriesDatum.simple({ x: 3 }); + const next = MockDataSeriesDatum.full({ x: 4, y1: 20 }); + const actual = testModule.getValue(current, 0, null, next, Fit.Lookahead); + + expect(actual).toMatchObject({ + ...current, + y1: 20, + filled: { y1: 20 }, + }); + }); + }); + + describe('current and previous datums are not null', () => { + describe('Average - fit type', () => { + it('should return current datum with average values from previous and next', () => { + const previous = MockDataSeriesDatum.full({ x: 0, y1: 10 }); + const current = MockDataSeriesDatum.simple({ x: 3 }); + const next = MockDataSeriesDatum.full({ x: 4, y1: 20 }); + const actual = testModule.getValue(current, 0, previous, next, Fit.Average); + + expect(actual).toMatchObject({ + ...current, + y1: (10 + 20) / 2, + filled: { y1: (10 + 20) / 2 }, + }); + }); + }); + + describe('Nearest - fit type', () => { + it('should return current datum with values from previous not next', () => { + const previous = MockDataSeriesDatum.full({ x: 0, y1: 10 }); + const current = MockDataSeriesDatum.simple({ x: 3 }); + const next = MockDataSeriesDatum.full({ x: 10, y1: 20 }); + const actual = testModule.getValue(current, 0, previous, next, Fit.Nearest); + + expect(actual).toMatchObject({ + ...current, + y1: 10, + filled: { y1: 10 }, + }); + }); + + it('should return current datum with values from next not previous', () => { + const previous = MockDataSeriesDatum.full({ x: 0, y1: 10 }); + const current = MockDataSeriesDatum.simple({ x: 9 }); + const next = MockDataSeriesDatum.full({ x: 10, y1: 20 }); + const actual = testModule.getValue(current, 0, previous, next, Fit.Nearest); + + expect(actual).toMatchObject({ + ...current, + y1: 20, + filled: { y1: 20 }, + }); + }); + }); + + describe('Linear - fit type', () => { + it('should return average from equidistant previous and next datums', () => { + const previous = MockDataSeriesDatum.full({ x: 0, y1: 10 }); + const current = MockDataSeriesDatum.simple({ x: 5 }); + const next = MockDataSeriesDatum.full({ x: 10, y1: 20 }); + const actual = testModule.getValue(current, 0, previous, next, Fit.Linear); + + expect(actual).toMatchObject({ + ...current, + y1: (10 + 20) / 2, + filled: { y1: (10 + 20) / 2 }, + }); + }); + + it('should return positive slope interpolated value from equidistant previous and next datums', () => { + const previous = MockDataSeriesDatum.full({ x: 0, y1: 10 }); + const current = MockDataSeriesDatum.simple({ x: 3 }); + const next = MockDataSeriesDatum.full({ x: 10, y1: 20 }); + const actual = testModule.getValue(current, 0, previous, next, Fit.Linear); + + expect(actual).toMatchObject({ + ...current, + y1: 13, + filled: { y1: 13 }, + }); + }); + + it('should return negative slope interpolated value from equidistant previous and next datums', () => { + const previous = MockDataSeriesDatum.full({ x: 0, y1: 20 }); + const current = MockDataSeriesDatum.simple({ x: 3 }); + const next = MockDataSeriesDatum.full({ x: 10, y1: 10 }); + const actual = testModule.getValue(current, 0, previous, next, Fit.Linear); + + expect(actual).toMatchObject({ + ...current, + y1: 17, + filled: { y1: 17 }, + }); + }); + + it('should return complex interpolated value from equidistant previous and next datums', () => { + const previous = MockDataSeriesDatum.full({ x: 0.767, y1: 10.545 }); + const current = MockDataSeriesDatum.simple({ x: 3.564 }); + const next = MockDataSeriesDatum.full({ x: 10.767, y1: 20.657 }); + const actual = testModule.getValue(current, 0, previous, next, Fit.Linear); + + expect(actual).toMatchObject({ + ...current, + y1: 13.3733264, + filled: { y1: 13.3733264 }, + }); + }); + }); + }); + + describe('next or previous datums are not null - with fits requring bounding datums', () => { + describe('Nearest - fit type', () => { + it('should return current datum with value from next when previous is null', () => { + const current = MockDataSeriesDatum.simple({ x: 3 }); + const next = MockDataSeriesDatum.full({ x: 4, y1: 20 }); + const actual = testModule.getValue(current, 0, null, next, Fit.Nearest); + + expect(actual).toMatchObject({ + ...current, + y1: 20, + filled: { y1: 20 }, + }); + }); + + it('should return current datum with value from previous when next is null', () => { + const previous = MockDataSeriesDatum.full({ x: 4, y1: 20 }); + const current = MockDataSeriesDatum.simple({ x: 3 }); + const actual = testModule.getValue(current, 0, previous, null, Fit.Nearest); + + expect(actual).toMatchObject({ + ...current, + y1: 20, + filled: { y1: 20 }, + }); + }); + }); + + describe("endValue is set to 'nearest'", () => { + it('should return current datum with value from next when previous is null', () => { + const current = MockDataSeriesDatum.simple({ x: 3 }); + const next = MockDataSeriesDatum.full({ x: 4, y1: 20 }); + const actual = testModule.getValue(current, 0, null, next, Fit.Average, 'nearest'); + + expect(actual).toMatchObject({ + ...current, + y1: 20, + filled: { y1: 20 }, + }); + }); + + it('should return current datum with value from previous when next is null', () => { + const previous = MockDataSeriesDatum.full({ x: 0, y1: 20 }); + const current = MockDataSeriesDatum.simple({ x: 3 }); + const actual = testModule.getValue(current, 0, previous, null, Fit.Average, 'nearest'); + + expect(actual).toMatchObject({ + ...current, + y1: 20, + filled: { y1: 20 }, + }); + }); + }); + }); + }); + describe('Ordinal scale', () => { + it('should return current datum if next and previous are null with no endValue', () => { + const current = MockDataSeriesDatum.ordinal(); + const actual = testModule.getValue(current, 0, null, null, Fit.Average); + + expect(actual).toBe(current); + expect(actual.filled).toBeUndefined(); + }); + + it('should return current datum with filled endValue if next and previous are null with endValue', () => { + const current = MockDataSeriesDatum.ordinal(); + const actual = testModule.getValue(current, 0, null, null, Fit.Average, 100); + + expect(actual.filled?.y1).toBe(100); + }); + + describe('previous is not null and fit type is Carry', () => { + it('should return current datum with value from next when previous is null', () => { + const previous = MockDataSeriesDatum.full({ x: 'a', y1: 20, fittingIndex: 0 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const actual = testModule.getValue(current, 3, previous, null, Fit.Carry); + + expect(actual).toMatchObject({ + ...current, + y1: 20, + filled: { y1: 20 }, + }); + }); + }); + + describe('next is not null and fit type is Lookahead', () => { + it('should return current datum with value from next when previous is null', () => { + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 4 }); + const actual = testModule.getValue(current, 3, null, next, Fit.Lookahead); + + expect(actual).toMatchObject({ + ...current, + y1: 20, + filled: { y1: 20 }, + }); + }); + }); + + describe('current and previous datums are not null', () => { + describe('Average - fit type', () => { + it('should return current datum with average values from previous and next', () => { + const previous = MockDataSeriesDatum.full({ x: 'a', y1: 10, fittingIndex: 0 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 4 }); + const actual = testModule.getValue(current, 3, previous, next, Fit.Average); + + expect(actual).toMatchObject({ + ...current, + y1: (10 + 20) / 2, + filled: { y1: (10 + 20) / 2 }, + }); + }); + }); + + describe('Nearest - fit type', () => { + it('should return current datum with values from previous not next', () => { + const previous = MockDataSeriesDatum.full({ x: 'a', y1: 10, fittingIndex: 0 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 10 }); + const actual = testModule.getValue(current, 3, previous, next, Fit.Nearest); + + expect(actual).toMatchObject({ + ...current, + y1: 10, + filled: { y1: 10 }, + }); + }); + + it('should return current datum with values from next not previous', () => { + const previous = MockDataSeriesDatum.full({ x: 'a', y1: 10, fittingIndex: 0 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 10 }); + const actual = testModule.getValue(current, 9, previous, next, Fit.Nearest); + + expect(actual).toMatchObject({ + ...current, + y1: 20, + filled: { y1: 20 }, + }); + }); + }); + + describe('Linear - fit type', () => { + it('should return average from equidistant previous and next datums', () => { + const previous = MockDataSeriesDatum.full({ x: 'a', y1: 10, fittingIndex: 0 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 10 }); + const actual = testModule.getValue(current, 5, previous, next, Fit.Linear); + + expect(actual).toMatchObject({ + ...current, + y1: (10 + 20) / 2, + filled: { y1: (10 + 20) / 2 }, + }); + }); + + it('should return positive slope interpolated value from equidistant previous and next datums', () => { + const previous = MockDataSeriesDatum.full({ x: 'a', y1: 10, fittingIndex: 0 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 10 }); + const actual = testModule.getValue(current, 3, previous, next, Fit.Linear); + + expect(actual).toMatchObject({ + ...current, + y1: 13, + filled: { y1: 13 }, + }); + }); + + it('should return negative slope interpolated value from equidistant previous and next datums', () => { + const previous = MockDataSeriesDatum.full({ x: 'a', y1: 20, fittingIndex: 0 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', y1: 10, fittingIndex: 10 }); + const actual = testModule.getValue(current, 3, previous, next, Fit.Linear); + + expect(actual).toMatchObject({ + ...current, + y1: 17, + filled: { y1: 17 }, + }); + }); + + it('should return complex interpolated value from equidistant previous and next datums', () => { + const previous = MockDataSeriesDatum.full({ x: 'a', fittingIndex: 0.767, y1: 10.545 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', fittingIndex: 10.767, y1: 20.657 }); + const actual = testModule.getValue(current, 3.564, previous, next, Fit.Linear); + + expect(actual).toMatchObject({ + ...current, + y1: 13.3733264, + filled: { y1: 13.3733264 }, + }); + }); + }); + }); + + describe('next or previous datums are not null - with fits requring bounding datums', () => { + describe('Nearest - fit type', () => { + it('should return current datum with value from next when previous is null', () => { + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 4 }); + const actual = testModule.getValue(current, 3, null, next, Fit.Nearest); + + expect(actual).toMatchObject({ + ...current, + y1: 20, + filled: { y1: 20 }, + }); + }); + + it('should return current datum with value from previous when next is null', () => { + const previous = MockDataSeriesDatum.full({ x: 4, y1: 20, fittingIndex: 0 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const actual = testModule.getValue(current, 3, previous, null, Fit.Nearest); + + expect(actual).toMatchObject({ + ...current, + y1: 20, + filled: { y1: 20 }, + }); + }); + }); + + describe("endValue is set to 'nearest'", () => { + it('should return current datum with value from next when previous is null', () => { + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 4 }); + const actual = testModule.getValue(current, 3, null, next, Fit.Average, 'nearest'); + + expect(actual).toMatchObject({ + ...current, + y1: 20, + filled: { y1: 20 }, + }); + }); + + it('should return current datum with value from previous when next is null', () => { + const previous = MockDataSeriesDatum.full({ x: 4, y1: 20, fittingIndex: 0 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const actual = testModule.getValue(current, 3, previous, null, Fit.Average, 'nearest'); + + expect(actual).toMatchObject({ + ...current, + y1: 20, + filled: { y1: 20 }, + }); + }); + }); + }); + }); + }); + + describe('parseConfig', () => { + it('should return default type when none exists', () => { + const actual = testModule.parseConfig(); + + expect(actual).toEqual({ + type: Fit.None, + }); + }); + + it('should parse string config', () => { + const actual = testModule.parseConfig(Fit.Average); + + expect(actual).toEqual({ + type: Fit.Average, + }); + }); + + it('should return default when Explicit is passes without value', () => { + const actual = testModule.parseConfig({ type: Fit.Explicit }); + + expect(actual).toEqual({ + type: Fit.None, + }); + }); + + it('should return type and value when Explicit is passes with value', () => { + const actual = testModule.parseConfig({ type: Fit.Explicit, value: 20 }); + + expect(actual).toEqual({ + type: Fit.Explicit, + value: 20, + endValue: undefined, + }); + }); + + it('should return type when no value or endValue is given', () => { + const actual = testModule.parseConfig({ type: Fit.Average }); + + expect(actual).toEqual({ + type: Fit.Average, + value: undefined, + endValue: undefined, + }); + }); + + it('should return type and endValue when endValue is passed', () => { + const actual = testModule.parseConfig({ type: Fit.Average, endValue: 20 }); + + expect(actual).toEqual({ + type: Fit.Average, + value: undefined, + endValue: 20, + }); + }); + }); + + describe('fitFunction', () => { + let dataSeries: DataSeries; + + beforeAll(() => { + jest.spyOn(testModule, 'parseConfig'); + jest.spyOn(testModule, 'getValue'); + dataSeries = MockDataSeries.fitFunction(); + }); + + describe('allow mutliple fit config types', () => { + it('should allow string config', () => { + testModule.fitFunction(dataSeries.data, Fit.None, ScaleType.Linear); + + expect(testModule.parseConfig).toHaveBeenCalledWith(Fit.None); + expect(testModule.parseConfig).toHaveBeenCalledTimes(1); + }); + + it('should allow object config', () => { + const fitConfig = { + type: Fit.None, + }; + testModule.fitFunction(dataSeries.data, fitConfig, ScaleType.Linear); + + expect(testModule.parseConfig).toHaveBeenCalledWith(fitConfig); + expect(testModule.parseConfig).toHaveBeenCalledTimes(1); + }); + }); + + describe('sorting', () => { + const spies: jest.SpyInstance[] = []; + const mockArray: any[] = []; + // @ts-ignore + jest.spyOn(mockArray, 'sort'); + + beforeAll(() => { + // @ts-ignore + spies.push(jest.spyOn(dataSeries.data, 'sort')); + // @ts-ignore + spies.push(jest.spyOn(dataSeries.data, 'slice').mockReturnValue(mockArray)); + }); + + afterAll(() => { + spies.forEach((s) => s.mockRestore()); + }); + + it('should call splice sort only', () => { + testModule.fitFunction(dataSeries.data, Fit.Linear, ScaleType.Linear); + + expect(dataSeries.data.sort).not.toHaveBeenCalled(); + expect(dataSeries.data.slice).toHaveBeenCalledTimes(1); + expect(mockArray.sort).toHaveBeenCalledTimes(1); + }); + + it('should not call splice.sort if sorted is true', () => { + testModule.fitFunction(dataSeries.data, Fit.Linear, ScaleType.Linear, true); + + expect(dataSeries.data.slice).not.toHaveBeenCalled(); + expect(mockArray.sort).not.toHaveBeenCalled(); + }); + + it('should not call splice.sort if scale is ordinal', () => { + testModule.fitFunction(dataSeries.data, Fit.Linear, ScaleType.Ordinal); + + expect(dataSeries.data.slice).not.toHaveBeenCalled(); + expect(mockArray.sort).not.toHaveBeenCalled(); + }); + + it('should call splice.sort with predicate', () => { + jest.spyOn(seriesUtils, 'datumXSortPredicate'); + testModule.fitFunction(dataSeries.data, Fit.Linear, ScaleType.Linear); + + expect(seriesUtils.datumXSortPredicate).toHaveBeenCalledWith(Fit.Linear); + }); + }); + + describe.each([ScaleType.Linear, ScaleType.Ordinal])('ScaleType - %s', (scaleType) => { + const ordinal = scaleType === ScaleType.Ordinal; + + beforeAll(() => { + dataSeries = MockDataSeries.fitFunction({ ordinal }); + }); + + describe('EndValues', () => { + const sortedDS = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + + describe('number value', () => { + const endValue = 100; + it('should set end values - None', () => { + const actual = testModule.fitFunction(sortedDS.data, { type: Fit.None, endValue }, scaleType); + const finalValues = getYResolvedData(actual); + + expect(finalValues[0]).toBeNull(); + expect(finalValues[finalValues.length - 1]).toBeNull(); + }); + + it('should set end values - Zero', () => { + const actual = testModule.fitFunction(sortedDS.data, { type: Fit.Zero, endValue }, scaleType); + const finalValues = getYResolvedData(actual); + + expect(finalValues[0]).toEqual(0); + expect(finalValues[finalValues.length - 1]).toEqual(0); + }); + + it('should set end values - Explicit', () => { + const actual = testModule.fitFunction( + sortedDS.data, + { type: Fit.Explicit, value: 20, endValue }, + scaleType, + ); + const finalValues = getYResolvedData(actual); + + expect(finalValues[0]).toEqual(20); + expect(finalValues[finalValues.length - 1]).toEqual(20); + }); + + it('should set end values - Lookahead', () => { + const actual = testModule.fitFunction(dataSeries.data, { type: Fit.Lookahead, endValue }, scaleType); + const finalValues = getYResolvedData(actual); + + expect(finalValues[0]).toEqual(3); + expect(finalValues[finalValues.length - 1]).toEqual(endValue); + }); + + it('should set end values - Nearest', () => { + const actual = testModule.fitFunction(dataSeries.data, { type: Fit.Nearest, endValue }, scaleType); + const finalValues = getYResolvedData(actual); + + expect(finalValues[0]).toEqual(3); + expect(finalValues[finalValues.length - 1]).toEqual(12); + }); + + it('should set end values - Average', () => { + const actual = testModule.fitFunction(dataSeries.data, { type: Fit.Average, endValue }, scaleType); + const finalValues = getYResolvedData(actual); + + expect(finalValues[0]).toEqual(endValue); + expect(finalValues[finalValues.length - 1]).toEqual(endValue); + }); + + it('should set end values - Linear', () => { + const actual = testModule.fitFunction(dataSeries.data, { type: Fit.Linear, endValue }, scaleType); + const finalValues = getYResolvedData(actual); + + expect(finalValues[0]).toEqual(endValue); + expect(finalValues[finalValues.length - 1]).toEqual(endValue); + }); + }); + + describe("'nearest' value", () => { + const endValue = 'nearest'; + + it('should set end values - None', () => { + const actual = testModule.fitFunction(sortedDS.data, { type: Fit.None, endValue }, scaleType); + const finalValues = getYResolvedData(actual); + + expect(finalValues[0]).toBeNull(); + expect(finalValues[finalValues.length - 1]).toBeNull(); + }); + + it('should set end values - Zero', () => { + const actual = testModule.fitFunction(sortedDS.data, { type: Fit.Zero, endValue }, scaleType); + const finalValues = getYResolvedData(actual); + + expect(finalValues[0]).toEqual(0); + expect(finalValues[finalValues.length - 1]).toEqual(0); + }); + + it('should set end values - Explicit', () => { + const actual = testModule.fitFunction( + sortedDS.data, + { type: Fit.Explicit, value: 20, endValue }, + scaleType, + ); + const finalValues = getYResolvedData(actual); + + expect(finalValues[0]).toEqual(20); + expect(finalValues[finalValues.length - 1]).toEqual(20); + }); + + it('should set end values - Lookahead', () => { + const actual = testModule.fitFunction(dataSeries.data, { type: Fit.Lookahead, endValue }, scaleType); + const finalValues = getYResolvedData(actual); + + expect(finalValues[0]).toEqual(3); + expect(finalValues[finalValues.length - 1]).toEqual(12); + }); + + it('should set end values - Nearest', () => { + const actual = testModule.fitFunction(dataSeries.data, { type: Fit.Nearest, endValue }, scaleType); + const finalValues = getYResolvedData(actual); + + expect(finalValues[0]).toEqual(3); + expect(finalValues[finalValues.length - 1]).toEqual(12); + }); + + it('should set end values - Average', () => { + const actual = testModule.fitFunction(dataSeries.data, { type: Fit.Average, endValue }, scaleType); + const finalValues = getYResolvedData(actual); + + expect(finalValues[0]).toEqual(3); + expect(finalValues[finalValues.length - 1]).toEqual(12); + }); + + it('should set end values - Linear', () => { + const actual = testModule.fitFunction(dataSeries.data, { type: Fit.Linear, endValue }, scaleType); + const finalValues = getYResolvedData(actual); + + expect(finalValues[0]).toEqual(3); + expect(finalValues[finalValues.length - 1]).toEqual(12); + }); + }); + }); + + describe('Fit Types', () => { + describe('None', () => { + it('should return original dataSeries', () => { + const actual = testModule.fitFunction(dataSeries.data, Fit.None, scaleType); + + expect(actual).toBe(dataSeries.data); + }); + + it('should return null data values without fit', () => { + const actual = testModule.fitFunction(dataSeries.data, Fit.None, scaleType); + + expect(getFilledNullData(actual)).toHaveLength(0); + }); + }); + + describe('Zero', () => { + it('should NOT return original dataSeries', () => { + const actual = testModule.fitFunction(dataSeries.data, Fit.Zero, scaleType); + + expect(actual).not.toBe(dataSeries.data); + }); + + it('should return null data values with zeros', () => { + const actual = testModule.fitFunction(dataSeries.data, Fit.Zero, scaleType); + const testActual = getFilledNullData(actual); + expect(testActual).toEqualArrayOf(0, 7); + }); + }); + + describe('Explicit', () => { + it('should return original dataSeries if no value provided', () => { + const actual = testModule.fitFunction(dataSeries.data, { type: Fit.Explicit }, scaleType); + + expect(actual).toBe(dataSeries.data); + }); + + it('should return null data values with set value', () => { + const actual = testModule.fitFunction(dataSeries.data, { type: Fit.Explicit, value: 20 }, scaleType); + const testActual = getFilledNullData(actual); + + expect(testActual).toEqualArrayOf(20, 7); + }); + }); + + describe('Lookahead', () => { + it('should not return original dataSeries', () => { + const actual = testModule.fitFunction(dataSeries.data, Fit.Lookahead, scaleType); + + expect(actual).not.toBe(dataSeries.data); + }); + + it('should not fill non-null values', () => { + const actual = testModule.fitFunction(dataSeries.data, Fit.Lookahead, scaleType); + expect(getFilledNonNullData(actual)).toEqualArrayOf(undefined, 6); + }); + + it('should call getValue for first datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + testModule.fitFunction(ds.data, Fit.Lookahead, scaleType); + const [current, next] = ds.data; + + expect(testModule.getValue).nthCalledWith( + 1, + expect.objectContaining(current), + 0, + null, + expect.objectContaining(next), + Fit.Lookahead, + undefined, + ); + }); + + it('should call getValue for 10th (4th null) datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + const actual = testModule.fitFunction(ds.data, Fit.Lookahead, scaleType); + const previous = actual[7]; + const current = ds.data[8]; + const next = ds.data[11]; + + expect(testModule.getValue).nthCalledWith( + 4, + expect.objectContaining(current), + 8, + expect.objectContaining(previous), + expect.objectContaining(next), + Fit.Lookahead, + undefined, + ); + }); + + it('should call getValue for last datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + testModule.fitFunction(ds.data, Fit.Lookahead, scaleType); + const [current, previous] = ds.data.slice().reverse(); + + expect(testModule.getValue).lastCalledWith( + expect.objectContaining(current), + 12, + expect.objectContaining(previous), + null, + Fit.Lookahead, + undefined, + ); + }); + + it('should call getValue for only null values', () => { + testModule.fitFunction(dataSeries.data, Fit.Lookahead, scaleType); + + const { length } = dataSeries.data.filter(({ y1 }) => y1 === null); + + expect(testModule.getValue).toBeCalledTimes(length); + }); + + it('should fill null values correctly', () => { + const actual = testModule.fitFunction(dataSeries.data, Fit.Lookahead, scaleType); + const finalValues = getYResolvedData(actual); + + expect(finalValues).toEqual([3, 3, 5, 4, 4, 5, 5, 6, 12, 12, 12, 12, null]); + }); + }); + describe('Nearest', () => { + it('should not return original dataSeries', () => { + const actual = testModule.fitFunction(dataSeries.data, Fit.Nearest, scaleType); + + expect(actual).not.toBe(dataSeries.data); + }); + + it('should not fill non-null values', () => { + const actual = testModule.fitFunction(dataSeries.data, Fit.Nearest, scaleType); + + expect(getFilledNonNullData(actual)).toEqualArrayOf(undefined, 6); + }); + + it('should call getValue for first datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + testModule.fitFunction(ds.data, Fit.Nearest, scaleType); + const [current, next] = ds.data; + + expect(testModule.getValue).nthCalledWith( + 1, + expect.objectContaining(current), + 0, + null, + expect.objectContaining(next), + Fit.Nearest, + undefined, + ); + }); + + it('should call getValue for 10th (4th null) datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + const actual = testModule.fitFunction(ds.data, Fit.Nearest, scaleType); + const previous = actual[7]; + const current = ds.data[8]; + const next = ds.data[11]; + + expect(testModule.getValue).nthCalledWith( + 4, + expect.objectContaining(current), + 8, + expect.objectContaining(previous), + expect.objectContaining(next), + Fit.Nearest, + undefined, + ); + }); + + it('should call getValue for last datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + testModule.fitFunction(ds.data, Fit.Nearest, scaleType); + const [current, previous] = ds.data.slice().reverse(); + + expect(testModule.getValue).lastCalledWith( + expect.objectContaining(current), + 12, + expect.objectContaining(previous), + null, + Fit.Nearest, + undefined, + ); + }); + + it('should call getValue for only null values', () => { + testModule.fitFunction(dataSeries.data, Fit.Nearest, scaleType); + const { length } = dataSeries.data.filter(({ y1 }) => y1 === null); + + expect(testModule.getValue).toBeCalledTimes(length); + }); + + it('should fill null values correctly', () => { + const actual = testModule.fitFunction(dataSeries.data, Fit.Nearest, scaleType); + const finalValues = getYResolvedData(actual); + + expect(finalValues).toEqual([3, 3, 5, 5, 4, 4, 5, 6, 6, 6, 12, 12, 12]); + }); + }); + + describe('Average', () => { + it('should not return original dataSeries', () => { + const actual = testModule.fitFunction(dataSeries.data, Fit.Average, scaleType); + + expect(actual).not.toBe(dataSeries.data); + }); + + it('should not fill non-null values', () => { + const actual = testModule.fitFunction(dataSeries.data, Fit.Average, scaleType); + + expect(getFilledNonNullData(actual)).toEqualArrayOf(undefined, 6); + }); + + it('should call getValue for first datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + testModule.fitFunction(ds.data, Fit.Average, scaleType); + const [current, next] = ds.data; + + expect(testModule.getValue).nthCalledWith( + 1, + expect.objectContaining(current), + 0, + null, + expect.objectContaining(next), + Fit.Average, + undefined, + ); + }); + + it('should call getValue for 10th (4th null) datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + const actual = testModule.fitFunction(ds.data, Fit.Average, scaleType); + const previous = actual[7]; + const current = ds.data[8]; + const next = ds.data[11]; + + expect(testModule.getValue).nthCalledWith( + 4, + expect.objectContaining(current), + 8, + expect.objectContaining(previous), + expect.objectContaining(next), + Fit.Average, + undefined, + ); + }); + + it('should call getValue for last datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + testModule.fitFunction(ds.data, Fit.Average, scaleType); + const [current, previous] = ds.data.slice().reverse(); + + expect(testModule.getValue).lastCalledWith( + expect.objectContaining(current), + 12, + expect.objectContaining(previous), + null, + Fit.Average, + undefined, + ); + }); + + it('should call getValue for only null values', () => { + testModule.fitFunction(dataSeries.data, Fit.Average, scaleType); + const { length } = dataSeries.data.filter(({ y1 }) => y1 === null); + + expect(testModule.getValue).toBeCalledTimes(length); + }); + + it('should fill null values correctly', () => { + const actual = testModule.fitFunction(dataSeries.data, Fit.Average, scaleType); + const finalValues = getYResolvedData(actual); + + expect(finalValues).toEqual([null, 3, 5, 4.5, 4, 4.5, 5, 6, 9, 9, 9, 12, null]); + }); + }); + + describe('Linear', () => { + it('should not return original dataSeries', () => { + const actual = testModule.fitFunction(dataSeries.data, Fit.Linear, scaleType); + + expect(actual).not.toBe(dataSeries.data); + }); + + it('should not fill non-null values', () => { + const actual = testModule.fitFunction(dataSeries.data, Fit.Linear, scaleType); + + expect(getFilledNonNullData(actual)).toEqualArrayOf(undefined, 6); + }); + + it('should call getValue for first datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + testModule.fitFunction(ds.data, Fit.Linear, scaleType); + const [current, next] = ds.data; + + expect(testModule.getValue).nthCalledWith( + 1, + expect.objectContaining(current), + 0, + null, + expect.objectContaining(next), + Fit.Linear, + undefined, + ); + }); + + it('should call getValue for 10th (4th null) datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + const actual = testModule.fitFunction(ds.data, Fit.Linear, scaleType); + const previous = actual[7]; + const current = ds.data[8]; + const next = ds.data[11]; + + expect(testModule.getValue).nthCalledWith( + 4, + expect.objectContaining(current), + 8, + expect.objectContaining(previous), + expect.objectContaining(next), + Fit.Linear, + undefined, + ); + }); + + it('should call getValue for last datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + testModule.fitFunction(ds.data, Fit.Linear, scaleType); + const [current, previous] = ds.data.slice().reverse(); + + expect(testModule.getValue).lastCalledWith( + expect.objectContaining(current), + 12, + expect.objectContaining(previous), + null, + Fit.Linear, + undefined, + ); + }); + + it('should call getValue for only null values', () => { + testModule.fitFunction(dataSeries.data, Fit.Linear, scaleType); + const { length } = dataSeries.data.filter(({ y1 }) => y1 === null); + + expect(testModule.getValue).toBeCalledTimes(length); + }); + + it('should fill null values correctly', () => { + const actual = testModule.fitFunction(dataSeries.data, Fit.Linear, scaleType); + const finalValues = getYResolvedData(actual); + + expect(finalValues).toEqual([null, 3, 5, 4.5, 4, 4.5, 5, 6, 7.5, 9, 10.5, 12, null]); + }); + }); + }); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/fit_function.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/fit_function.ts new file mode 100644 index 000000000000..d18f5ddaca7f --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/fit_function.ts @@ -0,0 +1,276 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DeepNonNullable } from 'utility-types'; + +import { ScaleType } from '../../../scales/constants'; +import { DataSeriesDatum } from './series'; +import { Fit, FitConfig } from './specs'; +import { datumXSortPredicate } from './stacked_series_utils'; + +/** + * Fit type that requires previous and/or next `non-nullable` values + * + */ +type BoundingFit = Exclude; + +/** + * `DataSeriesDatum` with non-`null` value for `x` and `y1` + * @internal + */ +export type FullDataSeriesDatum = Omit & + DeepNonNullable>; + +/** + * Embellishes `FullDataSeriesDatum` with `fittingIndex` for ordinal scales + * @internal + */ +export type WithIndex = T & { fittingIndex: number }; + +/** + * Returns `[x, y1]` values for a given datum with `fittingIndex` + * + */ +const getXYValues = ({ x, y1, fittingIndex }: WithIndex): [number, number] => [ + typeof x === 'string' ? fittingIndex : x, + y1, +]; + +/** @internal */ +export const getValue = ( + current: DataSeriesDatum, + currentIndex: number, + previous: WithIndex | null, + next: WithIndex | null, + type: BoundingFit, + endValue?: number | 'nearest', +): DataSeriesDatum => { + if (previous !== null && type === Fit.Carry) { + const { y1 } = previous; + return { + ...current, + y1, + filled: { + ...current.filled, + y1, + }, + }; + } + if (next !== null && type === Fit.Lookahead) { + const { y1 } = next; + return { + ...current, + y1, + filled: { + ...current.filled, + y1, + }, + }; + } + if (previous !== null && next !== null) { + if (type === Fit.Average) { + const y1 = (previous.y1 + next.y1) / 2; + return { + ...current, + y1, + filled: { + ...current.filled, + y1, + }, + }; + } + if (current.x !== null && previous.x !== null && next.x !== null) { + const [x1, y1] = getXYValues(previous); + const [x2, y2] = getXYValues(next); + const currentX = typeof current.x === 'string' ? currentIndex : current.x; + + if (type === Fit.Nearest) { + const x1Delta = Math.abs(currentX - x1); + const x2Delta = Math.abs(currentX - x2); + const y1Delta = x1Delta > x2Delta ? y2 : y1; + return { + ...current, + y1: y1Delta, + filled: { + ...current.filled, + y1: y1Delta, + }, + }; + } + if (type === Fit.Linear) { + // simple linear interpolation function + const linearInterpolatedY1 = previous.y1 + (currentX - x1) * ((y2 - y1) / (x2 - x1)); + return { + ...current, + y1: linearInterpolatedY1, + filled: { + ...current.filled, + y1: linearInterpolatedY1, + }, + }; + } + } + } else if ((previous !== null || next !== null) && (type === Fit.Nearest || endValue === 'nearest')) { + const nearestY1 = previous !== null ? previous.y1 : next!.y1; + return { + ...current, + y1: nearestY1, + filled: { + ...current.filled, + y1: nearestY1, + }, + }; + } + + if (endValue === undefined || typeof endValue === 'string') { + return current; + } + + // No matching fit - should only fall here on end conditions + return { + ...current, + y1: endValue, + filled: { + ...current.filled, + y1: endValue, + }, + }; +}; + +/** @internal */ +export const parseConfig = (config?: Exclude | FitConfig): FitConfig => { + if (!config) { + return { + type: Fit.None, + }; + } + + if (typeof config === 'string') { + return { + type: config, + }; + } + + if (config.type === Fit.Explicit && config.value === undefined) { + // Using explicit fit function requires a value + return { + type: Fit.None, + }; + } + + return { + type: config.type, + value: config.type === Fit.Explicit ? config.value : undefined, + endValue: config.endValue, + }; +}; + +/** @internal */ +export const fitFunction = ( + data: DataSeriesDatum[], + fitConfig: Exclude | FitConfig, + xScaleType: ScaleType, + sorted = false, +): DataSeriesDatum[] => { + const { type, value, endValue } = parseConfig(fitConfig); + + if (type === Fit.None) { + return data; + } + + if (type === Fit.Zero) { + return data.map((datum) => ({ + ...datum, + y1: datum.y1 === null ? 0 : datum.y1, + filled: { + ...datum.filled, + y1: datum.y1 === null ? 0 : undefined, + }, + })); + } + + if (type === Fit.Explicit) { + if (value === undefined) { + return data; + } + + return data.map((datum) => ({ + ...datum, + y1: datum.y1 === null ? value : datum.y1, + filled: { + ...datum.filled, + y1: datum.y1 === null ? value : undefined, + }, + })); + } + + const sortedData = + sorted || xScaleType === ScaleType.Ordinal ? data : data.slice().sort(datumXSortPredicate(xScaleType)); + const newData: DataSeriesDatum[] = []; + let previousNonNullDatum: WithIndex | null = null; + let nextNonNullDatum: WithIndex | null = null; + + for (let i = 0; i < sortedData.length; i++) { + let j = i; + const currentValue = sortedData[i]; + + if ( + currentValue.y1 === null && + nextNonNullDatum === null && + (type === Fit.Lookahead || + type === Fit.Nearest || + type === Fit.Average || + type === Fit.Linear || + endValue === 'nearest') + ) { + // Forward lookahead to get next non-null value + for (j = i + 1; j < sortedData.length; j++) { + const nextValue = sortedData[j]; + + if (nextValue.y1 !== null && nextValue.x !== null) { + nextNonNullDatum = { + ...(nextValue as FullDataSeriesDatum), + fittingIndex: j, + }; + break; + } + } + } + + const newValue = + currentValue.y1 === null + ? getValue(currentValue, i, previousNonNullDatum, nextNonNullDatum, type, endValue) + : currentValue; + + newData[i] = newValue; + + if (currentValue.y1 !== null && currentValue.x !== null) { + previousNonNullDatum = { + ...(currentValue as FullDataSeriesDatum), + fittingIndex: i, + }; + } + + if (nextNonNullDatum !== null && nextNonNullDatum.x <= currentValue.x) { + nextNonNullDatum = null; + } + } + + return newData; +}; diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/fit_function_utils.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/fit_function_utils.ts new file mode 100644 index 000000000000..5416f5f6d218 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/fit_function_utils.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ScaleType } from '../../../scales/constants'; +import { getSpecsById } from '../state/utils/spec'; +import { fitFunction } from './fit_function'; +import { DataSeries } from './series'; +import { isAreaSeriesSpec, isLineSeriesSpec, SeriesSpecs, BasicSeriesSpec } from './specs'; + +/** @internal */ +export const applyFitFunctionToDataSeries = ( + dataSeries: DataSeries[], + seriesSpecs: SeriesSpecs, + xScaleType: ScaleType, +): DataSeries[] => { + return dataSeries.map(({ specId, data, ...rest }) => { + const spec = getSpecsById(seriesSpecs, specId); + + if ( + spec !== null && + spec !== undefined && + (isAreaSeriesSpec(spec) || isLineSeriesSpec(spec)) && + spec.fit !== undefined + ) { + const fittedData = fitFunction(data, spec.fit, xScaleType); + + return { + specId, + ...rest, + data: fittedData, + }; + } + return { specId, data, ...rest }; + }); +}; diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/grid_lines.test.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/grid_lines.test.ts new file mode 100644 index 000000000000..520394fd850b --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/grid_lines.test.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getGridLineForHorizontalAxisAt, getGridLineForVerticalAxisAt } from './grid_lines'; + +describe('Grid lines', () => { + test('should compute positions for grid lines', () => { + const tickPosition = 25; + const panel = { + width: 100, + height: 100, + top: 0, + left: 0, + }; + const verticalAxisGridLines = getGridLineForVerticalAxisAt(tickPosition, panel); + expect(verticalAxisGridLines).toEqual({ x1: 0, y1: 25, x2: 100, y2: 25 }); + + const horizontalAxisGridLines = getGridLineForHorizontalAxisAt(tickPosition, panel); + expect(horizontalAxisGridLines).toEqual({ x1: 25, y1: 0, x2: 25, y2: 100 }); + }); + + test('should compute axis grid line positions', () => { + const panel = { + width: 100, + height: 200, + top: 0, + left: 0, + }; + const tickPosition = 10; + + const verticalAxisGridLinePositions = getGridLineForVerticalAxisAt(tickPosition, panel); + + expect(verticalAxisGridLinePositions).toEqual({ x1: 0, y1: 10, x2: 100, y2: 10 }); + + const horizontalAxisGridLinePositions = getGridLineForHorizontalAxisAt(tickPosition, panel); + + expect(horizontalAxisGridLinePositions).toEqual({ x1: 10, y1: 0, x2: 10, y2: 200 }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/grid_lines.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/grid_lines.ts new file mode 100644 index 000000000000..cc071d8c7576 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/grid_lines.ts @@ -0,0 +1,151 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { stringToRGB } from '../../../common/color_library_wrappers'; +import { Line, Stroke } from '../../../geoms/types'; +import { mergePartial, RecursivePartial } from '../../../utils/common'; +import { Size } from '../../../utils/dimensions'; +import { AxisId } from '../../../utils/ids'; +import { Point } from '../../../utils/point'; +import { AxisStyle } from '../../../utils/themes/theme'; +import { MIN_STROKE_WIDTH } from '../renderer/canvas/primitives/line'; +import { SmallMultipleScales } from '../state/selectors/compute_small_multiple_scales'; +import { isVerticalAxis } from './axis_type_utils'; +import { AxisGeometry, AxisTick } from './axis_utils'; +import { getPanelSize } from './panel'; +import { getPerPanelMap } from './panel_utils'; +import { AxisSpec } from './specs'; + +/** @internal */ +export interface GridLineGroup { + lines: Array; + stroke: Stroke; + axisId: AxisId; +} + +/** @internal */ +export type LinesGrid = { + panelAnchor: Point; + lineGroups: Array; +}; + +/** @internal */ +export function getGridLines( + axesSpecs: Array, + axesGeoms: Array, + themeAxisStyle: AxisStyle, + scales: SmallMultipleScales, +): Array { + const panelSize = getPanelSize(scales); + return getPerPanelMap(scales, () => { + // get grids per panel (depends on all the axis that exist) + const lines = axesGeoms.reduce>((linesAcc, { axis, visibleTicks }) => { + const axisSpec = axesSpecs.find(({ id }) => id === axis.id); + if (!axisSpec) { + return linesAcc; + } + const linesForSpec = getGridLinesForSpec(axisSpec, visibleTicks, themeAxisStyle, panelSize); + if (!linesForSpec) { + return linesAcc; + } + return [...linesAcc, linesForSpec]; + }, []); + return { lineGroups: lines }; + }); +} + +/** + * Get grid lines for a specific axis + * @internal + * @param axisSpec + * @param visibleTicks + * @param themeAxisStyle + * @param panelSize + */ +export function getGridLinesForSpec( + axisSpec: AxisSpec, + visibleTicks: AxisTick[], + themeAxisStyle: AxisStyle, + panelSize: Size, +): GridLineGroup | null { + // vertical ==> horizontal grid lines + const isVertical = isVerticalAxis(axisSpec.position); + + // merge the axis configured style with the theme style + const axisStyle = mergePartial(themeAxisStyle, axisSpec.style as RecursivePartial, { + mergeOptionalPartialValues: true, + }); + const gridLineThemeStyle = isVertical ? axisStyle.gridLine.vertical : axisStyle.gridLine.horizontal; + + // axis can have a configured grid line style + const gridLineStyles = axisSpec.gridLine ? mergePartial(gridLineThemeStyle, axisSpec.gridLine) : gridLineThemeStyle; + + const showGridLines = axisSpec.showGridLines ?? gridLineStyles.visible; + if (!showGridLines) { + return null; + } + + // compute all the lines points for the specific grid + const lines = visibleTicks.map((tick: AxisTick) => { + return isVertical + ? getGridLineForVerticalAxisAt(tick.position, panelSize) + : getGridLineForHorizontalAxisAt(tick.position, panelSize); + }); + + // define the stroke for the specific set of grid lines + if (!gridLineStyles.stroke || !gridLineStyles.strokeWidth || gridLineStyles.strokeWidth < MIN_STROKE_WIDTH) { + return null; + } + const strokeColor = stringToRGB(gridLineStyles.stroke); + strokeColor.opacity = + gridLineStyles.opacity !== undefined ? strokeColor.opacity * gridLineStyles.opacity : strokeColor.opacity; + const stroke: Stroke = { + color: strokeColor, + width: gridLineStyles.strokeWidth, + dash: gridLineStyles.dash, + }; + + return { + lines, + stroke, + axisId: axisSpec.id, + }; +} + +/** + * Get a horizontal grid line at `tickPosition` + * used for vertical axis specs + * @param tickPosition the position of the tick + * @param panelSize the size of the target panel + * @internal + */ +export function getGridLineForVerticalAxisAt(tickPosition: number, panelSize: Size): Line { + return { x1: 0, y1: tickPosition, x2: panelSize.width, y2: tickPosition }; +} + +/** + * Get a vertical grid line at `tickPosition` + * used for horizontal axis specs + * @param tickPosition the position of the tick + * @param panelSize the size of the target panel + * @internal + */ +export function getGridLineForHorizontalAxisAt(tickPosition: number, panelSize: Size): Line { + return { x1: tickPosition, y1: 0, x2: tickPosition, y2: panelSize.height }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/group_data_series.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/group_data_series.ts new file mode 100644 index 000000000000..16ddbecfab6b --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/group_data_series.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +type Group = Record; +type GroupByKeyFn = (data: T) => string; +type GroupKeysOrKeyFn = Array | GroupByKeyFn; + +/** @internal */ +export function groupBy(data: T[], keysOrKeyFn: GroupKeysOrKeyFn, asArray: false): Group; +/** @internal */ +export function groupBy(data: T[], keysOrKeyFn: GroupKeysOrKeyFn, asArray: true): T[][]; +/** @internal */ +export function groupBy(data: T[], keysOrKeyFn: GroupKeysOrKeyFn, asArray: boolean): T[][] | Group { + const keyFn = Array.isArray(keysOrKeyFn) ? getUniqueKey(keysOrKeyFn) : keysOrKeyFn; + const grouped = data.reduce>((acc, curr) => { + const key = keyFn(curr); + if (!acc[key]) { + acc[key] = []; + } + acc[key].push(curr); + return acc; + }, {}); + return asArray ? Object.values(grouped) : grouped; +} + +/** @internal */ +export function getUniqueKey(keys: Array, concat = '|') { + return (data: T): string => { + return keys + .map((key) => { + return data[key]; + }) + .join(concat); + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/indexed_geometry_linear_map.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/indexed_geometry_linear_map.ts new file mode 100644 index 000000000000..4d19b8ccd42a --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/indexed_geometry_linear_map.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IndexedGeometry } from '../../../utils/geometry'; + +/** @internal */ +export class IndexedGeometryLinearMap { + private map = new Map(); + + get size() { + return this.map.size; + } + + set(geometry: IndexedGeometry) { + const { x } = geometry.value; + const existing = this.map.get(x); + if (existing === undefined) { + this.map.set(x, [geometry]); + } else { + this.map.set(x, [geometry, ...existing]); + } + } + + getMergeData() { + return [...this.map.values()]; + } + + keys(): Array { + return [...this.map.keys()]; + } + + find(x: number | string | null): IndexedGeometry[] { + if (x === null) { + return []; + } + + return this.map.get(x) ?? []; + } +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/indexed_geometry_map.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/indexed_geometry_map.ts new file mode 100644 index 000000000000..449092267fa4 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/indexed_geometry_map.ts @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $Values } from 'utility-types'; + +import { isNil } from '../../../utils/common'; +import { Bounds } from '../../../utils/d3-delaunay'; +import { IndexedGeometry, isPointGeometry } from '../../../utils/geometry'; +import { Point } from '../../../utils/point'; +import { PrimitiveValue } from '../../partition_chart/layout/utils/group_by_rollup'; +import { IndexedGeometryLinearMap } from './indexed_geometry_linear_map'; +import { IndexedGeometrySpatialMap } from './indexed_geometry_spatial_map'; + +/** @internal */ +export const GeometryType = Object.freeze({ + linear: 'linear' as const, + spatial: 'spatial' as const, +}); +/** @internal */ +export type GeometryType = $Values; + +/** @internal */ +export class IndexedGeometryMap { + private linearMap = new IndexedGeometryLinearMap(); + + private spatialMap = new IndexedGeometrySpatialMap(); + + /** + * Returns triangulation instance to render spatial grid + * + * @param bounds + */ + triangulation(bounds?: Bounds) { + return this.spatialMap.triangulation(bounds); + } + + keys(): Array { + return [...this.linearMap.keys(), ...this.spatialMap.keys()]; + } + + get size(): number { + return this.linearMap.size + this.spatialMap.size; + } + + set(geometry: IndexedGeometry, type: GeometryType = GeometryType.linear) { + if (type === GeometryType.spatial && isPointGeometry(geometry)) { + // TODO: Add dev error here when attempting spatial upset with non-point + this.spatialMap.set([geometry]); + } else { + this.linearMap.set(geometry); + } + } + + find( + x: number | string | null, + point?: Point, + smHorizontalValue?: PrimitiveValue, + smVerticalValue?: PrimitiveValue, + ): IndexedGeometry[] { + if (x === null && !point) { + return []; + } + + const spatialValues = point === undefined ? [] : this.spatialMap.find(point); + return [...this.linearMap.find(x), ...spatialValues].filter( + ({ seriesIdentifier: { smHorizontalAccessorValue, smVerticalAccessorValue } }) => + (isNil(smVerticalValue) || smVerticalAccessorValue === smVerticalValue) && + (isNil(smHorizontalValue) || smHorizontalAccessorValue === smHorizontalValue), + ); + } + + getMergeData() { + return { + spatialGeometries: this.spatialMap.getMergeData(), + linearGeometries: this.linearMap.getMergeData(), + }; + } + + /** + * Merge multiple indexedMaps into base indexedMaps + * @param indexedMaps + */ + merge(...indexedMaps: IndexedGeometryMap[]) { + // eslint-disable-next-line no-restricted-syntax + for (const indexedMap of indexedMaps) { + const { spatialGeometries, linearGeometries } = indexedMap.getMergeData(); + this.spatialMap.set(spatialGeometries); + linearGeometries.forEach((geometry) => { + if (Array.isArray(geometry)) { + geometry.forEach((geometry) => this.linearMap.set(geometry)); + } else { + this.linearMap.set(geometry); + } + }); + } + } +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/indexed_geometry_spatial_map.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/indexed_geometry_spatial_map.ts new file mode 100644 index 000000000000..242cf44ad047 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/indexed_geometry_spatial_map.ts @@ -0,0 +1,135 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getDistance } from '../../../utils/common'; +import { Delaunay, Bounds } from '../../../utils/d3-delaunay'; +import { IndexedGeometry, PointGeometry } from '../../../utils/geometry'; +import { Point } from '../../../utils/point'; +import { DEFAULT_HIGHLIGHT_PADDING } from '../rendering/constants'; + +/** @internal */ +export type IndexedGeometrySpatialMapPoint = [number, number]; + +/** @internal */ +export class IndexedGeometrySpatialMap { + private map: Delaunay | null = null; + + private points: IndexedGeometrySpatialMapPoint[] = []; + + private pointGeometries: PointGeometry[] = []; + + private searchStartIndex: number = 0; + + private maxRadius = -Infinity; + + constructor(points: PointGeometry[] = []) { + this.set(points); + } + + get size() { + return this.points.length; + } + + isSpatial() { + return this.pointGeometries.length > 0; + } + + set(points: PointGeometry[]) { + this.maxRadius = Math.max(this.maxRadius, ...points.map(({ radius }) => radius)); + this.pointGeometries.push(...points); + this.points.push( + ...points.map(({ x, y }) => { + // TODO: handle coincident points better + // This nonce is used to slightly offset every point such that each point + // has a unique poition in the index. This number is only used in the index. + // The other option would be to find the point(s) near a Point and add logic + // to account for multiple values in the pointGeometries array. This would be + // a very comutationally expensive approach having to repeat for every point. + const nonce = Math.random() * 0.000001; + return [x + nonce, y]; + }), + ); + + if (this.points.length > 0) { + // TODO: handle write/read init + this.map = Delaunay.from(this.points); + } + } + + triangulation = (bounds?: Bounds) => this.map?.voronoi(bounds); + + getMergeData() { + return [...this.pointGeometries]; + } + + keys(): Array { + return this.pointGeometries.map(({ value: { x } }) => x); + } + + find(point: Point): IndexedGeometry[] { + const elements = []; + if (this.map !== null) { + const index = this.map.find(point.x, point.y, this.searchStartIndex); + const geometry = this.pointGeometries[index]; + + if (geometry) { + // Set next starting search index for faster lookup + this.searchStartIndex = index; + elements.push(geometry); + elements.push(...this.getRadialNeighbors(index, point, new Set([index]))); + } + } + + return elements; + } + + /** + * Gets surrounding points whose radius could be within the active cursor position + * + * @param selectedIndex + * @param point + * @param visitedIndices + */ + private getRadialNeighbors(selectedIndex: number, point: Point, visitedIndices: Set): IndexedGeometry[] { + if (this.map === null) { + return []; + } + + const neighbors = [...this.map.neighbors(selectedIndex)]; + return neighbors.reduce((acc, i) => { + if (visitedIndices.has(i)) { + return acc; + } + + visitedIndices.add(i); + const geometry = this.pointGeometries[i]; + + if (geometry) { + acc.push(geometry); + + if (getDistance(geometry, point) < Math.min(this.maxRadius, DEFAULT_HIGHLIGHT_PADDING)) { + // Gets neighbors based on relation to maxRadius + acc.push(...this.getRadialNeighbors(i, point, visitedIndices)); + } + } + + return acc; + }, []); + } +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/interactions.test.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/interactions.test.ts new file mode 100644 index 000000000000..8baf77920f2b --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/interactions.test.ts @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { isCrosshairTooltipType, isFollowTooltipType } from '../../../specs'; +import { TooltipType } from '../../../specs/constants'; +import { Dimensions } from '../../../utils/dimensions'; +import { getOrientedXPosition, getOrientedYPosition } from './interactions'; + +describe('Interaction utils', () => { + const chartDimensions: Dimensions = { + width: 200, + height: 100, + left: 10, + top: 10, + }; + + test('limit x position with x already relative to chart', () => { + const xPos = 30; + const yPos = 50; + let validPosition = getOrientedXPosition(xPos, yPos, 0, chartDimensions); + expect(validPosition).toBe(xPos); + validPosition = getOrientedXPosition(xPos, yPos, 180, chartDimensions); + expect(validPosition).toBe(chartDimensions.width - xPos); + validPosition = getOrientedXPosition(xPos, yPos, 90, chartDimensions); + expect(validPosition).toBe(yPos); + validPosition = getOrientedXPosition(xPos, yPos, -90, chartDimensions); + expect(validPosition).toBe(chartDimensions.height - yPos); + }); + test('limit y position with x already relative to chart', () => { + const yPos = 30; + const xPos = 50; + let validPosition = getOrientedYPosition(xPos, yPos, 0, chartDimensions); + expect(validPosition).toBe(yPos); + validPosition = getOrientedYPosition(xPos, yPos, 180, chartDimensions); + expect(validPosition).toBe(chartDimensions.height - yPos); + validPosition = getOrientedYPosition(xPos, yPos, 90, chartDimensions); + expect(validPosition).toBe(chartDimensions.width - xPos); + validPosition = getOrientedYPosition(xPos, yPos, -90, chartDimensions); + expect(validPosition).toBe(xPos); + }); + test('checks tooltip type helpers', () => { + expect(isCrosshairTooltipType(TooltipType.Crosshairs)).toBe(true); + expect(isCrosshairTooltipType(TooltipType.VerticalCursor)).toBe(true); + expect(isCrosshairTooltipType(TooltipType.Follow)).toBe(false); + expect(isCrosshairTooltipType(TooltipType.None)).toBe(false); + + expect(isFollowTooltipType(TooltipType.Crosshairs)).toBe(false); + expect(isFollowTooltipType(TooltipType.VerticalCursor)).toBe(false); + expect(isFollowTooltipType(TooltipType.Follow)).toBe(true); + expect(isFollowTooltipType(TooltipType.None)).toBe(false); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/interactions.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/interactions.ts new file mode 100644 index 000000000000..06ed2fc76eb2 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/interactions.ts @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Rotation } from '../../../utils/common'; +import { Size } from '../../../utils/dimensions'; + +/** + * Get the cursor position depending on the chart rotation + * @param xPos x position relative to chart + * @param yPos y position relative to chart + * @param chartRotation the chart rotation + * @param chartDimension the chart dimension + * @internal + */ +export function getOrientedXPosition(xPos: number, yPos: number, chartRotation: Rotation, chartDimension: Size) { + switch (chartRotation) { + case 180: + return chartDimension.width - xPos; + case 90: + return yPos; + case -90: + return chartDimension.height - yPos; + case 0: + default: + return xPos; + } +} + +/** @internal */ +export function getOrientedYPosition(xPos: number, yPos: number, chartRotation: Rotation, chartDimension: Size) { + switch (chartRotation) { + case 180: + return chartDimension.height - yPos; + case -90: + return xPos; + case 90: + return chartDimension.width - xPos; + case 0: + default: + return yPos; + } +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts new file mode 100644 index 000000000000..281f74cd8b53 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts @@ -0,0 +1,312 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MockDataSeries } from '../../../mocks'; +import { MockSeriesSpecs, MockSeriesSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store'; +import { ScaleType } from '../../../scales/constants'; +import { computeSeriesDomainsSelector } from '../state/selectors/compute_series_domains'; +import * as fitFunctionModule from './fit_function'; +import * as testModule from './fit_function_utils'; +import { Fit } from './specs'; + +const EMPTY_DATA_SET = MockSeriesSpec.area({ + xScaleType: ScaleType.Linear, + yAccessors: ['y1'], + splitSeriesAccessors: ['g'], + data: [], +}); +const STANDARD_DATA_SET = MockSeriesSpec.area({ + xScaleType: ScaleType.Linear, + yAccessors: ['y1'], + splitSeriesAccessors: ['g'], + data: [ + { x: 0, y1: 10, g: 'a' }, + { x: 0, y1: 20, g: 'b' }, + { x: 0, y1: 30, g: 'c' }, + ], +}); +const WITH_NULL_DATASET = MockSeriesSpec.area({ + xScaleType: ScaleType.Linear, + yAccessors: ['y1'], + splitSeriesAccessors: ['g'], + data: [ + { x: 0, y1: 10, g: 'a' }, + { x: 0, y1: null, g: 'b' }, + { x: 0, y1: 30, g: 'c' }, + ], +}); +const STANDARD_DATA_SET_WY0 = MockSeriesSpec.area({ + xScaleType: ScaleType.Linear, + yAccessors: ['y1'], + y0Accessors: ['y0'], + splitSeriesAccessors: ['g'], + data: [ + { x: 0, y0: 2, y1: 10, g: 'a' }, + { x: 0, y0: 4, y1: 20, g: 'b' }, + { x: 0, y0: 6, y1: 30, g: 'c' }, + ], +}); +const WITH_NULL_DATASET_WY0 = MockSeriesSpec.area({ + xScaleType: ScaleType.Linear, + yAccessors: ['y1'], + y0Accessors: ['y0'], + splitSeriesAccessors: ['g'], + data: [ + { x: 0, y0: 2, y1: 10, g: 'a' }, + { x: 0, y1: null, g: 'b' }, + { x: 0, y0: 6, y1: 30, g: 'c' }, + ], +}); +const DATA_SET_WITH_NULL_2 = MockSeriesSpec.area({ + xScaleType: ScaleType.Linear, + yAccessors: ['y1'], + splitSeriesAccessors: ['g'], + data: [ + { x: 1, y1: 1, g: 'a' }, + { x: 2, y1: 2, g: 'a' }, + { x: 4, y1: 4, g: 'a' }, + { x: 1, y1: 21, g: 'b' }, + { x: 3, y1: 23, g: 'b' }, + ], +}); +describe('Non-Stacked Series Utils', () => { + describe('Format stacked dataset', () => { + test('empty data', () => { + const store = MockStore.default(); + MockStore.addSpecs(EMPTY_DATA_SET, store); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + expect(formattedDataSeries).toHaveLength(0); + }); + test('format data without nulls', () => { + const store = MockStore.default(); + MockStore.addSpecs(STANDARD_DATA_SET, store); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + expect(formattedDataSeries[0].data[0]).toMatchObject({ + initialY0: null, + initialY1: 10, + x: 0, + y0: null, + y1: 10, + mark: null, + }); + expect(formattedDataSeries[1].data[0]).toMatchObject({ + initialY0: null, + initialY1: 20, + x: 0, + y0: null, + y1: 20, + mark: null, + }); + expect(formattedDataSeries[2].data[0]).toMatchObject({ + initialY0: null, + initialY1: 30, + x: 0, + y0: null, + y1: 30, + mark: null, + }); + }); + test('format data with nulls', () => { + const store = MockStore.default(); + MockStore.addSpecs(WITH_NULL_DATASET, store); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + expect(formattedDataSeries[1].data[0]).toMatchObject({ + initialY0: null, + initialY1: null, + x: 0, + y1: null, + y0: null, + mark: null, + }); + }); + test('format data without nulls with y0 values', () => { + const store = MockStore.default(); + MockStore.addSpecs(STANDARD_DATA_SET_WY0, store); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + expect(formattedDataSeries[0].data[0]).toMatchObject({ + initialY0: 2, + initialY1: 10, + x: 0, + y0: 2, + y1: 10, + mark: null, + }); + expect(formattedDataSeries[1].data[0]).toMatchObject({ + initialY0: 4, + initialY1: 20, + x: 0, + y0: 4, + y1: 20, + mark: null, + }); + expect(formattedDataSeries[2].data[0]).toMatchObject({ + initialY0: 6, + initialY1: 30, + x: 0, + y0: 6, + y1: 30, + mark: null, + }); + }); + test('format data with nulls - fit functions', () => { + const store = MockStore.default(); + MockStore.addSpecs(WITH_NULL_DATASET_WY0, store); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + expect(formattedDataSeries[0].data[0]).toMatchObject({ + initialY0: 2, + initialY1: 10, + x: 0, + y0: 2, + y1: 10, + mark: null, + }); + expect(formattedDataSeries[1].data[0]).toMatchObject({ + initialY0: null, + initialY1: null, + x: 0, + y1: null, + y0: null, + mark: null, + }); + expect(formattedDataSeries[2].data[0]).toMatchObject({ + initialY0: 6, + initialY1: 30, + x: 0, + y0: 6, + y1: 30, + mark: null, + }); + }); + test('format data without nulls on second series', () => { + const store = MockStore.default(); + MockStore.addSpecs(DATA_SET_WITH_NULL_2, store); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + expect(formattedDataSeries.length).toBe(2); + // this because linear non stacked area/lines doesn't fill up the dataset + // with missing x data points + expect(formattedDataSeries[0].data.length).toBe(3); + expect(formattedDataSeries[1].data.length).toBe(2); + + expect(formattedDataSeries[0].data[0]).toMatchObject({ + initialY0: null, + initialY1: 1, + x: 1, + y0: null, // todo check if we can move that to 0 + y1: 1, + mark: null, + }); + expect(formattedDataSeries[0].data[1]).toMatchObject({ + initialY0: null, + initialY1: 2, + x: 2, + y0: null, + y1: 2, + mark: null, + }); + expect(formattedDataSeries[0].data[2]).toMatchObject({ + initialY0: null, + initialY1: 4, + x: 4, + y0: null, + y1: 4, + mark: null, + }); + expect(formattedDataSeries[1].data[0]).toMatchObject({ + initialY0: null, + initialY1: 21, + x: 1, + y0: null, + y1: 21, + mark: null, + }); + expect(formattedDataSeries[1].data[1]).toMatchObject({ + initialY0: null, + initialY1: 23, + x: 3, + y0: null, + y1: 23, + mark: null, + }); + }); + }); + + describe('Using fit functions', () => { + describe.each(['area', 'line'])('Spec type - %s', (specType) => { + const dataSeries = [MockDataSeries.fitFunction({ shuffle: false })]; + const dataSeriesData = MockDataSeries.fitFunction({ shuffle: false }).data; + const spec = + specType === 'area' ? MockSeriesSpec.area({ fit: Fit.Linear }) : MockSeriesSpec.line({ fit: Fit.Linear }); + const seriesSpecs = MockSeriesSpecs.fromSpecs([spec]); + + beforeAll(() => { + jest.spyOn(fitFunctionModule, 'fitFunction').mockReturnValue(dataSeriesData); + }); + + it('return call fitFunction with args', () => { + testModule.applyFitFunctionToDataSeries(dataSeries, seriesSpecs, ScaleType.Linear); + + expect(fitFunctionModule.fitFunction).toHaveBeenCalledWith(dataSeriesData, Fit.Linear, ScaleType.Linear); + }); + + it('return not call fitFunction if no fit specified', () => { + const currentSpec = + specType === 'area' ? MockSeriesSpec.area({ fit: undefined }) : MockSeriesSpec.line({ fit: undefined }); + const noFitSpec = MockSeriesSpecs.fromSpecs([currentSpec]); + testModule.applyFitFunctionToDataSeries(dataSeries, noFitSpec, ScaleType.Linear); + + expect(fitFunctionModule.fitFunction).not.toHaveBeenCalled(); + }); + + it('return fitted dataSeries', () => { + const actual = testModule.applyFitFunctionToDataSeries(dataSeries, seriesSpecs, ScaleType.Linear); + + expect(actual[0].data).toBe(dataSeriesData); + }); + }); + + describe('Non area and line specs', () => { + const dataSeries = [MockDataSeries.fitFunction({ shuffle: false })]; + const dataSeriesData = MockDataSeries.fitFunction({ shuffle: false }).data; + const spec = MockSeriesSpec.bar(); + const seriesSpecs = MockSeriesSpecs.fromSpecs([spec]); + + beforeAll(() => { + jest.spyOn(fitFunctionModule, 'fitFunction').mockReturnValue(dataSeriesData); + }); + + it('return call fitFunction with args', () => { + testModule.applyFitFunctionToDataSeries(dataSeries, seriesSpecs, ScaleType.Linear); + + expect(fitFunctionModule.fitFunction).not.toHaveBeenCalled(); + }); + + it('return fitted dataSeries', () => { + const actual = testModule.applyFitFunctionToDataSeries(dataSeries, seriesSpecs, ScaleType.Linear); + + expect(actual[0].data).toBe(dataSeriesData); + }); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/panel.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/panel.ts new file mode 100644 index 000000000000..9fc627e3b86a --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/panel.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Size } from '../../../utils/dimensions'; +import { SmallMultipleScales } from '../state/selectors/compute_small_multiple_scales'; + +/** @internal */ +export function getPanelSize({ horizontal, vertical }: SmallMultipleScales): Size { + return { width: horizontal.bandwidth, height: vertical.bandwidth }; +} + +/** @internal */ +export const hasSMDomain = ({ domain }: SmallMultipleScales['horizontal'] | SmallMultipleScales['vertical']) => + domain.length > 0 && domain[0] !== undefined; diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/panel_utils.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/panel_utils.ts new file mode 100644 index 000000000000..c83d444bc309 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/panel_utils.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Point } from '../../../utils/point'; +import { SmallMultipleScales } from '../state/selectors/compute_small_multiple_scales'; + +/** @internal */ +export interface PerPanelMap { + panelAnchor: Point; + horizontalValue: any; + verticalValue: any; +} + +/** @internal */ +export function getPerPanelMap( + scales: SmallMultipleScales, + fn: (panelAnchor: Point, horizontalValue: any, verticalValue: any, scales: SmallMultipleScales) => T | null, +): Array { + const { horizontal, vertical } = scales; + return vertical.domain.reduce>((acc, verticalValue) => { + return [ + ...acc, + ...horizontal.domain.reduce>((hAcc, horizontalValue) => { + const panelAnchor: Point = { + x: horizontal.scale(horizontalValue) ?? 0, + y: vertical.scale(verticalValue) ?? 0, + }; + const fnObj = fn(panelAnchor, horizontalValue, verticalValue, scales); + if (!fnObj) { + return hAcc; + } + return [ + ...hAcc, + { + panelAnchor, + horizontalValue, + verticalValue, + ...fnObj, + }, + ]; + }, []), + ]; + }, []); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/scales.test.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/scales.test.ts new file mode 100644 index 000000000000..87504595517c --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/scales.test.ts @@ -0,0 +1,145 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MockXDomain } from '../../../mocks/xy/domains'; +import { ScaleType } from '../../../scales/constants'; +import { computeXScale } from './scales'; + +describe('Series scales', () => { + const xDomainLinear = MockXDomain.fromScaleType(ScaleType.Linear, { + isBandScale: true, + domain: [0, 3], + minInterval: 1, + }); + + const xDomainOrdinal = MockXDomain.fromScaleType(ScaleType.Ordinal, { + isBandScale: true, + domain: ['a', 'b'], + minInterval: 1, + }); + + test('should compute X Scale linear min, max with bands', () => { + const scale = computeXScale({ xDomain: xDomainLinear, totalBarsInCluster: 1, range: [0, 120] }); + const expectedBandwidth = 120 / 4; + expect(scale.bandwidth).toBe(120 / 4); + expect(scale.scale(0)).toBe(0); + expect(scale.scale(1)).toBe(expectedBandwidth); + expect(scale.scale(2)).toBe(expectedBandwidth * 2); + expect(scale.scale(3)).toBe(expectedBandwidth * 3); + }); + + test('should compute X Scale linear inverse min, max with bands', () => { + const scale = computeXScale({ xDomain: xDomainLinear, totalBarsInCluster: 1, range: [120, 0] }); + const expectedBandwidth = 120 / 4; + expect(scale.bandwidth).toBe(expectedBandwidth); + expect(scale.scale(0)).toBe(expectedBandwidth * 3); + expect(scale.scale(1)).toBe(expectedBandwidth * 2); + expect(scale.scale(2)).toBe(expectedBandwidth); + expect(scale.scale(3)).toBe(0); + }); + + describe('computeXScale with single value domain', () => { + const maxRange = 120; + const singleDomainValue = 3; + const minInterval = 1; + + test('should return extended domain & range when in histogram mode', () => { + const xDomain = MockXDomain.fromScaleType(ScaleType.Linear, { + isBandScale: true, + domain: [singleDomainValue, singleDomainValue], + minInterval, + }); + const enableHistogramMode = true; + + const scale = computeXScale({ + xDomain, + totalBarsInCluster: 1, + range: [0, maxRange], + barsPadding: 0, + enableHistogramMode, + }); + expect(scale.bandwidth).toBe(maxRange); + expect(scale.domain).toEqual([singleDomainValue, singleDomainValue + minInterval]); + // reducing of 1 pixel the range for band scale + expect(scale.range).toEqual([0, maxRange]); + }); + + test('should return unextended domain & range when not in histogram mode', () => { + const xDomain = MockXDomain.fromScaleType(ScaleType.Linear, { + isBandScale: true, + domain: [singleDomainValue, singleDomainValue], + minInterval, + }); + const enableHistogramMode = false; + + const scale = computeXScale({ + xDomain, + totalBarsInCluster: 1, + range: [0, maxRange], + barsPadding: 0, + enableHistogramMode, + }); + expect(scale.bandwidth).toBe(maxRange); + expect(scale.domain).toEqual([singleDomainValue, singleDomainValue]); + expect(scale.range).toEqual([0, 0]); + }); + }); + + test('should compute X Scale ordinal', () => { + const nonZeroGroupScale = computeXScale({ xDomain: xDomainOrdinal, totalBarsInCluster: 1, range: [120, 0] }); + const expectedBandwidth = 60; + expect(nonZeroGroupScale.bandwidth).toBe(expectedBandwidth); + expect(nonZeroGroupScale.scale('a')).toBe(expectedBandwidth); + expect(nonZeroGroupScale.scale('b')).toBe(0); + + const zeroGroupScale = computeXScale({ xDomain: xDomainOrdinal, totalBarsInCluster: 0, range: [120, 0] }); + expect(zeroGroupScale.bandwidth).toBe(expectedBandwidth); + }); + + describe('bandwidth when totalBarsInCluster is greater than 0 or less than 0', () => { + const xDomainLinear = MockXDomain.fromScaleType(ScaleType.Linear, { + isBandScale: true, + domain: [0, 3], + minInterval: 1, + }); + const maxRange = 120; + const scaleOver0 = computeXScale({ + xDomain: xDomainLinear, + totalBarsInCluster: 2, + range: [0, maxRange], + barsPadding: 0, + enableHistogramMode: false, + }); + + test('totalBarsInCluster greater than 0', () => { + expect(scaleOver0.bandwidth).toBe(maxRange / 4 / 2); + }); + + const scaleUnder0 = computeXScale({ + xDomain: xDomainLinear, + totalBarsInCluster: 0, + range: [0, maxRange], + barsPadding: 0, + enableHistogramMode: false, + }); + test('totalBarsInCluster less than 0', () => { + expect(scaleUnder0.bandwidth).toBe(maxRange / 4); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/scales.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/scales.ts new file mode 100644 index 000000000000..98243d07126a --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/scales.ts @@ -0,0 +1,162 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Scale, ScaleBand, ScaleContinuous } from '../../../scales'; +import { ScaleType } from '../../../scales/constants'; +import { LogBase } from '../../../scales/scale_continuous'; +import { ContinuousDomain, Range } from '../../../utils/domain'; +import { GroupId } from '../../../utils/ids'; +import { XDomain, YDomain } from '../domains/types'; + +function getBandScaleRange( + isInverse: boolean, + isSingleValueHistogram: boolean, + minRange: number, + maxRange: number, + bandwidth: number, +): { + start: number; + end: number; +} { + const rangeEndOffset = isSingleValueHistogram ? 0 : bandwidth; + const start = isInverse ? minRange - rangeEndOffset : minRange; + const end = isInverse ? maxRange : maxRange - rangeEndOffset; + return { start, end }; +} + +interface XScaleOptions { + xDomain: XDomain; + totalBarsInCluster: number; + range: Range; + barsPadding?: number; + enableHistogramMode?: boolean; + integersOnly?: boolean; + logBase?: LogBase; + logMinLimit?: number; +} + +/** + * Compute the x scale used to align geometries to the x axis. + * @internal + */ +export function computeXScale(options: XScaleOptions): Scale { + const { xDomain, totalBarsInCluster, range, barsPadding, enableHistogramMode, integersOnly } = options; + const { type, nice, minInterval, domain, isBandScale, timeZone, logBase, desiredTickCount } = xDomain; + const rangeDiff = Math.abs(range[1] - range[0]); + const isInverse = range[1] < range[0]; + if (type === ScaleType.Ordinal) { + const dividend = totalBarsInCluster > 0 ? totalBarsInCluster : 1; + const bandwidth = rangeDiff / (domain.length * dividend); + return new ScaleBand(domain, range, bandwidth, barsPadding); + } + if (isBandScale) { + const [domainMin, domainMax] = domain as ContinuousDomain; + const isSingleValueHistogram = !!enableHistogramMode && domainMax - domainMin === 0; + + const adjustedDomainMax = isSingleValueHistogram ? domainMin + minInterval : domainMax; + const adjustedDomain = [domainMin, adjustedDomainMax]; + + const intervalCount = (adjustedDomain[1] - adjustedDomain[0]) / minInterval; + const intervalCountOffset = isSingleValueHistogram ? 0 : 1; + const bandwidth = rangeDiff / (intervalCount + intervalCountOffset); + const { start, end } = getBandScaleRange(isInverse, isSingleValueHistogram, range[0], range[1], bandwidth); + + return new ScaleContinuous( + { + type, + domain: adjustedDomain, + range: [start, end], + nice, + }, + { + bandwidth: totalBarsInCluster > 0 ? bandwidth / totalBarsInCluster : bandwidth, + minInterval, + timeZone, + totalBarsInCluster, + barsPadding, + desiredTickCount, + isSingleValueHistogram, + logBase, + }, + ); + } + return new ScaleContinuous( + { type, domain, range, nice }, + { + bandwidth: 0, + minInterval, + timeZone, + totalBarsInCluster, + barsPadding, + desiredTickCount, + integersOnly, + logBase, + }, + ); +} + +interface YScaleOptions { + yDomains: YDomain[]; + range: Range; + integersOnly?: boolean; +} + +/** + * Compute the y scales, one per groupId for the y axis. + * @internal + */ +export function computeYScales(options: YScaleOptions): Map { + const { yDomains, range, integersOnly } = options; + return yDomains.reduce( + ( + yScales, + { + type, + nice, + desiredTickCount, + domain, + groupId, + logBase, + logMinLimit, + domainPixelPadding, + constrainDomainPadding, + }, + ) => { + const yScale = new ScaleContinuous( + { + type, + domain, + range, + nice, + }, + { + desiredTickCount, + integersOnly, + logBase, + logMinLimit, + domainPixelPadding, + constrainDomainPadding, + }, + ); + yScales.set(groupId, yScale); + return yScales; + }, + new Map(), + ); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/series.test.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/series.test.ts new file mode 100644 index 000000000000..3b3a05f0401a --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/series.test.ts @@ -0,0 +1,1020 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { flatten } from 'lodash'; + +import { ChartType } from '../..'; +import { MockDataSeries } from '../../../mocks/series'; +import { MockSeriesIdentifier } from '../../../mocks/series/series_identifiers'; +import { MockSeriesSpec, MockGlobalSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store'; +import { SeededDataGenerator, getRandomNumberGenerator } from '../../../mocks/utils'; +import { ScaleType } from '../../../scales/constants'; +import { SpecType } from '../../../specs/constants'; +import { AccessorFn } from '../../../utils/accessor'; +import { Position } from '../../../utils/common'; +import * as TestDataset from '../../../utils/data_samples/test_dataset'; +import { KIBANA_METRICS } from '../../../utils/data_samples/test_dataset_kibana'; +import { ColorConfig } from '../../../utils/themes/theme'; +import { computeSeriesDomainsSelector } from '../state/selectors/compute_series_domains'; +import { + getFormattedDataSeries, + getSeriesColors, + getDataSeriesFromSpecs, + XYChartSeriesIdentifier, + extractYAndMarkFromDatum, + getSeriesName, + DataSeries, + splitSeriesDataByAccessors, +} from './series'; +import { BasicSeriesSpec, LineSeriesSpec, SeriesType, AreaSeriesSpec } from './specs'; +import { formatStackedDataSeriesValues } from './stacked_series_utils'; + +const dg = new SeededDataGenerator(); +const getRandomNumber = getRandomNumberGenerator(); + +function matchOnlyDataSeriesLegacySnapshot(d: DataSeries) { + const { + spec, + groupId, + isStacked, + seriesType, + smVerticalAccessorValue, + smHorizontalAccessorValue, + stackMode, + insertIndex, + isFiltered, + ...rest + } = d; + return { + ...rest, + }; +} + +describe('Series', () => { + test('Can split dataset into 1Y0G series', () => { + const splitSeries = splitSeriesDataByAccessors( + MockSeriesSpec.bar({ + id: 'spec1', + data: TestDataset.BARCHART_1Y0G, + xAccessor: 'x', + yAccessors: ['y'], + }), + new Map(), + ); + + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + test('Can split dataset into 1Y1G series', () => { + const splitSeries = splitSeriesDataByAccessors( + MockSeriesSpec.bar({ + id: 'spec1', + data: TestDataset.BARCHART_1Y1G, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + }), + new Map(), + ); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + test('Can split dataset into 1Y2G series', () => { + const splitSeries = splitSeriesDataByAccessors( + MockSeriesSpec.bar({ + id: 'spec1', + data: TestDataset.BARCHART_1Y2G, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g1', 'g2'], + }), + new Map(), + ); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + test('Can split dataset into 2Y0G series', () => { + const splitSeries = splitSeriesDataByAccessors( + MockSeriesSpec.bar({ + id: 'spec1', + data: TestDataset.BARCHART_2Y0G, + xAccessor: 'x', + yAccessors: ['y1', 'y2'], + }), + new Map(), + ); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + test('Can split dataset into 2Y1G series', () => { + const splitSeries = splitSeriesDataByAccessors( + MockSeriesSpec.bar({ + id: 'spec1', + data: TestDataset.BARCHART_2Y1G, + xAccessor: 'x', + yAccessors: ['y1', 'y2'], + splitSeriesAccessors: ['g'], + }), + new Map(), + ); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + test('Can split dataset into 2Y2G series', () => { + const splitSeries = splitSeriesDataByAccessors( + MockSeriesSpec.bar({ + id: 'spec1', + data: TestDataset.BARCHART_2Y2G, + xAccessor: 'x', + yAccessors: ['y1', 'y2'], + splitSeriesAccessors: ['g1', 'g2'], + }), + new Map(), + ); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + it('should get sum of all xValues', () => { + const xValueSums = new Map(); + splitSeriesDataByAccessors( + MockSeriesSpec.bar({ + id: 'spec1', + data: TestDataset.BARCHART_1Y1G_ORDINAL, + xAccessor: 'x', + yAccessors: ['y'], + splitSeriesAccessors: ['g'], + }), + xValueSums, + ); + expect(xValueSums).toEqual( + new Map([ + ['a', 3], + ['b', 5], + ['c', 3], + ['d', 4], + ['e', 9], + ]), + ); + }); + test('Can stack simple dataseries', () => { + const store = MockStore.default(); + MockStore.addSpecs( + MockSeriesSpec.area({ + id: 'spec1', + splitSeriesAccessors: ['g'], + yAccessors: ['y1'], + stackAccessors: ['x'], + xScaleType: ScaleType.Linear, + data: [ + { x: 1, y1: 1, g: 'a' }, + { x: 2, y1: 2, g: 'a' }, + { x: 4, y1: 4, g: 'a' }, + { x: 1, y1: 21, g: 'b' }, + { x: 3, y1: 23, g: 'b' }, + ], + }), + store, + ); + + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + expect(formattedDataSeries.map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + test('Can stack multiple dataseries', () => { + const dataSeries: DataSeries[] = [ + MockDataSeries.default({ + specId: 'spec1', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: ['a'], + key: 'a', + data: [ + { x: 1, y1: 1, mark: null, y0: null, initialY1: 1, initialY0: null, datum: undefined }, + { x: 2, y1: 2, mark: null, y0: null, initialY1: 2, initialY0: null, datum: undefined }, + { x: 3, y1: 3, mark: null, y0: null, initialY1: 3, initialY0: null, datum: undefined }, + { x: 4, y1: 4, mark: null, y0: null, initialY1: 4, initialY0: null, datum: undefined }, + ], + }), + MockDataSeries.default({ + specId: 'spec1', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: ['b'], + key: 'b', + data: [ + { x: 1, y1: 1, mark: null, y0: null, initialY1: 1, initialY0: null, datum: undefined }, + { x: 2, y1: 2, mark: null, y0: null, initialY1: 2, initialY0: null, datum: undefined }, + { x: 3, y1: 3, mark: null, y0: null, initialY1: 3, initialY0: null, datum: undefined }, + { x: 4, y1: 4, mark: null, y0: null, initialY1: 4, initialY0: null, datum: undefined }, + ], + }), + MockDataSeries.default({ + specId: 'spec1', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: ['b'], + key: 'b', + data: [ + { x: 1, y1: 1, mark: null, y0: null, initialY1: 1, initialY0: null, datum: undefined }, + { x: 2, y1: 2, mark: null, y0: null, initialY1: 2, initialY0: null, datum: undefined }, + { x: 3, y1: 3, mark: null, y0: null, initialY1: 3, initialY0: null, datum: undefined }, + { x: 4, y1: 4, mark: null, y0: null, initialY1: 4, initialY0: null, datum: undefined }, + ], + }), + MockDataSeries.default({ + specId: 'spec1', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: ['b'], + key: 'b', + data: [ + { x: 1, y1: 1, mark: null, y0: null, initialY1: 1, initialY0: null, datum: undefined }, + { x: 2, y1: 2, mark: null, y0: null, initialY1: 2, initialY0: null, datum: undefined }, + { x: 3, y1: 3, mark: null, y0: null, initialY1: 3, initialY0: null, datum: undefined }, + { x: 4, y1: 4, mark: null, y0: null, initialY1: 4, initialY0: null, datum: undefined }, + ], + }), + ]; + const xValues = new Set([1, 2, 3, 4]); + const stackedValues = formatStackedDataSeriesValues(dataSeries, xValues); + expect(stackedValues.map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + test('Can stack unsorted dataseries', () => { + const store = MockStore.default(); + MockStore.addSpecs( + MockSeriesSpec.area({ + id: 'spec1', + splitSeriesAccessors: ['g'], + yAccessors: ['y1'], + stackAccessors: ['x'], + xScaleType: ScaleType.Linear, + data: [ + { x: 1, y1: 1, g: 'a' }, + { x: 4, y1: 4, g: 'a' }, + { x: 2, y1: 2, g: 'a' }, + { x: 3, y1: 23, g: 'b' }, + { x: 1, y1: 21, g: 'b' }, + ], + }), + store, + ); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + expect(formattedDataSeries.map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + test('Can stack high volume of dataseries', () => { + const maxArrayItems = 1000; + const dataSeries: DataSeries[] = [ + MockDataSeries.default({ + specId: 'spec1', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: ['a'], + key: 'a', + data: new Array(maxArrayItems) + .fill(0) + .map((d, i) => ({ x: i, y1: i, mark: null, y0: null, initialY1: i, initialY0: null, datum: undefined })), + }), + MockDataSeries.default({ + specId: 'spec1', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: ['b'], + key: 'b', + data: new Array(maxArrayItems) + .fill(0) + .map((d, i) => ({ x: i, y1: i, mark: null, y0: null, initialY1: i, initialY0: null, datum: undefined })), + }), + ]; + const xValues = new Set(new Array(maxArrayItems).fill(0).map((d, i) => i)); + const stackedValues = formatStackedDataSeriesValues(dataSeries, xValues); + expect(stackedValues.map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + test('Can stack simple dataseries with scale to extent', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockGlobalSpec.axis({ + id: 'y', + position: Position.Left, + domain: { fit: true }, + }), + MockSeriesSpec.bar({ + id: 'spec1', + yAccessors: ['y1'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + xScaleType: ScaleType.Linear, + data: [ + { x: 1, y1: 1, g: 'a' }, + { x: 2, y1: 2, g: 'a' }, + { x: 4, y1: 4, g: 'a' }, + { x: 1, y1: 21, g: 'b' }, + { x: 3, y1: 23, g: 'b' }, + ], + }), + ], + store, + ); + + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + expect(formattedDataSeries.map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + test('Can stack multiple dataseries with scale to extent', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockGlobalSpec.axis({ + id: 'y', + position: Position.Left, + domain: { fit: true }, + }), + MockSeriesSpec.bar({ + id: 'spec1', + yAccessors: ['y1'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + xScaleType: ScaleType.Linear, + data: [ + { x: 1, y1: 1, g: 'a' }, + { x: 2, y1: 2, g: 'a' }, + { x: 3, y1: 3, g: 'a' }, + { x: 4, y1: 4, g: 'a' }, + { x: 1, y1: 1, g: 'b' }, + { x: 2, y1: 2, g: 'b' }, + { x: 3, y1: 3, g: 'b' }, + { x: 4, y1: 4, g: 'b' }, + { x: 1, y1: 1, g: 'c' }, + { x: 2, y1: 2, g: 'c' }, + { x: 3, y1: 3, g: 'c' }, + { x: 4, y1: 4, g: 'c' }, + { x: 1, y1: 1, g: 'd' }, + { x: 2, y1: 2, g: 'd' }, + { x: 3, y1: 3, g: 'd' }, + { x: 4, y1: 4, g: 'd' }, + ], + }), + ], + store, + ); + + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + expect(formattedDataSeries.map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + test('Can stack simple dataseries with y0', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockGlobalSpec.axis({ + id: 'y', + position: Position.Left, + domain: { fit: true }, + }), + MockSeriesSpec.bar({ + id: 'spec1', + yAccessors: ['y1'], + y0Accessors: ['y0'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + xScaleType: ScaleType.Linear, + data: [ + { x: 1, y1: 3, y0: 1, g: 'a' }, + { x: 2, y1: 3, y0: 2, g: 'a' }, + { x: 4, y1: 4, y0: 3, g: 'a' }, + { x: 1, y1: 2, y0: 1, g: 'b' }, + { x: 2, y1: 3, y0: 1, g: 'b' }, + { x: 3, y1: 23, y0: 4, g: 'b' }, + { x: 4, y1: 4, y0: 1, g: 'b' }, + ], + }), + ], + store, + ); + + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + expect(formattedDataSeries.map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + test('Can stack simple dataseries with scale to extent with y0', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockGlobalSpec.axis({ + id: 'y', + position: Position.Left, + domain: { fit: true }, + }), + MockSeriesSpec.bar({ + id: 'spec1', + yAccessors: ['y1'], + y0Accessors: ['y0'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + xScaleType: ScaleType.Linear, + data: [ + { x: 1, y1: 3, y0: 1, g: 'a' }, + { x: 2, y1: 3, y0: 2, g: 'a' }, + { x: 4, y1: 4, y0: 3, g: 'a' }, + { x: 1, y1: 2, y0: 1, g: 'b' }, + { x: 2, y1: 3, y0: 1, g: 'b' }, + { x: 3, y1: 23, y0: 4, g: 'b' }, + { x: 4, y1: 4, y0: 1, g: 'b' }, + ], + }), + ], + store, + ); + + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + expect(formattedDataSeries.map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + + test('should split an array of specs into data series', () => { + const spec1: LineSeriesSpec = { + specType: SpecType.Series, + chartType: ChartType.XYAxis, + id: 'spec1', + groupId: 'group', + seriesType: SeriesType.Line, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + data: TestDataset.BARCHART_1Y0G, + hideInLegend: false, + }; + const spec2: BasicSeriesSpec = { + specType: SpecType.Series, + chartType: ChartType.XYAxis, + id: 'spec2', + groupId: 'group2', + seriesType: SeriesType.Line, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y1', 'y2'], + stackAccessors: ['x'], + data: TestDataset.BARCHART_2Y0G, + hideInLegend: false, + }; + + const { dataSeries } = getDataSeriesFromSpecs([spec1, spec2]); + expect(dataSeries.filter(({ specId }) => specId === 'spec1')).toMatchSnapshot(); + expect(dataSeries.filter(({ specId }) => specId === 'spec2')).toMatchSnapshot(); + }); + test('should compute data series for stacked specs', () => { + const spec1: BasicSeriesSpec = { + specType: SpecType.Series, + chartType: ChartType.XYAxis, + id: 'spec1', + groupId: 'group', + seriesType: SeriesType.Line, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + data: TestDataset.BARCHART_1Y0G, + hideInLegend: false, + }; + const spec2: BasicSeriesSpec = { + specType: SpecType.Series, + chartType: ChartType.XYAxis, + id: 'spec2', + groupId: 'group2', + seriesType: SeriesType.Line, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y1', 'y2'], + stackAccessors: ['x'], + data: TestDataset.BARCHART_2Y0G, + hideInLegend: false, + }; + const xValues = new Set([0, 1, 2, 3]); + + const { dataSeries } = getDataSeriesFromSpecs([spec1, spec2]); + const stackedDataSeries = getFormattedDataSeries([spec1, spec2], dataSeries, xValues, ScaleType.Linear); + + expect(stackedDataSeries.map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + + describe('#getSeriesColors', () => { + const seriesKey = 'groupId{group1}spec{spec1}yAccessor{y1}splitAccessors{}'; + + const chartColors: ColorConfig = { + vizColors: ['elastic_charts_c1', 'elastic_charts_c2'], + defaultVizColor: 'elastic_charts', + }; + + const seriesColors = [ + MockDataSeries.default({ + specId: 'spec1', + yAccessor: 'y1', + splitAccessors: new Map(), + seriesKeys: ['a', 'b', 'c'], + key: seriesKey, + }), + ]; + + const emptyCustomColors = new Map(); + const persistedColor = 'persisted_color'; + const customColor = 'custom_color'; + const customColors: Map = new Map(); + customColors.set(seriesKey, customColor); + const emptyColorOverrides = { + persisted: {}, + temporary: {}, + }; + const persistedOverrides = { + persisted: { [seriesKey]: persistedColor }, + temporary: {}, + }; + + it('should return deafult color', () => { + const result = getSeriesColors(seriesColors, chartColors, emptyCustomColors, emptyColorOverrides); + const expected = new Map(); + expected.set(seriesKey, 'elastic_charts_c1'); + expect(result).toEqual(expected); + }); + + it('should return persisted color', () => { + const result = getSeriesColors(seriesColors, chartColors, emptyCustomColors, persistedOverrides); + const expected = new Map(); + expected.set(seriesKey, persistedColor); + expect(result).toEqual(expected); + }); + + it('should return custom color', () => { + const result = getSeriesColors(seriesColors, chartColors, customColors, persistedOverrides); + const expected = new Map(); + expected.set(seriesKey, customColor); + expect(result).toEqual(expected); + }); + + it('should return temporary color', () => { + const temporaryColor = 'persisted-color'; + const result = getSeriesColors(seriesColors, chartColors, customColors, { + ...persistedOverrides, + temporary: { [seriesKey]: temporaryColor }, + }); + const expected = new Map(); + expected.set(seriesKey, temporaryColor); + expect(result).toEqual(expected); + }); + }); + test('should only include deselectedDataSeries when splitting series if deselectedDataSeries is defined', () => { + const id = 'splitSpec'; + const yAccessors = ['y1', 'y2']; + const splitSpec: BasicSeriesSpec = { + specType: SpecType.Series, + chartType: ChartType.XYAxis, + id, + groupId: 'group', + seriesType: SeriesType.Line, + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors, + stackAccessors: ['x'], + data: TestDataset.BARCHART_2Y0G, + hideInLegend: false, + }; + + const allSeries = getDataSeriesFromSpecs([splitSpec]); + expect(allSeries.dataSeries.filter(({ specId }) => specId === id)).toHaveLength(2); + + const emptyDeselected = getDataSeriesFromSpecs([splitSpec]); + expect(emptyDeselected.dataSeries.filter(({ specId }) => specId === id)).toHaveLength(2); + + const deselectedDataSeries: XYChartSeriesIdentifier[] = [ + { + specId: id, + yAccessor: yAccessors[0], + splitAccessors: new Map(), + seriesKeys: [], + key: 'groupId{group}spec{splitSpec}yAccessor{y1}splitAccessors{}', + }, + ]; + const subsetSplit = getDataSeriesFromSpecs([splitSpec], deselectedDataSeries); + expect(subsetSplit.dataSeries.filter(({ specId, isFiltered }) => specId === id && !isFiltered)).toHaveLength(1); + }); + + test('clean datum shall parse string as number for y values', () => { + let datum = extractYAndMarkFromDatum([0, 1, 2], 1, [], 2); + expect(datum).toBeDefined(); + expect(datum?.y1).toBe(1); + expect(datum?.y0).toBe(2); + datum = extractYAndMarkFromDatum([0, '1', 2], 1, [], 2); + expect(datum).toBeDefined(); + expect(datum?.y1).toBe(1); + expect(datum?.y0).toBe(2); + + datum = extractYAndMarkFromDatum([0, '1', '2'], 1, [], 2); + expect(datum).toBeDefined(); + expect(datum?.y1).toBe(1); + expect(datum?.y0).toBe(2); + + datum = extractYAndMarkFromDatum([0, 1, '2'], 1, [], 2); + expect(datum).toBeDefined(); + expect(datum?.y1).toBe(1); + expect(datum?.y0).toBe(2); + + datum = extractYAndMarkFromDatum([0, 'invalid', 'invalid'], 1, [], 2); + expect(datum).toBeDefined(); + expect(datum?.y1).toBe(null); + expect(datum?.y0).toBe(null); + }); + describe('#getSeriesNameKeys', () => { + const data = dg.generateGroupedSeries(50, 2).map((d) => ({ ...d, y2: d.y })); + const spec = MockSeriesSpec.area({ + data, + yAccessors: ['y', 'y2'], + splitSeriesAccessors: ['g'], + }); + const indentifiers = MockSeriesIdentifier.fromSpecs([spec]); + + it('should get series label from spec', () => { + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, spec); + expect(actual).toBe('a - y'); + }); + + it('should not show y value with single yAccessor', () => { + const specSingleY: AreaSeriesSpec = { + ...spec, + yAccessors: ['y'], + }; + const [identifier] = MockSeriesIdentifier.fromSpecs([spec]); + const actual = getSeriesName(identifier, false, false, specSingleY); + + expect(actual).toBe('a'); + }); + + describe('Custom labeling', () => { + it('should replace full label', () => { + const label = 'My custom new label'; + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, { + ...spec, + name: ({ yAccessor, splitAccessors }) => + yAccessor === identifier.yAccessor && splitAccessors.get('g') === 'a' ? label : null, + }); + + expect(actual).toBe(label); + }); + + it('should have access to all accessors with single y', () => { + const specSingleY: AreaSeriesSpec = { + ...spec, + yAccessors: ['y'], + name: ({ seriesKeys }) => seriesKeys.join(' - '), + }; + const [identifier] = MockSeriesIdentifier.fromSpecs([spec]); + const actual = getSeriesName(identifier, false, false, specSingleY); + + expect(actual).toBe('a - y'); + }); + + it('should replace yAccessor sub label with map', () => { + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, { + ...spec, + name: { + names: [ + { + accessor: 'g', + value: 'a', + }, + { + accessor: 'y', + name: 'Yuuuup', + }, + ], + }, + }); + expect(actual).toBe('a - Yuuuup'); + }); + + it('should join with custom delimiter', () => { + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, { + ...spec, + name: { + names: [ + { + accessor: 'g', + value: 'a', + }, + { + accessor: 'y', + }, + ], + delimiter: ' ¯\\_(ツ)_/¯ ', + }, + }); + expect(actual).toBe('a ¯\\_(ツ)_/¯ y'); + }); + + it('should replace splitAccessor sub label with map', () => { + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, { + ...spec, + name: { + names: [ + { + accessor: 'g', + value: 'a', + name: 'Apple', + }, + { + accessor: 'y', + }, + ], + }, + }); + expect(actual).toBe('Apple - y'); + }); + + it('should mind order of names', () => { + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, { + ...spec, + name: { + names: [ + { + accessor: 'y', + name: 'Yuuum', + }, + { + accessor: 'g', + value: 'a', + name: 'Apple', + }, + ], + }, + }); + expect(actual).toBe('Yuuum - Apple'); + }); + + it('should mind sortIndex of names', () => { + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, { + ...spec, + name: { + names: [ + { + accessor: 'y', + name: 'Yuuum', + sortIndex: 2, + }, + { + accessor: 'g', + value: 'a', + name: 'Apple', + sortIndex: 0, + }, + ], + }, + }); + expect(actual).toBe('Apple - Yuuum'); + }); + + it('should allow undefined sortIndex', () => { + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, { + ...spec, + name: { + names: [ + { + accessor: 'y', + name: 'Yuuum', + }, + { + accessor: 'g', + value: 'a', + name: 'Apple', + sortIndex: 0, + }, + ], + }, + }); + expect(actual).toBe('Apple - Yuuum'); + }); + + it('should ignore missing names', () => { + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, { + ...spec, + name: { + names: [ + { + accessor: 'g', + value: 'a', + name: 'Apple', + }, + { + accessor: 'g', + value: 'Not a mapping', + name: 'No Value', + }, + { + accessor: 'y', + name: 'Yuuum', + }, + ], + }, + }); + expect(actual).toBe('Apple - Yuuum'); + }); + + it('should return fallback label if empty string', () => { + const [identifier] = indentifiers; + const actual = getSeriesName(identifier, false, false, { + ...spec, + name: { + names: [], + }, + }); + expect(actual).toBe('a - y'); + }); + }); + + test('Shall ignore undefined values on splitSeriesAccessors', () => { + const spec = MockSeriesSpec.bar({ + data: [ + [0, 1, 'a'], + [1, 1, 'a'], + [2, 1, 'a'], + [0, 1, 'b'], + [1, 1, 'b'], + [2, 1, 'b'], + [0, 1], + [1, 1], + [2, 1], + ], + xAccessor: 0, + yAccessors: [1], + splitSeriesAccessors: [2], + }); + const splitSeries = splitSeriesDataByAccessors(spec, new Map()); + expect([...splitSeries.dataSeries.values()].length).toBe(2); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + test('Should ignore series if splitSeriesAccessors are defined but not contained in any datum', () => { + const spec = MockSeriesSpec.bar({ + data: [ + [0, 1], + [1, 1], + [2, 1], + ], + xAccessor: 0, + yAccessors: [1], + splitSeriesAccessors: [2], + }); + const splitSeries = splitSeriesDataByAccessors(spec, new Map()); + expect([...splitSeries.dataSeries.values()].length).toBe(0); + }); + }); + + describe('functional accessors', () => { + test('Can use functional xAccessor', () => { + const xAccessor: AccessorFn = (d) => d.x; + const splitSeries = splitSeriesDataByAccessors( + MockSeriesSpec.bar({ + data: TestDataset.BARCHART_2Y2G, + xAccessor, + yAccessors: ['y1', 'y2'], + splitSeriesAccessors: ['g1'], + }), + new Map(), + ); + expect([...splitSeries.dataSeries.values()].length).toBe(4); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + + test('Can use default custom xAccessor', () => { + const xAccessor: AccessorFn = () => '_all'; + const splitSeries = splitSeriesDataByAccessors( + MockSeriesSpec.bar({ + data: TestDataset.BARCHART_2Y2G, + xAccessor, + yAccessors: ['y1'], + splitSeriesAccessors: ['g1'], + }), + new Map(), + ); + expect([...splitSeries.dataSeries.values()].length).toBe(2); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + + test('Can use functional yAccessor', () => { + const yAccessor: AccessorFn = (d) => d.y1; + const splitSeries = splitSeriesDataByAccessors( + MockSeriesSpec.bar({ + data: TestDataset.BARCHART_2Y2G, + yAccessors: [yAccessor], + splitSeriesAccessors: ['g1'], + }), + new Map(), + ); + expect([...splitSeries.dataSeries.values()].map(({ yAccessor }) => yAccessor)).toEqualArrayOf('(index:0)'); + expect([...splitSeries.dataSeries.values()].length).toBe(2); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + + test('Can use functional yAccessor with fieldName', () => { + const yAccessor: AccessorFn = (d) => d.y1; + yAccessor.fieldName = 'custom name'; + const splitSeries = splitSeriesDataByAccessors( + MockSeriesSpec.bar({ + data: TestDataset.BARCHART_2Y2G, + yAccessors: [yAccessor], + splitSeriesAccessors: ['g1'], + }), + new Map(), + ); + expect([...splitSeries.dataSeries.values()].map(({ yAccessor }) => yAccessor)).toEqualArrayOf( + yAccessor.fieldName, + ); + expect([...splitSeries.dataSeries.values()].length).toBe(2); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + + test('Can use functional y0Accessor', () => { + const splitSeries = splitSeriesDataByAccessors( + MockSeriesSpec.bar({ + data: KIBANA_METRICS.metrics.kibana_os_load[0].data.map((d: any) => ({ + x: d[0], + max: d[1] + 4 + 4 * getRandomNumber(), + min: d[1] - 4 - 4 * getRandomNumber(), + })), + yAccessors: [(d) => d.max], + y0Accessors: [(d) => d.min], + }), + new Map(), + ); + expect([...splitSeries.dataSeries.values()].map(({ yAccessor }) => yAccessor)).toEqualArrayOf('(index:0)'); + expect([...splitSeries.dataSeries.values()].length).toBe(1); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + + test('Can use functional splitSeriesAccessor', () => { + const splitSeriesAccessor: AccessorFn = (d) => d.g1; + const splitSeries = splitSeriesDataByAccessors( + MockSeriesSpec.bar({ + data: TestDataset.BARCHART_2Y2G, + yAccessors: ['y1'], + splitSeriesAccessors: [splitSeriesAccessor], + }), + new Map(), + ); + expect( + flatten([...splitSeries.dataSeries.values()].map(({ splitAccessors }) => [...splitAccessors.keys()])), + ).toEqualArrayOf('(index:0)'); + expect([...splitSeries.dataSeries.values()].length).toBe(2); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + + test('Can use functional splitSeriesAccessor with fieldName', () => { + const splitSeriesAccessor: AccessorFn = (d) => d.g1; + splitSeriesAccessor.fieldName = 'custom name'; + const splitSeries = splitSeriesDataByAccessors( + MockSeriesSpec.bar({ + data: TestDataset.BARCHART_2Y2G, + yAccessors: ['y1'], + splitSeriesAccessors: [splitSeriesAccessor], + }), + new Map(), + ); + expect( + flatten([...splitSeries.dataSeries.values()].map(({ splitAccessors }) => [...splitAccessors.keys()])), + ).toEqualArrayOf(splitSeriesAccessor.fieldName); + expect([...splitSeries.dataSeries.values()].length).toBe(2); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + + test('Can use multiple functional/static accessors', () => { + const splitSeries = splitSeriesDataByAccessors( + MockSeriesSpec.bar({ + data: TestDataset.BARCHART_2Y2G, + xAccessor: (d) => d.y1, + yAccessors: ['y1', (d) => d.y2], + splitSeriesAccessors: [(d) => d.g1, 'g2'], + }), + new Map(), + ); + expect([...splitSeries.dataSeries.values()].length).toBe(8); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/series.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/series.ts new file mode 100644 index 000000000000..6d48c8201c34 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/series.ts @@ -0,0 +1,629 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SeriesIdentifier, SeriesKey } from '../../../common/series_id'; +import { ScaleType } from '../../../scales/constants'; +import { BinAgg, Direction, XScaleType } from '../../../specs'; +import { OrderBy } from '../../../specs/settings'; +import { ColorOverrides } from '../../../state/chart_state'; +import { Accessor, AccessorFn, getAccessorValue } from '../../../utils/accessor'; +import { Color, Datum, isNil } from '../../../utils/common'; +import { GroupId } from '../../../utils/ids'; +import { Logger } from '../../../utils/logger'; +import { ColorConfig } from '../../../utils/themes/theme'; +import { groupSeriesByYGroup, isHistogramEnabled, isStackedSpec } from '../domains/y_domain'; +import { X_SCALE_DEFAULT } from '../scales/scale_defaults'; +import { SmallMultiplesGroupBy } from '../state/selectors/get_specs'; +import { applyFitFunctionToDataSeries } from './fit_function_utils'; +import { groupBy } from './group_data_series'; +import { BasicSeriesSpec, SeriesNameConfigOptions, SeriesSpecs, SeriesType, StackMode } from './specs'; +import { datumXSortPredicate, formatStackedDataSeriesValues } from './stacked_series_utils'; + +/** @internal */ +export const SERIES_DELIMITER = ' - '; + +/** @public */ +export interface FilledValues { + /** the x value */ + x?: number | string; + /** the max y value */ + y1?: number; + /** the minimum y value */ + y0?: number; +} + +/** @public */ +export interface DataSeriesDatum { + /** the x value */ + x: number | string; + /** the max y value */ + y1: number | null; + /** the minimum y value */ + y0: number | null; + /** initial y1 value, non stacked */ + initialY1: number | null; + /** initial y0 value, non stacked */ + initialY0: number | null; + /** the optional mark metric, used for lines and area series */ + mark: number | null; + /** initial datum */ + datum: T; + /** the list of filled values because missing or nulls */ + filled?: FilledValues; +} + +/** @public */ +export interface XYChartSeriesIdentifier extends SeriesIdentifier { + yAccessor: Accessor; + splitAccessors: Map; // does the map have a size vs making it optional + smVerticalAccessorValue?: string | number; + smHorizontalAccessorValue?: string | number; + seriesKeys: (string | number)[]; +} + +/** @internal */ +export type DataSeries = XYChartSeriesIdentifier & { + groupId: GroupId; + seriesType: SeriesType; + data: DataSeriesDatum[]; + isStacked: boolean; + stackMode: StackMode | undefined; + spec: Exclude; + insertIndex: number; + isFiltered: boolean; +}; + +/** @internal */ +export type DataSeriesCounts = { [key in SeriesType]: number }; + +/** @internal */ +export function getSeriesIndex(series: SeriesIdentifier[], target: SeriesIdentifier): number { + if (!series) { + return -1; + } + + return series.findIndex(({ key }) => target.key === key); +} + +/** + * Returns string form of accessor. Uses index when accessor is a function. + * @internal + */ +export function getAccessorFieldName(accessor: Accessor | AccessorFn, index: number) { + return typeof accessor === 'function' ? accessor.fieldName ?? `(index:${index})` : accessor; +} + +/** + * Split a dataset into multiple series depending on the accessors. + * Each series is then associated with a key that belongs to its configuration. + * This method removes every data with an invalid x: a string or number value is required + * `y` values and `mark` values are casted to number or null. + * @internal + */ +export function splitSeriesDataByAccessors( + spec: BasicSeriesSpec, + xValueSums: Map, + isStacked = false, + stackMode?: StackMode, + groupBySpec?: SmallMultiplesGroupBy, +): { + dataSeries: Map; + xValues: Array; +} { + const { + seriesType, + id: specId, + groupId, + data, + xAccessor, + yAccessors, + y0Accessors, + markSizeAccessor, + splitSeriesAccessors = [], + } = spec; + const dataSeries = new Map(); + const xValues: Array = []; + const nonNumericValues: any[] = []; + + for (let i = 0; i < data.length; i++) { + const datum = data[i]; + const splitAccessors = getSplitAccessors(datum, splitSeriesAccessors); + // if splitSeriesAccessors are defined we should have at least one split value to include datum + if (splitSeriesAccessors.length > 0 && splitAccessors.size < 1) { + continue; + } + + // skip if the datum is not an object or null + if (typeof datum !== 'object' || datum === null) { + continue; + } + const x = getAccessorValue(datum, xAccessor); + // skip if the x value is not a string or a number + if (typeof x !== 'string' && typeof x !== 'number') { + continue; + } + + xValues.push(x); + let sum = xValueSums.get(x) ?? 0; + + // extract small multiples aggregation values + const smH = groupBySpec?.horizontal?.by?.(spec, datum); + const smV = groupBySpec?.vertical?.by?.(spec, datum); + + yAccessors.forEach((accessor, index) => { + const cleanedDatum = extractYAndMarkFromDatum( + datum, + accessor, + nonNumericValues, + y0Accessors && y0Accessors[index], + markSizeAccessor, + ); + + const accessorStr = getAccessorFieldName(accessor, index); + const splitAccessorStrs = [...splitAccessors.values()].map((a, si) => getAccessorFieldName(a, si)); + const seriesKeys = [...splitAccessorStrs, accessorStr]; + const seriesIdentifier: Omit = { + specId, + seriesKeys, + yAccessor: accessorStr, + splitAccessors, + ...(!isNil(smV) && { smVerticalAccessorValue: smV }), + ...(!isNil(smH) && { smHorizontalAccessorValue: smH }), + }; + const seriesKey = getSeriesKey(seriesIdentifier, groupId); + sum += cleanedDatum.y1 ?? 0; + const newDatum = { x, ...cleanedDatum, smH, smV }; + const series = dataSeries.get(seriesKey); + if (series) { + series.data.push(newDatum); + } else { + dataSeries.set(seriesKey, { + ...seriesIdentifier, + groupId, + seriesType, + stackMode, + isStacked, + seriesKeys, + key: seriesKey, + data: [newDatum], + spec, + // current default to 0, will be correctly computed on a later stage + insertIndex: 0, + isFiltered: false, + }); + } + + xValueSums.set(x, sum); + }); + } + + if (nonNumericValues.length > 0) { + Logger.warn( + `Found non-numeric y value${nonNumericValues.length > 1 ? 's' : ''} in dataset for spec "${specId}"`, + `(${nonNumericValues.map((v) => JSON.stringify(v)).join(', ')})`, + ); + } + return { + dataSeries, + xValues, + }; +} + +/** + * Gets global series key to id any series as a string + * @internal + */ +export function getSeriesKey( + { + specId, + yAccessor, + splitAccessors, + smVerticalAccessorValue, + smHorizontalAccessorValue, + }: Pick< + XYChartSeriesIdentifier, + 'specId' | 'yAccessor' | 'splitAccessors' | 'smVerticalAccessorValue' | 'smHorizontalAccessorValue' + >, + groupId: GroupId, +): string { + const joinedAccessors = [...splitAccessors.entries()] + .sort(([a], [b]) => (a > b ? 1 : -1)) + .map(([key, value]) => `${key}-${value}`) + .join('|'); + const smV = smVerticalAccessorValue ? `smV{${smVerticalAccessorValue}}` : ''; + const smH = smHorizontalAccessorValue ? `smH{${smHorizontalAccessorValue}}` : ''; + return `groupId{${groupId}}spec{${specId}}yAccessor{${yAccessor}}splitAccessors{${joinedAccessors}}${smV}${smH}`; +} + +/** + * Get the array of values that forms a series key + * @internal + */ +function getSplitAccessors( + datum: Datum, + accessors: (Accessor | AccessorFn)[] = [], +): Map { + const splitAccessors = new Map(); + if (typeof datum === 'object' && datum !== null) { + accessors.forEach((accessor: Accessor | AccessorFn, index) => { + const value = getAccessorValue(datum, accessor); + if (typeof value === 'string' || typeof value === 'number') { + const accessorStr = getAccessorFieldName(accessor, index); + splitAccessors.set(accessorStr, value); + } + }); + } + return splitAccessors; +} + +/** + * Extract y1 and y0 and mark properties from Datum. Casting them to numbers or null + * @internal + */ +export function extractYAndMarkFromDatum( + datum: Datum, + yAccessor: Accessor | AccessorFn, + nonNumericValues: any[], + y0Accessor?: Accessor | AccessorFn, + markSizeAccessor?: Accessor | AccessorFn, +): Pick { + const mark = + markSizeAccessor === undefined ? null : castToNumber(getAccessorValue(datum, markSizeAccessor), nonNumericValues); + const y1Value = getAccessorValue(datum, yAccessor); + const y1 = castToNumber(y1Value, nonNumericValues); + const y0 = y0Accessor ? castToNumber(getAccessorValue(datum, y0Accessor), nonNumericValues) : null; + return { y1, datum, y0, mark, initialY0: y0, initialY1: y1 }; +} + +function castToNumber(value: any, nonNumericValues: any[]): number | null { + if (value === null || value === undefined) { + return null; + } + const num = Number(value); + + if (isNaN(num)) { + nonNumericValues.push(value); + return null; + } + return num; +} + +/** Sorts data based on order of xValues */ +const getSortedDataSeries = ( + dataSeries: DataSeries[], + xValues: Set, + xScaleType: ScaleType, +): DataSeries[] => + dataSeries.map(({ data, ...rest }) => ({ + ...rest, + data: data.sort(datumXSortPredicate(xScaleType, [...xValues.values()])), + })); + +/** @internal */ +export function getFormattedDataSeries( + seriesSpecs: SeriesSpecs, + availableDataSeries: DataSeries[], + xValues: Set, + xScaleType: ScaleType, +): DataSeries[] { + const histogramEnabled = isHistogramEnabled(seriesSpecs); + + // apply fit function to every data series + const fittedDataSeries = applyFitFunctionToDataSeries( + getSortedDataSeries(availableDataSeries, xValues, xScaleType), + seriesSpecs, + xScaleType, + ); + + // apply fitting for stacked DataSeries by YGroup, Panel + const stackedDataSeries = fittedDataSeries.filter(({ spec }) => isStackedSpec(spec, histogramEnabled)); + const stackedGroups = groupBy( + stackedDataSeries, + ['smHorizontalAccessorValue', 'smVerticalAccessorValue', 'groupId'], + true, + ); + + const fittedAndStackedDataSeries = stackedGroups.reduce((acc, dataSeries) => { + const [{ stackMode }] = dataSeries; + const formatted = formatStackedDataSeriesValues(dataSeries, xValues, stackMode); + return [...acc, ...formatted]; + }, []); + // get already fitted non stacked dataSeries + const nonStackedDataSeries = fittedDataSeries.filter(({ spec }) => !isStackedSpec(spec, histogramEnabled)); + + return [...fittedAndStackedDataSeries, ...nonStackedDataSeries]; +} + +/** @internal */ +export function getDataSeriesFromSpecs( + seriesSpecs: BasicSeriesSpec[], + deselectedDataSeries: SeriesIdentifier[] = [], + orderOrdinalBinsBy?: OrderBy, + groupBySpec?: SmallMultiplesGroupBy, +): { + dataSeries: DataSeries[]; + xValues: Set; + smVValues: Set; + smHValues: Set; + fallbackScale?: XScaleType; +} { + let globalDataSeries: DataSeries[] = []; + const mutatedXValueSums = new Map(); + + // the unique set of values along the x axis + const globalXValues: Set = new Set(); + + let isNumberArray = true; + let isOrdinalScale = false; + + const specsByYGroup = groupSeriesByYGroup(seriesSpecs); + + // eslint-disable-next-line no-restricted-syntax + for (const spec of seriesSpecs) { + // check scale type and cast to Ordinal if we found at least one series + // with Ordinal Scale + if (spec.xScaleType === ScaleType.Ordinal) { + isOrdinalScale = true; + } + + const specGroup = specsByYGroup.get(spec.groupId); + const isStacked = Boolean(specGroup?.stacked.find(({ id }) => id === spec.id)); + const { dataSeries, xValues } = splitSeriesDataByAccessors( + spec, + mutatedXValueSums, + isStacked, + specGroup?.stackMode, + groupBySpec, + ); + + // filter deselected DataSeries + let filteredDataSeries: DataSeries[] = [...dataSeries.values()]; + if (deselectedDataSeries.length > 0) { + filteredDataSeries = filteredDataSeries.map((series) => ({ + ...series, + isFiltered: deselectedDataSeries.some(({ key: deselectedKey }) => series.key === deselectedKey), + })); + } + + globalDataSeries = [...globalDataSeries, ...filteredDataSeries]; + + // check the nature of the x values. If all of them are numbers + // we can use a continuous scale, if not we should use an ordinal scale. + // The xValue is already casted to be a valid number or a string + // eslint-disable-next-line no-restricted-syntax + for (const xValue of xValues) { + if (isNumberArray && typeof xValue !== 'number') { + isNumberArray = false; + } + globalXValues.add(xValue); + } + } + + const xValues = + isOrdinalScale || !isNumberArray + ? getSortedOrdinalXValues(globalXValues, mutatedXValueSums, orderOrdinalBinsBy) + : new Set( + [...globalXValues].sort((a, b) => { + if (typeof a === 'string' || typeof b === 'string') { + return 0; + } + return a - b; + }), + ); + + const dataSeries = globalDataSeries.map((d, i) => ({ + ...d, + insertIndex: i, + })); + + const smallMultipleUniqueValues = dataSeries.reduce<{ + smVValues: Set; + smHValues: Set; + }>( + (acc, curr) => { + if (curr.isFiltered) { + return acc; + } + if (!isNil(curr.smHorizontalAccessorValue)) { + acc.smHValues.add(curr.smHorizontalAccessorValue); + } + if (!isNil(curr.smVerticalAccessorValue)) { + acc.smVValues.add(curr.smVerticalAccessorValue); + } + return acc; + }, + { smVValues: new Set(), smHValues: new Set() }, + ); + + return { + dataSeries, + // keep the user order for ordinal scales + xValues, + ...smallMultipleUniqueValues, + fallbackScale: !isOrdinalScale && !isNumberArray ? X_SCALE_DEFAULT.type : undefined, + }; +} + +/** @internal */ +export function isDataSeriesBanded({ spec }: DataSeries) { + return spec.y0Accessors && spec.y0Accessors.length > 0; +} + +function getSortedOrdinalXValues( + xValues: Set, + xValueSums: Map, + orderOrdinalBinsBy?: OrderBy, +) { + if (!orderOrdinalBinsBy) { + return xValues; // keep the user order for ordinal scales + } + + switch (orderOrdinalBinsBy?.binAgg) { + case BinAgg.None: + return xValues; // keep the user order for ordinal scales + case BinAgg.Sum: + default: + return new Set( + [...xValues].sort( + (v1, v2) => + (orderOrdinalBinsBy.direction === Direction.Ascending ? 1 : -1) * + ((xValueSums.get(v1) ?? 0) - (xValueSums.get(v2) ?? 0)), + ), + ); + } +} + +const BIG_NUMBER = Number.MAX_SAFE_INTEGER; // the sort comparator must yield finite results, can't use infinities + +function getSeriesNameFromOptions( + options: SeriesNameConfigOptions, + { yAccessor, splitAccessors }: XYChartSeriesIdentifier, + delimiter: string, +): string | null { + if (!options.names) { + return null; + } + + return ( + options.names + .slice() + .sort(({ sortIndex: a = BIG_NUMBER }, { sortIndex: b = BIG_NUMBER }) => a - b) + .map(({ accessor, value, name }) => { + const accessorValue = splitAccessors.get(accessor) ?? null; + if (accessorValue === value) { + return name ?? value; + } + + if (yAccessor === accessor) { + return name ?? accessor; + } + return null; + }) + .filter((d) => Boolean(d) || d === 0) + .join(delimiter) || null + ); +} + +/** + * Get series name based on `SeriesIdentifier` + * @internal + */ +export function getSeriesName( + seriesIdentifier: XYChartSeriesIdentifier, + hasSingleSeries: boolean, + isTooltip: boolean, + spec?: BasicSeriesSpec, +): string { + const customLabel = + typeof spec?.name === 'function' + ? spec.name(seriesIdentifier, isTooltip) + : typeof spec?.name === 'object' // extract booleans once https://github.com/microsoft/TypeScript/issues/12184 is fixed + ? getSeriesNameFromOptions(spec.name, seriesIdentifier, spec.name.delimiter ?? SERIES_DELIMITER) + : null; + + if (customLabel !== null) { + return customLabel.toString(); + } + + const multipleYAccessors = spec && spec.yAccessors.length > 1; + const nameKeys = multipleYAccessors ? seriesIdentifier.seriesKeys : seriesIdentifier.seriesKeys.slice(0, -1); + const nonZeroLength = nameKeys.length > 0; + + return nonZeroLength && (spec?.splitSeriesAccessors || !hasSingleSeries) + ? nameKeys.join(typeof spec?.name === 'object' ? spec.name.delimiter ?? SERIES_DELIMITER : SERIES_DELIMITER) + : spec === undefined + ? '' + : typeof spec.name === 'string' + ? spec.name + : spec.id; +} + +/** + * Helper function to get highest override color. + * From highest to lowest: `temporary`, `seriesSpec.color` then, unless `temporary` is set to `null`, `persisted` + */ +function getHighestOverride( + key: string, + customColors: Map, + overrides: ColorOverrides, +): Color | undefined { + const tempColor: Color | undefined | null = overrides.temporary[key]; + // Unexpected empty `tempColor` string is falsy and falls through, see comment in `export type Color = ...` + // Use default color when temporary and custom colors are null + return tempColor || customColors.get(key) || (tempColor === null ? undefined : overrides.persisted[key]); +} + +/** + * Returns color for a series given all color hierarchies + * @internal + */ +export function getSeriesColors( + dataSeries: DataSeries[], + chartColors: ColorConfig, + customColors: Map, + overrides: ColorOverrides, +): Map { + const seriesColorMap = new Map(); + let counter = 0; + const sortedDataSeries = dataSeries.slice().sort((a, b) => a.insertIndex - b.insertIndex); + groupBy( + sortedDataSeries, + (ds) => { + return [ds.specId, ds.groupId, ds.yAccessor, ...ds.splitAccessors.values()].join('__'); + }, + true, + ).forEach((ds) => { + const dsKeys = { + specId: ds[0].specId, + yAccessor: ds[0].yAccessor, + splitAccessors: ds[0].splitAccessors, + smVerticalAccessorValue: undefined, + smHorizontalAccessorValue: undefined, + }; + const seriesKey = getSeriesKey(dsKeys, ds[0].groupId); + const colorOverride = getHighestOverride(seriesKey, customColors, overrides); + const color = colorOverride || chartColors.vizColors[counter % chartColors.vizColors.length]; + + seriesColorMap.set(seriesKey, color); + counter++; + }); + return seriesColorMap; +} + +/** @internal */ +export function getSeriesIdentifierFromDataSeries(dataSeries: DataSeries): XYChartSeriesIdentifier { + const { + yAccessor, + splitAccessors, + smVerticalAccessorValue, + smHorizontalAccessorValue, + seriesKeys, + specId, + key, + } = dataSeries; + return { + yAccessor, + splitAccessors, + smVerticalAccessorValue, + smHorizontalAccessorValue, + seriesKeys, + specId, + key, + }; +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/specs.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/specs.ts new file mode 100644 index 000000000000..12ca9dcf328f --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/specs.ts @@ -0,0 +1,989 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ReactNode } from 'react'; +import { $Values } from 'utility-types'; + +import { ChartType } from '../..'; +import { TooltipPortalSettings } from '../../../components/portal/types'; +import { ScaleContinuousType } from '../../../scales'; +import { ScaleType } from '../../../scales/constants'; +import { LogScaleOptions } from '../../../scales/scale_continuous'; +import { Spec } from '../../../specs'; +import { SpecType } from '../../../specs/constants'; +import { Accessor, AccessorFormat, AccessorFn } from '../../../utils/accessor'; +import { RecursivePartial, Color, Position, Datum } from '../../../utils/common'; +import { CurveType } from '../../../utils/curves'; +import { OrdinalDomain } from '../../../utils/domain'; +import { AxisId, GroupId } from '../../../utils/ids'; +import { + AreaSeriesStyle, + BarSeriesStyle, + GridLineStyle, + LineAnnotationStyle, + LineSeriesStyle, + PointStyle, + RectAnnotationStyle, + BubbleSeriesStyle, + AxisStyle, +} from '../../../utils/themes/theme'; +import { PrimitiveValue } from '../../partition_chart/layout/utils/group_by_rollup'; +import { + AnnotationTooltipFormatter, + ComponentWithAnnotationDatum, + CustomAnnotationTooltip, +} from '../annotations/types'; +import { XYChartSeriesIdentifier, DataSeriesDatum } from './series'; + +/** @public */ +export type BarStyleOverride = RecursivePartial | Color | null; +/** @public */ +export type PointStyleOverride = RecursivePartial | Color | null; + +/** @public */ +export const SeriesType = Object.freeze({ + Area: 'area' as const, + Bar: 'bar' as const, + Line: 'line' as const, + Bubble: 'bubble' as const, +}); + +/** + * XY series type + * @public + */ +export type SeriesType = $Values; + +/** + * The offset and mode applied when stacking values + * @public + */ +export const StackMode = Object.freeze({ + /** Applies a zero baseline and normalizes the values for each point such that the topline is always one. */ + Percentage: 'percentage' as const, + /** Shifts the baseline so as to minimize the weighted wiggle of layers. */ + Wiggle: 'wiggle' as const, + /** Shifts the baseline down such that the center of the streamgraph is always at zero. */ + Silhouette: 'silhouette' as const, +}); + +/** + * The offset and mode applied when stacking values + * @public + */ +export type StackMode = $Values; + +/** + * Override for bar styles per datum + * + * Return types: + * - `Color`: Color value as a `string` will set the bar `fill` to that color + * - `RecursivePartial`: Style values to be merged with base bar styles + * - `null`: Keep existing bar style + * @public + */ +export type BarStyleAccessor = (datum: DataSeriesDatum, seriesIdentifier: XYChartSeriesIdentifier) => BarStyleOverride; +/** + * Override for bar styles per datum + * + * Return types: + * - `Color`: Color value as a `string` will set the point `stroke` to that color + * - `RecursivePartial`: Style values to be merged with base point styles + * - `null`: Keep existing point style + * @public + */ +export type PointStyleAccessor = ( + datum: DataSeriesDatum, + seriesIdentifier: XYChartSeriesIdentifier, +) => PointStyleOverride; + +/** + * The global id used by default to group together series + * @public + */ +export const DEFAULT_GLOBAL_ID = '__global__'; + +/** @public */ +export type FilterPredicate = (series: XYChartSeriesIdentifier) => boolean; +/** @public */ +export type SeriesName = string | number | null; +/** + * Function to create custom series name for a given series + * @public + */ +export type SeriesNameFn = (series: XYChartSeriesIdentifier, isTooltip: boolean) => SeriesName; +/** + * Accessor mapping to replace names + * @public + */ +export interface SeriesNameConfig { + /** + * accessor key (i.e. `yAccessors` and `seriesSplitAccessors`) + */ + accessor: string | number; + /** + * Accessor value (i.e. values from `seriesSplitAccessors`) + */ + value?: string | number; + /** + * New name for Accessor value + * + * If not provided, the original value will be used + */ + name?: string | number; + /** + * Sort order of name, overrides order listed in array. + * + * lower values - left-most + * higher values - right-most + */ + sortIndex?: number; +} +/** @public */ +export interface SeriesNameConfigOptions { + /** + * Array of accessor naming configs to replace series names + * + * Only provided configs will be included + * (i.e. if you only provide a single mapping for `yAccessor`, all other series accessor names will be ignored) + * + * The order of configs is the order in which the resulting names will + * be joined, if no `sortIndex` is specified. + * + * If no values are found for a giving mapping in a series, the mapping will be ignored. + */ + names?: SeriesNameConfig[]; + /** + * Delimiter to join values/names + * + * @defaultValue an hyphen with spaces ` - ` + */ + delimiter?: string; +} +/** @public */ +export type SeriesNameAccessor = string | SeriesNameFn | SeriesNameConfigOptions; + +/** + * The fit function type + * @public + */ +export const Fit = Object.freeze({ + /** + * Don't draw value on the graph. Slices out area between `null` values. + * + * @example + * ```js + * [2, null, null, 8] => [2, null null, 8] + * ``` + */ + None: 'none' as const, + /** + * Use the previous non-`null` value + * + * @remarks + * This is the opposite of `Fit.Lookahead` + * + * @example + * ```js + * [2, null, null, 8] => [2, 2, 2, 8] + * ``` + */ + Carry: 'carry' as const, + /** + * Use the next non-`null` value + * + * @remarks + * This is the opposite of `Fit.Carry` + * + * @example + * ```js + * [2, null, null, 8] => [2, 8, 8, 8] + * ``` + */ + Lookahead: 'lookahead' as const, + /** + * Use the closest non-`null` value (before or after) + * + * @example + * ```js + * [2, null, null, 8] => [2, 2, 8, 8] + * ``` + */ + Nearest: 'nearest' as const, + /** + * Average between the closest non-`null` values + * + * @example + * ```js + * [2, null, null, 8] => [2, 5, 5, 8] + * ``` + */ + Average: 'average' as const, + /** + * Linear interpolation between the closest non-`null` values + * + * @example + * ```js + * [2, null, null, 8] => [2, 4, 6, 8] + * ``` + */ + Linear: 'linear' as const, + /** + * Sets all `null` values to `0` + * + * @example + * ```js + * [2, null, null, 8] => [2, 0, 0, 8] + * ``` + */ + Zero: 'zero' as const, + /** + * Specify an explicit value `X` + * + * @example + * ```js + * [2, null, null, 8] => [2, X, X, 8] + * ``` + */ + Explicit: 'explicit' as const, +}); + +/** @public */ +export type Fit = $Values; + +interface DomainBase { + /** + * Custom minInterval for the domain which will affect data bucket size. + * The minInterval cannot be greater than the computed minimum interval between any two adjacent data points. + * Further, if you specify a custom numeric minInterval for a time-series, please note that due to the restriction + * above, the specified numeric minInterval will be interpreted as a fixed interval. + * This means that, for example, if you have yearly time-series data that ranges from 2016 to 2019 and you manually + * compute the interval between 2016 and 2017, you'll have 366 days due to 2016 being a leap year. This will not + * be a valid interval because it is greater than the computed minInterval of 365 days between the other years. + */ + minInterval?: number; +} + +/** + * Padding unit for domain + * @public + */ +export const DomainPaddingUnit = Object.freeze({ + /** + * Raw value in the domain space. + * + * Example: + * + * If your domain is `[20, 40]` and your padding value is `10`. + * The resulting domain would be `[10, 50]` + */ + Domain: 'domain' as const, + /** + * Spatial pixel value (aka screenspace) not dependent on domain. + * + * @alpha + */ + Pixel: 'pixel' as const, + /** + * Ratio of total domain relative to domain space + * + * Example: + * + * If your domain is `[20, 40]` and your padding value is `0.1`. + * The resulting padding would be 2 (i.e. `0.1 * (40 - 20)`) + * resulting in a domain of `[18, 42]` + */ + DomainRatio: 'domainRatio' as const, +}); +/** + * Padding unit + * @public + */ +export type DomainPaddingUnit = $Values; + +/** + * Domain option that **only** apply to `yDomains`. + * @public + */ +export interface YDomainBase { + /** + * Whether to fit the domain to the data. + * + * Setting `max` or `min` will override this functionality. + * @defaultValue false + */ + fit?: boolean; + /** + * Padding for computed domain as positive number. + * Applied to domain __before__ nicing + * + * Setting `max` or `min` will override this functionality. + */ + padding?: number; + /** + * Unit of padding dimension + * + * @defaultValue 'domain' + */ + paddingUnit?: DomainPaddingUnit; + /** + * Constrains padded domain to the zero baseline. + * + * e.g. If your domain is `[10, 100]` and `[-10, 120]` with padding. + * The domain would be `[0, 120]` if **constrained** or `[-10, 120]` if **unconstrained**. + * + * @defaultValue true + */ + constrainPadding?: boolean; +} + +interface LowerBound { + /** + * Lower bound of domain range + */ + min: number; +} + +interface UpperBound { + /** + * Upper bound of domain range + */ + max: number; +} + +/** @public */ +export type LowerBoundedDomain = DomainBase & LowerBound; +/** @public */ +export type UpperBoundedDomain = DomainBase & UpperBound; +/** @public */ +export type CompleteBoundedDomain = DomainBase & LowerBound & UpperBound; +/** @public */ +export type UnboundedDomainWithInterval = DomainBase; + +/** @public */ +export type DomainRange = LowerBoundedDomain | UpperBoundedDomain | CompleteBoundedDomain | UnboundedDomainWithInterval; +/** @public */ +export type YDomainRange = YDomainBase & DomainRange & LogScaleOptions; + +/** @public */ +export type CustomXDomain = (DomainRange & Pick) | OrdinalDomain; + +/** @public */ +export interface DisplayValueSpec { + /** Show value label in chart element */ + showValueLabel?: boolean; + /** If value labels are shown, skips every other label */ + isAlternatingValueLabel?: boolean; + /** Function for formatting values; will use axis tickFormatter if none specified */ + valueFormatter?: TickFormatter; + /** If true will contain value label within element, else dimensions are computed based on value */ + isValueContainedInElement?: boolean; + /** If true will hide values that are clipped at chart edges */ + hideClippedValue?: boolean; +} + +/** @public */ +export interface SeriesSpec extends Spec { + specType: typeof SpecType.Series; + chartType: typeof ChartType.XYAxis; + /** + * The name of the spec. Also a mechanism to provide custom series names. + */ + name?: SeriesNameAccessor; + /** + * The ID of the spec group + * @defaultValue {@link DEFAULT_GLOBAL_ID} + */ + groupId: string; + /** + * When specify a groupId on this series, this option can be used to compute this series domain as it was part + * of the default group (when using the boolean value true) + * or as the series was part of the specified group (when issuing a string) + */ + useDefaultGroupDomain?: boolean | string; + /** An array of data */ + data: Datum[]; + /** The type of series you are looking to render */ + seriesType: SeriesType; + /** Set colors for specific series */ + color?: SeriesColorAccessor; + /** + * If the series should appear in the legend + * @defaultValue `false` + */ + hideInLegend?: boolean; + /** + * Index per series to sort by + * @deprecated This prop is not currently used and will + * soon be removed. + */ + sortIndex?: number; + displayValueSettings?: DisplayValueSpec; + /** + * Postfix string or accessor function for y1 accessor when using `y0Accessors` + * + * @defaultValue ` - upper` + */ + y0AccessorFormat?: AccessorFormat; + /** + * Postfix string or accessor function for y1 accessor when using `y0Accessors` + * + * @defaultValue ` - lower` + */ + y1AccessorFormat?: AccessorFormat; + /** + * Hide series in tooltip + */ + filterSeriesInTooltip?: FilterPredicate; + /** + * A function called to format every value label. + * Uses axis `tickFormat` when not provided. + */ + tickFormat?: TickFormatter; +} + +/** @public */ +export interface Postfixes { + /** + * Postfix for y1 accessor when using `y0Accessors` + * + * @defaultValue `upper` + */ + y0AccessorFormat?: string; + /** + * Postfix for y1 accessor when using `y0Accessors` + * + * @defaultValue `lower` + */ + y1AccessorFormat?: string; +} + +/** @public */ +export type SeriesColorsArray = string[]; +/** @public */ +export type SeriesColorAccessorFn = (seriesIdentifier: XYChartSeriesIdentifier) => string | null; +/** @public */ +export type SeriesColorAccessor = string | SeriesColorsArray | SeriesColorAccessorFn; + +/** @public */ +export interface SeriesAccessors { + /** The field name of the x value on Datum object */ + xAccessor: Accessor | AccessorFn; + /** An array of field names one per y metric value */ + yAccessors: (Accessor | AccessorFn)[]; + /** An optional accessor of the y0 value: base point for area/bar charts */ + y0Accessors?: (Accessor | AccessorFn)[]; + /** An array of fields thats indicates the datum series membership */ + splitSeriesAccessors?: (Accessor | AccessorFn)[]; + /** An array of fields thats indicates the stack membership */ + stackAccessors?: (Accessor | AccessorFn)[]; + /** + * Field name of mark size metric on `Datum` + * + * Only used with line/area series + */ + markSizeAccessor?: Accessor | AccessorFn; +} + +/** @public */ +export type XScaleType = typeof ScaleType.Ordinal | ScaleContinuousType; + +/** @public */ +export interface SeriesScales { + /** + * The x axis scale type + * @defaultValue `ordinal` {@link (ScaleType:type) | ScaleType.Ordinal} + */ + xScaleType: XScaleType; + /** + * Extends the x domain so that it starts and ends on nice round values. + * @defaultValue `false` + */ + xNice?: boolean; + /** + * If using a ScaleType.Time this timezone identifier is required to + * compute a nice set of xScale ticks. Can be any IANA zone supported by + * the host environment, or a fixed-offset name of the form 'utc+3', + * or the strings 'local' or 'utc'. + */ + timeZone?: string; + /** + * The y axis scale type + * @defaultValue `linear` {@link (ScaleType:type) | ScaleType.Linear} + */ + yScaleType: ScaleContinuousType; + /** + * Extends the y domain so that it starts and ends on nice round values. + * @defaultValue `false` + */ + yNice?: boolean; +} + +/** @public */ + +export type BasicSeriesSpec = SeriesSpec & + SeriesAccessors & + SeriesScales & { + /** + * A function called to format every single mark value + * + * Only used with line/area series + */ + markFormat?: TickFormatter; + }; + +/** @public */ +export type SeriesSpecs = Array; + +/** + * This spec describe the dataset configuration used to display a bar series. + * @public + */ +export type BarSeriesSpec = BasicSeriesSpec & + Postfixes & { + /** @defaultValue `bar` {@link (SeriesType:type) | SeriesType.Bar} */ + seriesType: typeof SeriesType.Bar; + /** If true, will stack all BarSeries and align bars to ticks (instead of centered on ticks) */ + enableHistogramMode?: boolean; + barSeriesStyle?: RecursivePartial; + /** + * Stack each series using a specific mode: Percentage, Wiggle, Silhouette. + * The last two modes are generally used for stream graphs + */ + stackMode?: StackMode; + /** + * Functional accessor to return custom color or style for bar datum + */ + styleAccessor?: BarStyleAccessor; + /** + * Min height to render bars for highly variable data + * + * @remarks + * i.e. ranges from 100,000 to 1. + * + * The unit is expressed in `pixel` + */ + minBarHeight?: number; + }; + +/** + * This spec describe the dataset configuration used to display a histogram bar series. + * A histogram bar series is identical to a bar series except that stackAccessors are not allowed. + * @public + */ +export type HistogramBarSeriesSpec = Omit & { + enableHistogramMode: true; +}; + +/** @public */ +export type FitConfig = { + /** + * Fit type for data with null values + */ + type: Fit; + /** + * Fit value used when `type` is set to `Fit.Explicit` + */ + value?: number; + /** + * Value used for first and last point if fitting is not possible + * + * `'nearest'` will set indeterminate end values to the closes _visible_ point. + * + * Note: Computed fit values will always take precedence over `endValues` + */ + endValue?: number | 'nearest'; +}; + +/** + * This spec describe the dataset configuration used to display a line series. + * @public + */ +export type LineSeriesSpec = BasicSeriesSpec & + HistogramConfig & { + /** @defaultValue `line` {@link (SeriesType:type) | SeriesType.Line} */ + seriesType: typeof SeriesType.Line; + curve?: CurveType; + lineSeriesStyle?: RecursivePartial; + /** + * An optional functional accessor to return custom color or style for point datum + */ + pointStyleAccessor?: PointStyleAccessor; + /** + * Fit config to fill `null` values in dataset + */ + fit?: Exclude | FitConfig; + }; + +/** + * This spec describe the dataset configuration used to display a line series. + * + * @alpha + */ +export type BubbleSeriesSpec = BasicSeriesSpec & { + /** @defaultValue `bubble` {@link (SeriesType:type) | SeriesType.Bubble} */ + seriesType: typeof SeriesType.Bubble; + bubbleSeriesStyle?: RecursivePartial; + /** + * An optional functional accessor to return custom color or style for point datum + */ + pointStyleAccessor?: PointStyleAccessor; +}; + +/** + * This spec describe the dataset configuration used to display an area series. + * @public + */ +export type AreaSeriesSpec = BasicSeriesSpec & + HistogramConfig & + Postfixes & { + /** @defaultValue `area` {@link (SeriesType:type) | SeriesType.Area} */ + seriesType: typeof SeriesType.Area; + /** The type of interpolator to be used to interpolate values between points */ + curve?: CurveType; + areaSeriesStyle?: RecursivePartial; + /** + * Stack each series using a specific mode: Percentage, Wiggle, Silhouette. + * The last two modes are generally used for stream graphs + */ + stackMode?: StackMode; + /** + * An optional functional accessor to return custom color or style for point datum + */ + pointStyleAccessor?: PointStyleAccessor; + /** + * Fit config to fill `null` values in dataset + */ + fit?: Exclude | FitConfig; + }; + +/** @public */ +export interface HistogramConfig { + /** + * Determines how points in the series will align to bands in histogram mode + * @defaultValue `start` + */ + histogramModeAlignment?: HistogramModeAlignment; +} + +/** @public */ +export const HistogramModeAlignments = Object.freeze({ + Start: 'start' as HistogramModeAlignment, + Center: 'center' as HistogramModeAlignment, + End: 'end' as HistogramModeAlignment, +}); + +/** @public */ +export type HistogramModeAlignment = 'start' | 'center' | 'end'; + +/** + * This spec describe the configuration for a chart axis. + * @public + */ +export interface AxisSpec extends Spec { + specType: typeof SpecType.Axis; + chartType: typeof ChartType.XYAxis; + /** The ID of the spec */ + id: AxisId; + /** Style options for grid line */ + gridLine?: Partial; + /** + * The ID of the axis group + * @defaultValue {@link DEFAULT_GLOBAL_ID} + */ + groupId: GroupId; + /** Hide this axis */ + hide: boolean; + /** shows all ticks, also the one from the overlapping labels */ + showOverlappingTicks: boolean; + /** Shows all labels, also the overlapping ones */ + showOverlappingLabels: boolean; + /** + * Shows grid lines for axis + * @defaultValue `false` + * @deprecated use `gridLine.visible` + */ + showGridLines?: boolean; + /** Where the axis appear on the chart */ + position: Position; + /** + * A function called to format every tick value label. + * Uses first series spec `tickFormat` when not provided. + * + * used in tooltip when no `tickFormat` is provided from series spec + */ + tickFormat?: TickFormatter; + /** + * A function called to format every label (excludes tooltip) + * + * overrides tickFormat for axis labels + */ + labelFormat?: TickFormatter; + /** An approximate count of how many ticks will be generated */ + ticks?: number; + /** The axis title */ + title?: string; + /** Custom style overrides */ + style?: RecursivePartial>; + /** If specified, it constrains the domain for these values */ + domain?: YDomainRange; + /** Show only integar values * */ + integersOnly?: boolean; + /** + * Show duplicated ticks + * @defaultValue `false` + */ + showDuplicatedTicks?: boolean; +} + +/** @public */ +export type TickFormatterOptions = { + timeZone?: string; +}; + +/** @public */ +export type TickFormatter = (value: V, options?: TickFormatterOptions) => string; + +/** @public */ +export const AnnotationType = Object.freeze({ + Line: 'line' as const, + Rectangle: 'rectangle' as const, + Text: 'text' as const, +}); +/** @public */ +export type AnnotationType = $Values; + +/** + * The domain type enum that can be associated with an annotation + * @public + */ +export const AnnotationDomainType = Object.freeze({ + XDomain: 'xDomain' as const, + YDomain: 'yDomain' as const, +}); + +/** + * The domain type that can be associated with an annotation + * @public + */ +export type AnnotationDomainType = $Values; + +/** + * The descriptive object of a line annotation + * @public + */ +export interface LineAnnotationDatum { + /** + * The value on the x or y axis accordingly to the domainType configured + */ + dataValue: any; + /** + * A textual description of the annotation + */ + details?: string; + /** + * An header of the annotation. If undefined, than the formatted dataValue will be used + */ + header?: string; +} + +/** @public */ +export type LineAnnotationSpec = BaseAnnotationSpec< + typeof AnnotationType.Line, + LineAnnotationDatum, + LineAnnotationStyle +> & { + domainType: AnnotationDomainType; + /** Optional Custom marker icon centered on data value */ + marker?: ReactNode | ComponentWithAnnotationDatum; + /** Optional marker body, always contained within chart area */ + markerBody?: ReactNode | ComponentWithAnnotationDatum; + /** + * Custom marker dimensions; will be computed internally + * Any user-supplied values will be overwritten + */ + markerDimensions?: { + width: number; + height: number; + }; + /** + * An optional marker position. + * + * @remarks + * The default position, if this property is not specified, falls back to the linked axis position (if available). + * If no axis present on the chart, the marker position is positioned by default on the bottom on the X domain + * and on the left of the chart for the Y domain. The specified position is an absolute position and reflect + * the spatial position of the marker independently from the chart rotation. + */ + markerPosition?: Position; + /** Annotation lines are hidden */ + hideLines?: boolean; + /** + * Hide tooltip when hovering over the line + * @defaultValue `true` + */ + hideLinesTooltips?: boolean; + /** + * z-index of the annotation relative to other elements in the chart + * @defaultValue 1 + */ + zIndex?: number; +}; + +/** + * The descriptive object of a rectangular annotation + * @public + */ +export interface RectAnnotationDatum { + /** + * The coordinates for the 4 rectangle points. + */ + coordinates: { + /** + * The minuimum value on the x axis. If undefined, the minuimum value of the x domain will be used. + */ + x0?: PrimitiveValue; + /** + * The maximum value on the x axis. If undefined, the maximum value of the x domain will be used. + */ + x1?: PrimitiveValue; + /** + * The minimum value on the y axis. If undefined, the minimum value of the y domain will be used. + */ + y0?: PrimitiveValue; + /** + * The maximum value on the y axis. If undefined, the maximum value of the y domain will be used. + */ + y1?: PrimitiveValue; + }; + /** + * A textual description of the annotation + */ + details?: string; +} + +/** @public */ +export type RectAnnotationSpec = BaseAnnotationSpec< + typeof AnnotationType.Rectangle, + RectAnnotationDatum, + RectAnnotationStyle +> & { + /** + * @deprecated use customTooltipDetails + */ + renderTooltip?: AnnotationTooltipFormatter; + /** + * z-index of the annotation relative to other elements in the chart + * @defaultValue -1 + */ + zIndex?: number; +}; + +/** + * Portal settings for annotation tooltips + * + * @public + */ +export type AnnotationPortalSettings = TooltipPortalSettings<'chart'> & { + /** + * The react component used to render a custom tooltip + * @public + */ + customTooltip?: CustomAnnotationTooltip; + /** + * The react component used to render a custom tooltip details + * @public + */ + customTooltipDetails?: AnnotationTooltipFormatter; +}; + +/** @public */ +export interface BaseAnnotationSpec< + T extends typeof AnnotationType.Rectangle | typeof AnnotationType.Line, + D extends RectAnnotationDatum | LineAnnotationDatum, + S extends RectAnnotationStyle | LineAnnotationStyle +> extends Spec, + AnnotationPortalSettings { + chartType: typeof ChartType.XYAxis; + specType: typeof SpecType.Annotation; + /** + * Annotation type: line, rectangle + */ + annotationType: T; + /** + * The ID of the axis group, needed for yDomain position + * @defaultValue {@link DEFAULT_GLOBAL_ID} + */ + groupId: GroupId; + /** + * Data values defined with coordinates and details + */ + dataValues: D[]; + /** + * Custom annotation style + */ + style?: Partial; + /** + * Toggles tooltip annotation visibility + */ + hideTooltips?: boolean; + /** + * z-index of the annotation relative to other elements in the chart + * Default specified per specific annotation spec. + */ + zIndex?: number; +} + +/** @public */ +export type AnnotationSpec = LineAnnotationSpec | RectAnnotationSpec; + +/** @internal */ +export function isLineAnnotation(spec: AnnotationSpec): spec is LineAnnotationSpec { + return spec.annotationType === AnnotationType.Line; +} + +/** @internal */ +export function isRectAnnotation(spec: AnnotationSpec): spec is RectAnnotationSpec { + return spec.annotationType === AnnotationType.Rectangle; +} + +/** @internal */ +export function isBarSeriesSpec(spec: BasicSeriesSpec): spec is BarSeriesSpec { + return spec.seriesType === SeriesType.Bar; +} + +/** @internal */ +export function isBubbleSeriesSpec(spec: BasicSeriesSpec): spec is BubbleSeriesSpec { + return spec.seriesType === SeriesType.Bubble; +} + +/** @internal */ +export function isLineSeriesSpec(spec: BasicSeriesSpec): spec is LineSeriesSpec { + return spec.seriesType === SeriesType.Line; +} + +/** @internal */ +export function isAreaSeriesSpec(spec: BasicSeriesSpec): spec is AreaSeriesSpec { + return spec.seriesType === SeriesType.Area; +} + +/** @internal */ +export function isBandedSpec(y0Accessors: SeriesAccessors['y0Accessors']): boolean { + return Boolean(y0Accessors && y0Accessors.length > 0); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts new file mode 100644 index 000000000000..aa8951d88f31 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts @@ -0,0 +1,268 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MockSeriesSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store'; +import { ScaleType } from '../../../scales/constants'; +import { computeSeriesDomainsSelector } from '../state/selectors/compute_series_domains'; +import { StackMode } from './specs'; + +describe('Stacked Series Utils', () => { + const STANDARD_DATA_SET = [ + { x: 0, y1: 10, g: 'a' }, + { x: 0, y1: 20, g: 'b' }, + { x: 0, y1: 70, g: 'c' }, + ]; + + const WITH_NULL_DATASET = [ + { x: 0, y1: 10, g: 'a' }, + { x: 0, y1: null, g: 'b' }, + { x: 0, y1: 30, g: 'c' }, + ]; + const STANDARD_DATA_SET_WY0 = [ + { x: 0, y0: 2, y1: 10, g: 'a' }, + { x: 0, y0: 4, y1: 20, g: 'b' }, + { x: 0, y0: 6, y1: 70, g: 'c' }, + ]; + const WITH_NULL_DATASET_WY0 = [ + { x: 0, y0: 2, y1: 10, g: 'a' }, + { x: 0, y1: null, g: 'b' }, + { x: 0, y0: 6, y1: 90, mark: null, g: 'c' }, + ]; + const DATA_SET_WITH_NULL_2 = [ + { x: 1, y1: 10, g: 'a' }, + { x: 2, y1: 20, g: 'a' }, + { x: 4, y1: 40, g: 'a' }, + { x: 1, y1: 90, g: 'b' }, + { x: 3, y1: 30, g: 'b' }, + ]; + + describe('Format stacked dataset', () => { + test('format data without nulls', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockSeriesSpec.area({ + xAccessor: 'x', + yAccessors: ['y1'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + stackMode: StackMode.Percentage, + data: STANDARD_DATA_SET, + }), + ], + store, + ); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + const [data0] = formattedDataSeries[0].data; + expect(data0.initialY1).toBe(10); + expect(data0.y0).toBe(0); + expect(data0.y1).toBe(0.1); + + const [data1] = formattedDataSeries[1].data; + expect(data1.initialY1).toBe(20); + expect(data1.y0).toBe(0.1); + expect(data1.y1).toBeCloseTo(0.3); + + const [data2] = formattedDataSeries[2].data; + expect(data2.initialY1).toBe(70); + expect(data2.y0).toBeCloseTo(0.3); + expect(data2.y1).toBe(1); + }); + test('format data with nulls', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockSeriesSpec.area({ + yAccessors: ['y1'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + stackMode: StackMode.Percentage, + data: WITH_NULL_DATASET, + }), + ], + store, + ); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + const [data0] = formattedDataSeries[0].data; + expect(data0.initialY1).toBe(10); + expect(data0.y0).toBe(0); + expect(data0.y1).toBe(0.25); + + expect(formattedDataSeries[1].data[0]).toMatchObject({ + initialY0: null, + initialY1: null, + x: 0, + y1: 0.25, + y0: 0.25, + mark: null, + }); + + const [data2] = formattedDataSeries[2].data; + expect(data2.initialY1).toBe(30); + expect(data2.y0).toBe(0.25); + expect(data2.y1).toBe(1); + }); + test('format data without nulls with y0 values', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockSeriesSpec.area({ + yAccessors: ['y1'], + y0Accessors: ['y0'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + stackMode: StackMode.Percentage, + data: STANDARD_DATA_SET_WY0, + }), + ], + store, + ); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + const [data0] = formattedDataSeries[0].data; + expect(data0.initialY0).toBe(2); + expect(data0.initialY1).toBe(10); + expect(data0.y0).toBe(0.02); + expect(data0.y1).toBe(0.1); + + const [data1] = formattedDataSeries[1].data; + expect(data1.initialY0).toBe(4); + expect(data1.initialY1).toBe(20); + expect(data1.y0).toBe(0.14); + expect(data1.y1).toBeCloseTo(0.3, 5); + + const [data2] = formattedDataSeries[2].data; + expect(data2.initialY0).toBe(6); + expect(data2.initialY1).toBe(70); + expect(data2.y0).toBeCloseTo(0.36); + expect(data2.y1).toBe(1); + }); + test('format data with nulls - missing points', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockSeriesSpec.area({ + yAccessors: ['y1'], + y0Accessors: ['y0'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + stackMode: StackMode.Percentage, + data: WITH_NULL_DATASET_WY0, + }), + ], + store, + ); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + const [data0] = formattedDataSeries[0].data; + expect(data0.initialY0).toBe(2); + expect(data0.initialY1).toBe(10); + expect(data0.y0).toBe(0.02); + expect(data0.y1).toBe(0.1); + + const [data1] = formattedDataSeries[1].data; + expect(data1.initialY0).toBe(null); + expect(data1.initialY1).toBe(null); + expect(data1.y0).toBe(0.1); + expect(data1.y1).toBe(0.1); + + const [data2] = formattedDataSeries[2].data; + expect(data2.initialY0).toBe(6); + expect(data2.initialY1).toBe(90); + expect(data2.y0).toBe(0.16); + expect(data2.y1).toBe(1); + }); + test('format data without nulls on second series', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockSeriesSpec.area({ + xScaleType: ScaleType.Linear, + yAccessors: ['y1'], + y0Accessors: ['y0'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + stackMode: StackMode.Percentage, + data: DATA_SET_WITH_NULL_2, + }), + ], + store, + ); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + expect(formattedDataSeries).toHaveLength(2); + expect(formattedDataSeries[0].data).toHaveLength(4); + expect(formattedDataSeries[1].data).toHaveLength(4); + expect(formattedDataSeries[0].data[0]).toMatchObject({ + initialY0: null, + initialY1: 10, + x: 1, + y0: 0, + y1: 0.1, + mark: null, + }); + expect(formattedDataSeries[0].data[1]).toMatchObject({ + initialY0: null, + initialY1: 20, + x: 2, + y0: 0, + y1: 1, + mark: null, + }); + expect(formattedDataSeries[0].data[3]).toMatchObject({ + initialY0: null, + initialY1: 40, + x: 4, + y0: 0, + y1: 1, + mark: null, + }); + expect(formattedDataSeries[1].data[0]).toMatchObject({ + initialY0: null, + initialY1: 90, + x: 1, + y0: 0.1, + y1: 1, + mark: null, + }); + expect(formattedDataSeries[1].data[1]).toMatchObject({ + initialY0: null, + initialY1: null, + x: 2, + y0: 1, + y1: 1, + mark: null, + filled: { + x: 2, + }, + }); + expect(formattedDataSeries[1].data[2]).toMatchObject({ + initialY0: null, + initialY1: 30, + x: 3, + y0: 0, + y1: 1, + mark: null, + }); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/stacked_series_utils.test.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/stacked_series_utils.test.ts new file mode 100644 index 000000000000..02a1d871abd1 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/stacked_series_utils.test.ts @@ -0,0 +1,360 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MockSeriesSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store'; +import { ScaleType } from '../../../scales/constants'; +import { computeSeriesDomainsSelector } from '../state/selectors/compute_series_domains'; +import { StackMode } from './specs'; + +describe('Stacked Series Utils', () => { + const EMPTY_DATA_SET = MockSeriesSpec.area({ + xScaleType: ScaleType.Linear, + yAccessors: ['y1'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + data: [], + }); + const STANDARD_DATA_SET = MockSeriesSpec.area({ + xScaleType: ScaleType.Linear, + yAccessors: ['y1'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + data: [ + { x: 0, y1: 10, g: 'a' }, + { x: 0, y1: 20, g: 'b' }, + { x: 0, y1: 30, g: 'c' }, + ], + }); + const WITH_NULL_DATASET = MockSeriesSpec.area({ + xScaleType: ScaleType.Linear, + yAccessors: ['y1'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + data: [ + { x: 0, y1: 10, g: 'a' }, + { x: 0, y1: null, g: 'b' }, + { x: 0, y1: 30, g: 'c' }, + ], + }); + + const STANDARD_DATA_SET_WY0 = MockSeriesSpec.area({ + xScaleType: ScaleType.Linear, + yAccessors: ['y1'], + y0Accessors: ['y0'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + data: [ + { x: 0, y0: 2, y1: 10, g: 'a' }, + { x: 0, y0: 4, y1: 20, g: 'b' }, + { x: 0, y0: 6, y1: 30, g: 'c' }, + ], + }); + const WITH_NULL_DATASET_WY0 = MockSeriesSpec.area({ + xScaleType: ScaleType.Linear, + yAccessors: ['y1'], + y0Accessors: ['y0'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + data: [ + { x: 0, y0: 2, y1: 10, g: 'a' }, + { x: 0, y1: null, g: 'b' }, + { x: 0, y0: 6, y1: 30, g: 'c' }, + ], + }); + + const DATA_SET_WITH_NULL_2 = MockSeriesSpec.area({ + xScaleType: ScaleType.Linear, + yAccessors: ['y1'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + data: [ + { x: 1, y1: 1, g: 'a' }, + { x: 2, y1: 2, g: 'a' }, + { x: 4, y1: 4, g: 'a' }, + { x: 1, y1: 21, g: 'b' }, + { x: 3, y1: 23, g: 'b' }, + ], + }); + + describe('compute stacked arrays', () => { + test('with empty values', () => { + const store = MockStore.default(); + MockStore.addSpecs(EMPTY_DATA_SET, store); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + expect(formattedDataSeries).toHaveLength(0); + }); + test('with basic values', () => { + const store = MockStore.default(); + MockStore.addSpecs(STANDARD_DATA_SET, store); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + // stacked series are reverse ordered + const values = [ + formattedDataSeries[0].data[0].y0, + formattedDataSeries[0].data[0].y1, + formattedDataSeries[1].data[0].y1, + formattedDataSeries[2].data[0].y1, + ]; + expect(values).toEqual([0, 10, 30, 60]); + }); + + test('with basic values in percentage', () => { + const store = MockStore.default(); + MockStore.addSpecs( + MockSeriesSpec.area({ + ...STANDARD_DATA_SET, + stackMode: StackMode.Percentage, + }), + store, + ); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + const values = [ + formattedDataSeries[0].data[0].y0, + formattedDataSeries[0].data[0].y1, + formattedDataSeries[1].data[0].y1, + formattedDataSeries[2].data[0].y1, + ]; + expect(values).toEqual([0, 0.16666666666666666, 0.5, 1]); + }); + test('with null values', () => { + const store = MockStore.default(); + MockStore.addSpecs(WITH_NULL_DATASET, store); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + const values = [ + formattedDataSeries[0].data[0].y0, + formattedDataSeries[0].data[0].y1, + formattedDataSeries[1].data[0].y1, + formattedDataSeries[2].data[0].y1, + ]; + expect(values).toEqual([0, 10, 10, 40]); + }); + test('with null values as percentage', () => { + const store = MockStore.default(); + MockStore.addSpecs( + MockSeriesSpec.area({ + ...WITH_NULL_DATASET, + stackMode: StackMode.Percentage, + }), + store, + ); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + const values = [ + formattedDataSeries[0].data[0].y0, + formattedDataSeries[0].data[0].y1, + formattedDataSeries[1].data[0].y1, + formattedDataSeries[2].data[0].y1, + ]; + expect(values).toEqual([0, 0.25, 0.25, 1]); + }); + }); + describe('Format stacked dataset', () => { + test('format data without nulls', () => { + const store = MockStore.default(); + MockStore.addSpecs(STANDARD_DATA_SET, store); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + expect(formattedDataSeries[0].data[0]).toMatchObject({ + initialY0: null, + initialY1: 10, + x: 0, + y0: 0, + y1: 10, + mark: null, + }); + expect(formattedDataSeries[1].data[0]).toMatchObject({ + initialY0: null, + initialY1: 20, + x: 0, + y0: 10, + y1: 30, + mark: null, + }); + expect(formattedDataSeries[2].data[0]).toMatchObject({ + initialY0: null, + initialY1: 30, + x: 0, + y0: 30, + y1: 60, + mark: null, + }); + }); + test('format data with nulls', () => { + const store = MockStore.default(); + MockStore.addSpecs(WITH_NULL_DATASET, store); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + expect(formattedDataSeries[1].data[0]).toMatchObject({ + initialY0: null, + initialY1: null, + x: 0, + y1: 10, + y0: 10, + mark: null, + }); + }); + test('format data without nulls with y0 values', () => { + const store = MockStore.default(); + MockStore.addSpecs(STANDARD_DATA_SET_WY0, store); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + expect(formattedDataSeries[0].data[0]).toMatchObject({ + initialY0: 2, + initialY1: 10, + x: 0, + y0: 2, + y1: 10, + mark: null, + }); + expect(formattedDataSeries[1].data[0]).toMatchObject({ + initialY0: 4, + initialY1: 20, + x: 0, + y0: 14, + y1: 30, + mark: null, + }); + expect(formattedDataSeries[2].data[0]).toMatchObject({ + initialY0: 6, + initialY1: 30, + x: 0, + y0: 36, + y1: 60, + mark: null, + }); + }); + test('format data with nulls - missing points', () => { + const store = MockStore.default(); + MockStore.addSpecs(WITH_NULL_DATASET_WY0, store); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + expect(formattedDataSeries[0].data[0]).toMatchObject({ + initialY0: 2, + initialY1: 10, + x: 0, + y0: 2, + y1: 10, + mark: null, + }); + expect(formattedDataSeries[1].data[0]).toMatchObject({ + initialY0: null, + initialY1: null, + x: 0, + y1: 10, + y0: 10, + mark: null, + }); + expect(formattedDataSeries[2].data[0]).toMatchObject({ + initialY0: 6, + initialY1: 30, + x: 0, + y0: 16, + y1: 40, + mark: null, + }); + }); + test('format data without nulls on second series', () => { + const store = MockStore.default(); + MockStore.addSpecs(DATA_SET_WITH_NULL_2, store); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + expect(formattedDataSeries).toHaveLength(2); + expect(formattedDataSeries[0].data).toHaveLength(4); + expect(formattedDataSeries[1].data).toHaveLength(4); + + expect(formattedDataSeries[0].data[0]).toMatchObject({ + initialY0: null, + initialY1: 1, + x: 1, + y0: 0, + y1: 1, + mark: null, + }); + expect(formattedDataSeries[0].data[1]).toMatchObject({ + initialY0: null, + initialY1: 2, + x: 2, + y0: 0, + y1: 2, + mark: null, + }); + expect(formattedDataSeries[0].data[3]).toMatchObject({ + initialY0: null, + initialY1: 4, + x: 4, + y0: 0, + y1: 4, + mark: null, + }); + expect(formattedDataSeries[1].data[0]).toMatchObject({ + initialY0: null, + initialY1: 21, + x: 1, + y0: 1, + y1: 22, + mark: null, + }); + expect(formattedDataSeries[1].data[2]).toMatchObject({ + initialY0: null, + initialY1: 23, + x: 3, + y0: 0, + y1: 23, + mark: null, + }); + }); + }); + test('Correctly handle 0 values on percentage stack', () => { + const store = MockStore.default(); + MockStore.addSpecs( + MockSeriesSpec.area({ + xScaleType: ScaleType.Linear, + yAccessors: ['y1'], + splitSeriesAccessors: ['g'], + stackAccessors: ['x'], + data: [ + { x: 1, y1: 0, g: 'a' }, + { x: 1, y1: 0, g: 'b' }, + ], + }), + store, + ); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + + expect(formattedDataSeries[1].data[0]).toMatchObject({ + initialY0: null, + initialY1: 0, + x: 1, + y0: 0, + y1: 0, + mark: null, + }); + expect(formattedDataSeries[0].data[0]).toMatchObject({ + initialY0: null, + initialY1: 0, + x: 1, + y0: 0, + y1: 0, + mark: null, + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/stacked_series_utils.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/stacked_series_utils.ts new file mode 100644 index 000000000000..98b7aa288fbf --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/stacked_series_utils.ts @@ -0,0 +1,179 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + stack as D3Stack, + stackOffsetExpand as D3StackOffsetExpand, + stackOffsetNone as D3StackOffsetNone, + stackOffsetSilhouette as D3StackOffsetSilhouette, + stackOffsetWiggle as D3StackOffsetWiggle, + stackOrderNone, + SeriesPoint, +} from 'd3-shape'; + +import { SeriesKey } from '../../../common/series_id'; +import { ScaleType } from '../../../scales/constants'; +import { clamp } from '../../../utils/common'; +import { DataSeries, DataSeriesDatum } from './series'; +import { StackMode } from './specs'; + +/** @internal */ +export interface StackedValues { + values: number[]; + percent: Array; + total: number; +} + +/** @internal */ +export const datumXSortPredicate = (xScaleType: ScaleType, sortedXValues?: (string | number)[]) => ( + a: { x: number | string }, + b: { x: number | string }, +) => { + if (xScaleType === ScaleType.Ordinal || typeof a.x === 'string' || typeof b.x === 'string') { + return sortedXValues ? sortedXValues.indexOf(a.x) - sortedXValues.indexOf(b.x) : 0; + } + return a.x - b.x; +}; + +type D3StackArrayElement = Record; +type D3UnionStack = Record< + SeriesKey, + { + y0: SeriesPoint[]; + y1: SeriesPoint[]; + } +>; + +/** @internal */ +export function formatStackedDataSeriesValues( + dataSeries: DataSeries[], + xValues: Set, + stackMode?: StackMode, +): DataSeries[] { + const dataSeriesKeys = dataSeries.reduce>((acc, curr) => { + acc[curr.key] = curr; + return acc; + }, {}); + + const xValuesArray = [...xValues]; + const reorderedArray: Array = []; + const xValueMap: Map> = new Map(); + // transforming the current set of series into the d3 stack required data structure + dataSeries.forEach(({ data, key, isFiltered }) => { + if (isFiltered) { + return; + } + const dsMap: Map = new Map(); + data.forEach((d) => { + const { x, y0, y1 } = d; + const xIndex = xValuesArray.indexOf(x); + + if (reorderedArray[xIndex] === undefined) { + reorderedArray[xIndex] = { x }; + } + // y0 can be considered as always present + reorderedArray[xIndex][`${key}-y0`] = y0; + // if y0 is available, we have to count y1 as the different of y1 and y0 + // to correctly stack them when stacking banded charts + reorderedArray[xIndex][`${key}-y1`] = (y1 ?? 0) - (y0 ?? 0); + dsMap.set(x, d); + }); + xValueMap.set(key, dsMap); + }); + + const stackOffset = getOffsetBasedOnStackMode(stackMode); + + const keys = Object.keys(dataSeriesKeys).reduce((acc, key) => [...acc, `${key}-y0`, `${key}-y1`], []); + + const stack = D3Stack().keys(keys).order(stackOrderNone).offset(stackOffset)(reorderedArray); + + const unionedYStacks = stack.reduce((acc, d) => { + const key = d.key.slice(0, -3); + const accessor = d.key.slice(-2); + if (accessor !== 'y1' && accessor !== 'y0') { + return acc; + } + if (!acc[key]) { + acc[key] = { + y0: [], + y1: [], + }; + } + acc[key][accessor] = d.map((da) => da); + return acc; + }, {}); + + return Object.keys(unionedYStacks).map((stackedDataSeriesKey) => { + const dataSeriesProps = dataSeriesKeys[stackedDataSeriesKey]; + const dsMap = xValueMap.get(stackedDataSeriesKey); + const { y0: y0StackArray, y1: y1StackArray } = unionedYStacks[stackedDataSeriesKey]; + const data = y1StackArray + .map((y1Stack, index) => { + const { x } = y1Stack.data; + if (x === undefined || x === null) { + return null; + } + const originalData = dsMap?.get(x); + if (!originalData) { + return null; + } + const [, y0] = y0StackArray[index]; + const [, y1] = y1Stack; + const { initialY0, initialY1, mark, datum, filled } = originalData; + return { + x, + /** + * Due to floating point errors, values computed on a stack + * could falls out of the current defined domain boundaries. + * This in particular cause issues with percent stack, where the domain + * is hardcoded to [0,1] and some value can fall outside that domain. + */ + y1: clampIfStackedAsPercentage(y1, stackMode), + y0: clampIfStackedAsPercentage(y0, stackMode), + initialY0, + initialY1, + mark, + datum, + filled, + }; + }) + .filter((d) => d !== null) as DataSeriesDatum[]; + return { + ...dataSeriesProps, + data, + }; + }); +} + +function clampIfStackedAsPercentage(value: number, stackMode?: StackMode) { + return stackMode === StackMode.Percentage ? clamp(value, 0, 1) : value; +} + +function getOffsetBasedOnStackMode(stackMode?: StackMode) { + switch (stackMode) { + case StackMode.Percentage: + return D3StackOffsetExpand; + case StackMode.Silhouette: + return D3StackOffsetSilhouette; + case StackMode.Wiggle: + return D3StackOffsetWiggle; + default: + return D3StackOffsetNone; + } +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/texture.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/texture.ts new file mode 100644 index 000000000000..a06e842a4276 --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/texture.ts @@ -0,0 +1,115 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { OpacityFn } from '../../../common/color_library_wrappers'; +import { Texture } from '../../../geoms/types'; +import { Color, ColorVariant, getColorFromVariant, getRadians } from '../../../utils/common'; +import { Point } from '../../../utils/point'; +import { TexturedStyles, TextureShape } from '../../../utils/themes/theme'; +import { TextureRendererFn } from '../renderer/shapes_paths'; + +const getSpacing = ({ spacing }: TexturedStyles): Point => ({ + x: typeof spacing === 'number' ? spacing : spacing?.x ?? 0, + y: typeof spacing === 'number' ? spacing : spacing?.y ?? 0, +}); + +const getPath = (textureStyle: TexturedStyles, size: number, stokeWith: number): [path: Path2D, rotation: number] => { + if ('path' in textureStyle) { + const path = typeof textureStyle.path === 'string' ? new Path2D(textureStyle.path) : textureStyle.path; + + return [path, 0]; + } + const [pathFn, rotation] = TextureRendererFn[textureStyle.shape]; + // Prevents clipping shapes near edge + const stokeWidthPadding = [TextureShape.Circle, TextureShape.Square].includes(textureStyle.shape as any) + ? stokeWith + : 0; + + return [new Path2D(pathFn((size - stokeWidthPadding) / 2)), rotation]; +}; + +/** @internal */ +function createPattern( + ctx: CanvasRenderingContext2D, + dpi: number, + patternCanvas: HTMLCanvasElement, + baseColor: Color | ColorVariant, + fillOpacity: OpacityFn, + textureStyle?: TexturedStyles, +): CanvasPattern | undefined { + const pCtx = patternCanvas.getContext('2d'); + if (!textureStyle || !pCtx) return; + + const { size = 10, stroke, strokeWidth = 1, opacity, shapeRotation, fill, dash } = textureStyle; + + const spacing = getSpacing(textureStyle); + const cssWidth = size + spacing.x; + const cssHeight = size + spacing.y; + patternCanvas.width = dpi * cssWidth; + patternCanvas.height = dpi * cssHeight; + + pCtx.globalAlpha = opacity ? fillOpacity(opacity, 1) : fillOpacity(1); + pCtx.lineWidth = strokeWidth; + + pCtx.strokeStyle = getColorFromVariant(baseColor, stroke ?? ColorVariant.Series); + if (dash) pCtx.setLineDash(dash); + + if (fill) pCtx.fillStyle = getColorFromVariant(baseColor, fill); + + const [path, pathRotation] = getPath(textureStyle, size, strokeWidth); + const rotation = (shapeRotation ?? 0) + pathRotation; + + pCtx.scale(dpi, dpi); + pCtx.translate(cssWidth / 2, cssHeight / 2); + + if (rotation) pCtx.rotate(getRadians(rotation)); + + pCtx.beginPath(); + + if (path) { + pCtx.stroke(path); + if (fill) pCtx.fill(path); + } + + return ctx.createPattern(patternCanvas, 'repeat') ?? undefined; +} + +/** @internal */ +export const getTextureStyles = ( + ctx: CanvasRenderingContext2D, + patternCanvas: HTMLCanvasElement, + baseColor: Color | ColorVariant, + fillOpacity: OpacityFn, + texture?: TexturedStyles, +): Texture | undefined => { + const dpi = window.devicePixelRatio; + const pattern = createPattern(ctx, dpi, patternCanvas, baseColor, fillOpacity, texture); + + if (!pattern || !texture) return; + + const scale = 1 / dpi; + pattern.setTransform(new DOMMatrix([scale, 0, 0, scale, 0, 0])); + const { rotation, offset } = texture; + + return { + pattern, + rotation, + offset, + }; +}; diff --git a/packages/osd-charts/src/common/__mocks__/calcs.ts b/packages/osd-charts/src/common/__mocks__/calcs.ts new file mode 100644 index 000000000000..0edd9481b07b --- /dev/null +++ b/packages/osd-charts/src/common/__mocks__/calcs.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const module = jest.requireActual('../color_calcs.ts'); + +export const getBackgroundWithContainerColorFromUser = jest.fn(module.getBackgroundWithContainerColorFromUser); +export const makeHighContrastColor = jest.fn(module.makeHighContrastColor); diff --git a/packages/osd-charts/src/common/__mocks__/color_library_wrappers.ts b/packages/osd-charts/src/common/__mocks__/color_library_wrappers.ts new file mode 100644 index 000000000000..b1761601401d --- /dev/null +++ b/packages/osd-charts/src/common/__mocks__/color_library_wrappers.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const module = jest.requireActual('../color_library_wrappers.ts'); + +export const { defaultColor, transparentColor, defaultD3Color } = module; + +export const stringToRGB = jest.fn(module.stringToRGB); +export const validateColor = jest.fn(module.validateColor); +export const argsToRGB = jest.fn(module.argsToRGB); +export const argsToRGBString = jest.fn(module.argsToRGBString); +export const RGBtoString = jest.fn(module.RGBtoString); diff --git a/packages/osd-charts/src/common/__mocks__/fill_text_layout.ts b/packages/osd-charts/src/common/__mocks__/fill_text_layout.ts new file mode 100644 index 000000000000..a7326e083104 --- /dev/null +++ b/packages/osd-charts/src/common/__mocks__/fill_text_layout.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const module = jest.requireActual('../../viewmodel/fill_text_layout.ts'); + +export const getTextColorIfTextInvertible = jest.fn(module.getTextColorIfTextInvertible); diff --git a/packages/osd-charts/src/common/__mocks__/link_text_layout.ts b/packages/osd-charts/src/common/__mocks__/link_text_layout.ts new file mode 100644 index 000000000000..37cbc9b186b4 --- /dev/null +++ b/packages/osd-charts/src/common/__mocks__/link_text_layout.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const module = jest.requireActual('../../link_text_layout.ts'); + +export const linkTextLayout = jest.fn(module.linkTextLayout); diff --git a/packages/osd-charts/src/common/category.ts b/packages/osd-charts/src/common/category.ts new file mode 100644 index 000000000000..b22358fa37d8 --- /dev/null +++ b/packages/osd-charts/src/common/category.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * A string key is used to uniquely identify categories + * + * todo: broaden it; some options: + * - allow other values of `PrimitiveValue` type (now: string | number | null) but should add Symbol + * - allow a descriptor object, eg. `{ key: PrimitiveValue, label: string }` + * - allow an accessor that operates on the key, and maps it to a label + */ + +/** @public */ +export type CategoryKey = string; + +/** @internal */ +export type CategoryLabel = string; diff --git a/packages/osd-charts/src/common/color_calcs.test.ts b/packages/osd-charts/src/common/color_calcs.test.ts new file mode 100644 index 000000000000..c9c27cb436ce --- /dev/null +++ b/packages/osd-charts/src/common/color_calcs.test.ts @@ -0,0 +1,198 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { integerSnap, monotonicHillClimb } from '../solvers/monotonic_hill_climb'; +import { makeHighContrastColor, combineColors } from './color_calcs'; + +describe('calcs', () => { + describe('test makeHighContrastColor', () => { + it('hex input - should change white text to black when background is white', () => { + const expected = '#000'; + const result = makeHighContrastColor('#fff', '#fff'); + expect(result).toBe(expected); + }); + it('rgb input - should change white text to black when background is white', () => { + const expected = '#000'; + const result = makeHighContrastColor('rgb(255, 255, 255)', 'rgb(255, 255, 255)'); + expect(result).toBe(expected); + }); + it('rgba input - should change white text to black when background is white', () => { + const expected = '#000'; + const result = makeHighContrastColor('rgba(255, 255, 255, 1)', 'rgba(255, 255, 255, 1)'); + expect(result).toBe(expected); + }); + it('word input - should change white text to black when background is white', () => { + const expected = '#000'; + const result = makeHighContrastColor('white', 'white'); + expect(result).toBe(expected); + }); + // test contrast computation + it('should provide at least 4.5 contrast', () => { + const foreground = '#fff'; // white + const background = 'rgba(255, 255, 51, 0.3)'; // light yellow + const result = '#000'; // black + expect(result).toBe(makeHighContrastColor(foreground, background)); + }); + it('should use black text for hex value', () => { + const foreground = '#fff'; // white + const background = '#7874B2'; // Thailand color + const result = '#000'; // black + expect(result).toBe(makeHighContrastColor(foreground, background)); + }); + it('should switch to black text if background color is in rgba() format - Thailand', () => { + const containerBackground = 'white'; + const background = 'rgba(120, 116, 178, 0.7)'; + const resultForCombined = 'rgba(161, 158, 201, 1)'; // 0.3 'rgba(215, 213, 232, 1)'; // 0.5 - 'rgba(188, 186, 217, 1)'; //0.7 - ; + expect(combineColors(background, containerBackground)).toBe(resultForCombined); + const foreground = 'white'; + const resultForContrastedText = '#000'; // switches to black text + expect(makeHighContrastColor(foreground, resultForCombined)).toBe(resultForContrastedText); + }); + }); + describe('test the combineColors function', () => { + it('should return correct RGBA with opacity greater than 0.7', () => { + const expected = 'rgba(102, 43, 206, 1)'; + const result = combineColors('rgba(121, 47, 249, 0.8)', '#1c1c24'); + expect(result).toBe(expected); + }); + it('should return correct RGBA with opacity less than 0.7', () => { + const expected = 'rgba(226, 186, 187, 1)'; + const result = combineColors('rgba(228, 26, 28, 0.3)', 'rgba(225, 255, 255, 1)'); + expect(result).toBe(expected); + }); + it('should return correct RGBA with the input color as a word vs rgba or hex value', () => { + const expected = 'rgba(0, 0, 255, 1)'; + const result = combineColors('blue', 'black'); + expect(result).toBe(expected); + }); + it('should return the correct RGBA with hex input', () => { + const expected = 'rgba(212, 242, 210, 1)'; + const result = combineColors('#D4F2D2', '#BEB7DF'); + expect(result).toBe(expected); + }); + }); +}); + +describe('monotonicHillClimb', () => { + const arbitraryNumber = 27; + + describe('continuous functions', () => { + test('linear case', () => { + expect(monotonicHillClimb((n: number) => n, 100, arbitraryNumber)).toBeCloseTo(arbitraryNumber, 6); + }); + + test('flat case should yield `maxVar`', () => { + expect(monotonicHillClimb(() => arbitraryNumber, 100, 50)).toBeCloseTo(100, 6); + }); + + test('nonlinear case', () => { + expect(monotonicHillClimb((n: number) => Math.sin(n), Math.PI / 2, Math.sqrt(2) / 2)).toBeCloseTo(Math.PI / 4, 6); + }); + + test('non-compliant for even `minVar` should yield NaN', () => { + expect(monotonicHillClimb((n: number) => Math.sin(n), Math.PI / 2, -1)).toBeNaN(); + }); + + test('`loVar > hiVar` should yield NaN', () => { + expect( + monotonicHillClimb( + (n: number) => Math.sin(n), + 1, + arbitraryNumber, + (n: number) => n, + 2, + ), + ).toBeNaN(); + }); + + test('compliant for `maxVar` should yield `maxVar`', () => { + expect(monotonicHillClimb((n: number) => Math.sin(n), Math.PI / 2, 1)).toBeCloseTo(Math.PI / 2, 6); + }); + + test('`loVar === hiVar`, compliant', () => { + expect( + monotonicHillClimb( + (n: number) => Math.sin(n), + Math.PI / 2, + 1, + (n: number) => n, + Math.PI / 2, + ), + ).toBe(Math.PI / 2); + }); + + test('`loVar === hiVar`, non-compliant', () => { + expect( + monotonicHillClimb( + (n: number) => Math.sin(n), + Math.PI / 2, + Math.sqrt(2) / 2, + (n: number) => n, + Math.PI / 2, + ), + ).toBeNaN(); + }); + }); + + describe('integral domain functions', () => { + test('linear case', () => { + expect(monotonicHillClimb((n: number) => n, 100, arbitraryNumber, integerSnap)).toBe(arbitraryNumber); + }); + + test('flat case should yield `maxVar`', () => { + expect(monotonicHillClimb(() => arbitraryNumber, 100, 50)).toBe(100); + }); + + test('nonlinear case', () => { + expect(monotonicHillClimb((n: number) => Math.sin(n / 10), 15, Math.sqrt(2) / 2, integerSnap)).toBe(7); + }); + + test('non-compliant for even `minVar` should yield NaN', () => { + expect(monotonicHillClimb((n: number) => Math.sin(n), Math.PI / 2, -1, integerSnap)).toBeNaN(); + }); + + test('`loVar > hiVar` should yield NaN', () => { + expect(monotonicHillClimb((n: number) => Math.sin(n), 1, arbitraryNumber, integerSnap, 2)).toBeNaN(); + }); + + test('compliant for `maxVar` should yield `maxVar`', () => { + expect(monotonicHillClimb((n: number) => Math.sin(n / 10), 15, 1, integerSnap)).toBe(15); + }); + + test('`loVar === hiVar`, compliant', () => { + expect(monotonicHillClimb((n: number) => Math.sin(n / 10), 15, 1, integerSnap, 15)).toBe(15); + }); + + test('`loVar === hiVar`, non-compliant', () => { + expect(monotonicHillClimb((n: number) => Math.sin(n / 10), 15, Math.sqrt(2) / 2, integerSnap, 15)).toBeNaN(); + }); + + test('`loVar + 1 === hiVar`, latter is compliant', () => { + expect(monotonicHillClimb((n: number) => Math.sin(n / 10), 15, 1, integerSnap, 14)).toBe(15); + }); + + test('`loVar + 1 === hiVar`, only former is compliant', () => { + expect(monotonicHillClimb((n: number) => Math.sin(n / 10), 15, 0.99, integerSnap, 14)).toBe(14); + }); + + test('`loVar + 1 === hiVar`, non-compliant', () => { + expect(monotonicHillClimb((n: number) => Math.sin(n / 10), 15, Math.sqrt(2) / 2, integerSnap, 14)).toBeNaN(); + }); + }); +}); diff --git a/packages/osd-charts/src/common/color_calcs.ts b/packages/osd-charts/src/common/color_calcs.ts new file mode 100644 index 000000000000..5d44a8a7e802 --- /dev/null +++ b/packages/osd-charts/src/common/color_calcs.ts @@ -0,0 +1,211 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import chroma from 'chroma-js'; + +import { Color } from '../utils/common'; +import { RgbaTuple, RGBATupleToString, RgbTuple, stringToRGB } from './color_library_wrappers'; +import { Ratio } from './geometry'; +import { TextContrast } from './text_utils'; + +/** @internal */ +export function hueInterpolator(colors: RgbTuple[]) { + return (d: number) => { + const index = Math.round(d * 255); + const [r, g, b, a] = colors[index]; + return colors[index].length === 3 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${a ?? 1})`; + }; +} + +/** @internal */ +export function addOpacity(hexColorString: string, opacity: Ratio) { + // this is a super imperfect multiplicative alpha blender that assumes a "#rrggbb" or "#rrggbbaa" hexColorString + // todo roll some proper utility that can handle "rgb(...)", "rgba(...)", "red", {r, g, b} etc. + return opacity === 1 + ? hexColorString + : hexColorString.slice(0, 7) + + (hexColorString.slice(7).length === 0 || parseInt(hexColorString.slice(7, 2), 16) === 255 + ? `00${Math.round(opacity * 255).toString(16)}`.slice(-2) // color was of full opacity + : `00${Math.round((parseInt(hexColorString.slice(7, 2), 16) / 255) * opacity * 255).toString(16)}`.slice(-2)); +} + +/** @internal */ +export function arrayToLookup(keyFun: (v: any) => any, array: Array) { + return Object.assign({}, ...array.map((d) => ({ [keyFun(d)]: d }))); +} + +const rgbaCache: Map = new Map(); + +function colorToRgba(color: Color): RgbaTuple { + const cachedValue = rgbaCache.get(color); + if (cachedValue === undefined) { + const newValue = chroma(color).rgba(); + rgbaCache.set(color, newValue); + return newValue; + } + return cachedValue; +} + +/** If the user specifies the background of the container in which the chart will be on, we can use that color + * and make sure to provide optimal contrast + * @internal + */ +export function combineColors(foregroundColor: Color, backgroundColor: Color): Color { + const [red1, green1, blue1, alpha1] = colorToRgba(foregroundColor); + const [red2, green2, blue2, alpha2] = colorToRgba(backgroundColor); + + // For reference on alpha calculations: + // https://en.wikipedia.org/wiki/Alpha_compositing + const combinedAlpha = alpha1 + alpha2 * (1 - alpha1); + + if (combinedAlpha === 0) { + return 'rgba(0,0,0,0)'; + } + + const combinedRed = Math.round((red1 * alpha1 + red2 * alpha2 * (1 - alpha1)) / combinedAlpha); + const combinedGreen = Math.round((green1 * alpha1 + green2 * alpha2 * (1 - alpha1)) / combinedAlpha); + const combinedBlue = Math.round((blue1 * alpha1 + blue2 * alpha2 * (1 - alpha1)) / combinedAlpha); + const rgba: RgbTuple = [combinedRed, combinedGreen, combinedBlue, combinedAlpha]; + + return RGBATupleToString(rgba); +} + +const validCache: Map = new Map(); + +/** + * Return true if the color is a valid CSS color, false otherwise + * @param color a color written in string + * @internal + */ +export function isColorValid(color?: string): color is Color { + const cachedValue = validCache.get(color); + if (cachedValue === undefined) { + const newValue = Boolean(color) && chroma.valid(color); + validCache.set(color, newValue); + return newValue; + } + return cachedValue; +} + +/** + * Adjust the text color in cases black and white can't reach ideal 4.5 ratio + * @internal + */ +export function makeHighContrastColor(foreground: Color, background: Color, ratio = 4.5): Color { + // determine the lightness factor of the background color to determine whether to lighten or darken the foreground + const lightness = chroma(background).get('hsl.l'); + let highContrastTextColor = foreground; + const isBackgroundDark = colorIsDark(background); + // determine whether white or black text is ideal contrast vs a grey that just passes the ratio + if (isBackgroundDark && chroma.deltaE('black', foreground) === 0) { + highContrastTextColor = '#fff'; + } else if (lightness > 0.5 && chroma.deltaE('white', foreground) === 0) { + highContrastTextColor = '#000'; + } + const precision = 1e8; + let contrast = getContrast(highContrastTextColor, background); + // brighten and darken the text color if not meeting the ratio + while (contrast < ratio) { + highContrastTextColor = isBackgroundDark + ? chroma(highContrastTextColor).brighten().toString() + : chroma(highContrastTextColor).darken().toString(); + const scaledOldContrast = Math.round(contrast * precision) / precision; + contrast = getContrast(highContrastTextColor, background); + const scaledContrast = Math.round(contrast * precision) / precision; + // catch if the ideal contrast may not be possible + if (scaledOldContrast === scaledContrast) { + break; + } + } + return highContrastTextColor.toString(); +} + +/** + * show contrast amount + * @internal + */ +export function getContrast(foregroundColor: string | chroma.Color, backgroundColor: string | chroma.Color): number { + return chroma.contrast(foregroundColor, backgroundColor); +} + +/** + * determines if the color is dark based on the luminance + * @internal + */ +export function colorIsDark(color: Color): boolean { + const luminance = chroma(color).luminance(); + return luminance < 0.2; +} + +/** + * inverse color for text + * @internal + */ +export function getTextColorIfTextInvertible( + specifiedTextColorIsDark: boolean, + backgroundIsDark: boolean, + textColor: Color, + textContrast: TextContrast, + backgroundColor: Color, +): Color { + const inverseForContrast = specifiedTextColorIsDark === backgroundIsDark; + const { r: tr, g: tg, b: tb, opacity: to } = stringToRGB(textColor); + if (!textContrast) { + return inverseForContrast + ? to === undefined + ? `rgb(${255 - tr}, ${255 - tg}, ${255 - tb})` + : `rgba(${255 - tr}, ${255 - tg}, ${255 - tb}, ${to})` + : textColor; + } + if (textContrast === true && typeof textContrast !== 'number') { + return inverseForContrast + ? to === undefined + ? makeHighContrastColor(`rgb(${255 - tr}, ${255 - tg}, ${255 - tb})`, backgroundColor) + : makeHighContrastColor(`rgba(${255 - tr}, ${255 - tg}, ${255 - tb}, ${to})`, backgroundColor) + : makeHighContrastColor(textColor, backgroundColor); + } + if (typeof textContrast === 'number') { + return inverseForContrast + ? to === undefined + ? makeHighContrastColor(`rgb(${255 - tr}, ${255 - tg}, ${255 - tb})`, backgroundColor, textContrast) + : makeHighContrastColor(`rgba(${255 - tr}, ${255 - tg}, ${255 - tb}, ${to})`, backgroundColor, textContrast) + : makeHighContrastColor(textColor, backgroundColor, textContrast); + } + return 'black'; // this should never happen; added it as previously function return type included undefined; todo +} + +/** + * This function generates color for non-occluded text rendering directly on the + * paper, with possible background color, ie. not on some data ink + * + * @internal + */ +export function getOnPaperColorSet(textColor: Color, sectorLineStroke: Color, containerBackgroundColor?: Color) { + // determine the ideal contrast color for the link labels + const validBackgroundColor = isColorValid(containerBackgroundColor) + ? containerBackgroundColor + : 'rgba(255, 255, 255, 0)'; + const contrastTextColor = containerBackgroundColor + ? makeHighContrastColor(textColor, validBackgroundColor) + : textColor; + const strokeColor = containerBackgroundColor + ? makeHighContrastColor(sectorLineStroke, validBackgroundColor) + : undefined; + return { contrastTextColor, strokeColor }; +} diff --git a/packages/osd-charts/src/common/color_library_wrappers.test.ts b/packages/osd-charts/src/common/color_library_wrappers.test.ts new file mode 100644 index 000000000000..bccfbf46a903 --- /dev/null +++ b/packages/osd-charts/src/common/color_library_wrappers.test.ts @@ -0,0 +1,220 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + stringToRGB, + validateColor, + defaultD3Color, + argsToRGB, + RgbObject, + argsToRGBString, + RGBtoString, +} from './color_library_wrappers'; + +describe('d3 Utils', () => { + describe('stringToRGB', () => { + describe('bad colors or undefined', () => { + it('should return default color for undefined color string', () => { + expect(stringToRGB()).toMatchObject({ + r: 255, + g: 0, + b: 0, + opacity: 1, + }); + }); + + it('should return default RgbObject', () => { + expect(stringToRGB('not a color')).toMatchObject({ + r: 255, + g: 0, + b: 0, + opacity: 1, + }); + }); + + it('should return default color if bad opacity', () => { + expect(stringToRGB('rgba(50,50,50,x)')).toMatchObject({ + r: 255, + g: 0, + b: 0, + opacity: 1, + }); + }); + }); + + describe('hex colors', () => { + it('should return RgbObject', () => { + expect(stringToRGB('#ef713d')).toMatchObject({ + r: 239, + g: 113, + b: 61, + }); + }); + + it('should return RgbObject from shorthand', () => { + expect(stringToRGB('#ccc')).toMatchObject({ + r: 204, + g: 204, + b: 204, + }); + }); + + it('should return RgbObject with correct opacity', () => { + // https://gist.github.com/lopspower/03fb1cc0ac9f32ef38f4 + expect(stringToRGB('#ef713d80').opacity).toBeCloseTo(0.5, 1); + }); + + it('should return correct RgbObject for alpha value of 0', () => { + expect(stringToRGB('#00000000')).toMatchObject({ + r: 0, + g: 0, + b: 0, + opacity: 0, + }); + }); + }); + + describe('rgb colors', () => { + it('should return RgbObject', () => { + expect(stringToRGB('rgb(50,50,50)')).toMatchObject({ + r: 50, + g: 50, + b: 50, + }); + }); + + it('should return RgbObject with correct opacity', () => { + expect(stringToRGB('rgba(50,50,50,0.25)').opacity).toBe(0.25); + }); + + it('should return correct RgbObject for alpha value of 0', () => { + expect(stringToRGB('rgba(50,50,50,0)')).toMatchObject({ + r: 50, + g: 50, + b: 50, + opacity: 0, + }); + }); + }); + + describe('hsl colors', () => { + it('should return RgbObject', () => { + expect(stringToRGB('hsl(0,0%,50%)')).toMatchObject({ + r: 127.5, + g: 127.5, + b: 127.5, + }); + }); + + it('should return RgbObject with correct opacity', () => { + expect(stringToRGB('hsla(0,0%,50%,0.25)').opacity).toBe(0.25); + }); + + it('should return correct RgbObject for alpha value of 0', () => { + expect(stringToRGB('hsla(0,0%,50%,0)')).toEqual({ + r: 127.5, + g: 127.5, + b: 127.5, + opacity: 0, + }); + }); + }); + + describe('named colors', () => { + it('should return RgbObject', () => { + expect(stringToRGB('aquamarine')).toMatchObject({ + r: 127, + g: 255, + b: 212, + }); + }); + + it('should return default RgbObject with 0 opacity', () => { + expect(stringToRGB('transparent')).toMatchObject({ + r: 0, + g: 0, + b: 0, + opacity: 0, + }); + }); + + it('should return default RgbObject with 0 opacity even with override', () => { + expect(stringToRGB('transparent', 0.5)).toMatchObject({ + r: 0, + g: 0, + b: 0, + opacity: 0, + }); + }); + }); + + describe('Optional opactiy override', () => { + it('should override opacity from color', () => { + expect(stringToRGB('rgba(50,50,50,0.25)', 0.75).opacity).toBe(0.75); + }); + + it('should use OpacityFn to compute opacity override', () => { + expect(stringToRGB('rgba(50,50,50,0.25)', (o) => o * 2).opacity).toBe(0.5); + }); + }); + }); + + describe('validateColor', () => { + it.each(['r', 'g', 'b', 'opacity'])('should return null if %s is NaN', (value) => { + expect( + validateColor({ + ...defaultD3Color, + [value]: NaN, + }), + ).toBeNull(); + }); + + it('should return valid colors', () => { + expect(validateColor(defaultD3Color)).toBe(defaultD3Color); + }); + }); + + describe('argsToRGB', () => { + it.each(['r', 'g', 'b', 'opacity'])('should return defaultD3Color if %s is NaN', (value) => { + const { r, g, b, opacity }: RgbObject = { + ...defaultD3Color, + [value]: NaN, + }; + expect(argsToRGB(r, g, b, opacity)).toEqual(defaultD3Color); + }); + + it('should return valid colors', () => { + const { r, g, b, opacity } = defaultD3Color; + expect(argsToRGB(r, g, b, opacity)).toEqual(defaultD3Color); + }); + }); + + describe('argsToRGBString', () => { + it('should return valid colors', () => { + const { r, g, b, opacity } = defaultD3Color; + expect(argsToRGBString(r, g, b, opacity)).toBe('rgb(255, 0, 0)'); + }); + }); + + describe('RGBtoString', () => { + it('should return valid colors', () => { + expect(RGBtoString(defaultD3Color)).toBe('rgb(255, 0, 0)'); + }); + }); +}); diff --git a/packages/osd-charts/src/common/color_library_wrappers.ts b/packages/osd-charts/src/common/color_library_wrappers.ts new file mode 100644 index 000000000000..92c5434446e2 --- /dev/null +++ b/packages/osd-charts/src/common/color_library_wrappers.ts @@ -0,0 +1,135 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import chroma from 'chroma-js'; +import { rgb as d3Rgb, RGBColor as D3RGBColor } from 'd3-color'; + +import { Color } from '../utils/common'; + +type RGB = number; +type A = number; +/** @internal */ +export type RgbTuple = [RGB, RGB, RGB, RGB?]; +/** @public */ +export type RgbObject = { r: RGB; g: RGB; b: RGB; opacity: A }; + +/** @internal */ +export type RgbaTuple = [RGB, RGB, RGB, RGB]; + +/** @internal */ +export const defaultColor: RgbObject = { r: 255, g: 0, b: 0, opacity: 1 }; +/** @internal */ +export const transparentColor: RgbObject = { r: 0, g: 0, b: 0, opacity: 0 }; +/** @internal */ +export const defaultD3Color: D3RGBColor = d3Rgb(defaultColor.r, defaultColor.g, defaultColor.b, defaultColor.opacity); + +/** @internal */ +export type OpacityFn = (opacity: number, seriesOpacity?: number) => number; + +/** @internal */ +export function stringToRGB(cssColorSpecifier?: string, opacity?: number | OpacityFn): RgbObject { + if (cssColorSpecifier === 'transparent') { + return transparentColor; + } + const color = getColor(cssColorSpecifier); + + if (opacity === undefined) { + return color; + } + + const opacityOverride = typeof opacity === 'number' ? opacity : opacity(color.opacity); + + if (isNaN(opacityOverride)) { + return color; + } + + return { + ...color, + opacity: opacityOverride, + }; +} + +/** + * Returns color as RgbObject or default fallback. + * + * Handles issue in d3-color for hsla and rgba colors with alpha value of `0` + * + * @param cssColorSpecifier + */ +function getColor(cssColorSpecifier: string = ''): RgbObject { + const endRegEx = /,\s*0+(\.0*)?\s*\)$/; + // TODO: make this check more robust + const color: D3RGBColor = + /^(rgba|hsla)\(/i.test(cssColorSpecifier) && endRegEx.test(cssColorSpecifier) + ? { + ...d3Rgb(cssColorSpecifier.replace(endRegEx, ',1)')), + opacity: 0, + } + : d3Rgb(cssColorSpecifier); + + return validateColor(color) ?? defaultColor; +} + +/** @internal */ +export function validateColor(color: D3RGBColor): D3RGBColor | null { + const { r, g, b, opacity } = color; + + if (isNaN(r) || isNaN(g) || isNaN(b) || isNaN(opacity)) { + return null; + } + + return color; +} + +/** @internal */ +export function argsToRGB(r: number, g: number, b: number, opacity: number): D3RGBColor { + return validateColor(d3Rgb(r, g, b, opacity)) ?? defaultD3Color; +} + +/** @internal */ +export function argsToRGBString(r: number, g: number, b: number, opacity: number): string { + // d3.rgb returns an Rgb instance, which has a specialized `toString` method + return argsToRGB(r, g, b, opacity).toString(); +} + +/** @internal */ +export function RGBtoString(rgb: RgbObject): string { + const { r, g, b, opacity } = rgb; + return argsToRGBString(r, g, b, opacity); +} + +/** @internal */ +export function RGBATupleToString(rgba: RgbTuple): string { + if (rgba.length === 4) { + return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${rgba[3]})`; + } + return `rgb(${rgba[0]}, ${rgba[1]}, ${rgba[2]})`; +} + +/** convert rgb to hex + * @internal */ +export function RGBAToHex(rgba: Color) { + return chroma(rgba).hex(); +} + +/** convert hex to rgb + * @internal */ +export function HexToRGB(hex: string) { + return chroma(hex).rgba(); +} diff --git a/packages/osd-charts/src/common/config_objects.ts b/packages/osd-charts/src/common/config_objects.ts new file mode 100644 index 000000000000..9fc2c0403090 --- /dev/null +++ b/packages/osd-charts/src/common/config_objects.ts @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +interface PlainConfigItem { + type: 'group' | 'color' | 'string' | 'boolean' | 'number'; + dflt?: any; + min?: number; + max?: number; + reconfigurable?: boolean | string; + values?: unknown; + documentation?: string; +} + +interface GroupConfigItem extends PlainConfigItem { + type: 'group'; + values: Record; +} + +// switching to `io-ts` style, generic way of combining static and runtime type info - 1st step +class Type { + dflt: A; + + reconfigurable: boolean | string; + + documentation: string; + + constructor(dflt: A, reconfigurable: boolean | string, documentation: string) { + this.dflt = dflt; + this.reconfigurable = reconfigurable; + this.documentation = documentation; + } +} + +/** @internal */ +export class Numeric extends Type { + min: number; + + max: number; + + type = 'number'; + + constructor({ + dflt, + min, + max, + reconfigurable, + documentation, + }: { + dflt: number; + min: number; + max: number; + reconfigurable: boolean | string; + documentation: string; + }) { + super(dflt, reconfigurable, documentation); + this.min = min; + this.max = max; + } +} + +/** @internal */ +export type ConfigItem = PlainConfigItem | Numeric; + +function isGroupConfigItem(item: ConfigItem): item is GroupConfigItem { + return item.type === 'group'; +} + +/** todo switch to `io-ts` style, generic way of combining static and runtime type info + * @internal + */ +export function configMap(mapper: (v: ConfigItem) => unknown, cfgMetadata: Record): Conf { + return Object.assign( + {}, + ...Object.entries(cfgMetadata).map(([k, v]: [string, ConfigItem]) => { + if (isGroupConfigItem(v)) { + return { [k]: configMap(mapper, v.values) }; + } + return { [k]: mapper(v) }; + }), + ); +} diff --git a/packages/osd-charts/src/common/constants.ts b/packages/osd-charts/src/common/constants.ts new file mode 100644 index 000000000000..d94c20b64ab8 --- /dev/null +++ b/packages/osd-charts/src/common/constants.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const DEFAULT_CSS_CURSOR = 'default'; +/** + * @internal + */ +export const TAU = 2 * Math.PI; +/** + * @internal + */ +export const RIGHT_ANGLE = TAU / 4; +/** + * @internal + */ +export const GOLDEN_RATIO = 1.618; + +/** @public */ +export const TOP = 'top' as const; +/** @public */ +export const BOTTOM = 'bottom' as const; +/** @public */ +export const LEFT = 'left' as const; +/** @public */ +export const RIGHT = 'right' as const; +/** @public */ +export const MIDDLE = 'middle' as const; +/** @public */ +export const CENTER = 'center' as const; diff --git a/packages/osd-charts/src/common/event_handler_selectors.ts b/packages/osd-charts/src/common/event_handler_selectors.ts new file mode 100644 index 000000000000..517a5e02bb86 --- /dev/null +++ b/packages/osd-charts/src/common/event_handler_selectors.ts @@ -0,0 +1,121 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LayerValue, SettingsSpec, Spec } from '../specs'; +import { PointerStates } from '../state/chart_state'; +import { isClicking } from '../state/utils'; +import { SeriesIdentifier } from './series_id'; + +/** @internal */ +export const getOnElementClickSelector = (prev: { click: PointerStates['lastClick'] }) => ( + spec: Spec | null, + lastClick: PointerStates['lastClick'], + settings: SettingsSpec, + pickedShapes: LayerValue[][], +): void => { + if (!spec) { + return; + } + if (!settings.onElementClick) { + return; + } + const nextPickedShapesLength = pickedShapes.length; + if (nextPickedShapesLength > 0 && isClicking(prev.click, lastClick) && settings && settings.onElementClick) { + const elements = pickedShapes.map<[Array, SeriesIdentifier]>((values) => [ + values, + { + specId: spec.id, + key: `spec{${spec.id}}`, + }, + ]); + settings.onElementClick(elements); + } + prev.click = lastClick; +}; + +/** @internal */ +export const getOnElementOutSelector = (prev: { pickedShapes: number | null }) => ( + spec: Spec | null, + pickedShapes: LayerValue[][], + settings: SettingsSpec, +): void => { + if (!spec) { + return; + } + if (!settings.onElementOut) { + return; + } + const nextPickedShapes = pickedShapes.length; + + if (prev.pickedShapes !== null && prev.pickedShapes > 0 && nextPickedShapes === 0) { + settings.onElementOut(); + } + prev.pickedShapes = nextPickedShapes; +}; + +function isOverElement(prevPickedShapes: Array> = [], nextPickedShapes: Array>) { + if (nextPickedShapes.length === 0) { + return; + } + if (nextPickedShapes.length !== prevPickedShapes.length) { + return true; + } + return !nextPickedShapes.every((nextPickedShapeValues, index) => { + const prevPickedShapeValues = prevPickedShapes[index]; + if (prevPickedShapeValues === null) { + return false; + } + if (prevPickedShapeValues.length !== nextPickedShapeValues.length) { + return false; + } + return nextPickedShapeValues.every((layerValue, i) => { + const prevPickedValue = prevPickedShapeValues[i]; + if (!prevPickedValue) { + return false; + } + return layerValue.value === prevPickedValue.value && layerValue.groupByRollup === prevPickedValue.groupByRollup; + }); + }); +} + +/** @internal */ +export const getOnElementOverSelector = (prev: { pickedShapes: LayerValue[][] }) => ( + spec: Spec | null, + nextPickedShapes: LayerValue[][], + settings: SettingsSpec, +): void => { + if (!spec) { + return; + } + if (!settings.onElementOver) { + return; + } + + if (isOverElement(prev.pickedShapes, nextPickedShapes)) { + const elements = nextPickedShapes.map<[Array, SeriesIdentifier]>((values) => [ + values, + { + specId: spec.id, + key: `spec{${spec.id}}`, + }, + ]); + settings.onElementOver(elements); + } + prev.pickedShapes = nextPickedShapes; +}; diff --git a/packages/osd-charts/src/common/fill_text_color.ts b/packages/osd-charts/src/common/fill_text_color.ts new file mode 100644 index 000000000000..614699aa93c0 --- /dev/null +++ b/packages/osd-charts/src/common/fill_text_color.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import chroma from 'chroma-js'; + +import { Color } from '../utils/common'; +import { Logger } from '../utils/logger'; +import { + colorIsDark, + combineColors, + getTextColorIfTextInvertible, + isColorValid, + makeHighContrastColor, +} from './color_calcs'; +import { TextContrast } from './text_utils'; + +function isBackgroundColorValid(color: string | undefined, logWarning: boolean): color is string { + const bgColorAlpha = isColorValid(color) ? chroma(color).alpha() : 1; + if (isColorValid(color) && bgColorAlpha >= 1) { + return true; + } + if (logWarning && bgColorAlpha < 1) { + Logger.expected('Text contrast requires a background color with an alpha value of 1', 1, bgColorAlpha); + } + if (logWarning && color !== 'transparent') { + Logger.warn(`Invalid background color "${String(color)}"`); + } + return false; +} + +/** + * Determine the color for the text hinging on the parameters of textInvertible and textContrast + * @internal + */ +export function fillTextColor( + textColor: Color, + textInvertible: boolean, + textContrast: TextContrast, + shapeFillColor: string, + backgroundColor?: Color, +): string { + if (!isBackgroundColorValid(backgroundColor, true)) { + return getTextColorIfTextInvertible( + colorIsDark(shapeFillColor), + colorIsDark(textColor), + textColor, + false, + 'white', // never used + ); + } + + const adjustedTextColor: string | undefined = textColor; + const containerBackground = combineColors(shapeFillColor, backgroundColor); + const textShouldBeInvertedAndTextContrastIsFalse = textInvertible && !textContrast; + const textShouldBeInvertedAndTextContrastIsSetToTrue = textInvertible && typeof textContrast !== 'number'; + const textContrastIsSetToANumberValue = typeof textContrast === 'number'; + const textShouldNotBeInvertedButTextContrastIsDefined = textContrast && !textInvertible; + + // change the contrast for the inverted slices + if (textShouldBeInvertedAndTextContrastIsFalse || textShouldBeInvertedAndTextContrastIsSetToTrue) { + const backgroundIsDark = colorIsDark(combineColors(shapeFillColor, backgroundColor)); + const specifiedTextColorIsDark = colorIsDark(textColor); + return getTextColorIfTextInvertible( + backgroundIsDark, + specifiedTextColorIsDark, + textColor, + textContrast, + containerBackground, + ); + // if textContrast is a number then take that into account or if textInvertible is set to false + } + if (textContrastIsSetToANumberValue || textShouldNotBeInvertedButTextContrastIsDefined) { + return makeHighContrastColor(adjustedTextColor, containerBackground); + } + + return adjustedTextColor; +} diff --git a/packages/osd-charts/src/common/geometry.ts b/packages/osd-charts/src/common/geometry.ts new file mode 100644 index 000000000000..d8580fccddce --- /dev/null +++ b/packages/osd-charts/src/common/geometry.ts @@ -0,0 +1,109 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// In preparation of nominal types in future TS versions +// https://github.com/microsoft/TypeScript/pull/33038 +// eg. to avoid adding angles and coordinates and similar inconsistent number/number ops. +// could in theory be three-valued (in,on,out) +// It also serves as documentation. + +import { RIGHT_ANGLE, TAU } from './constants'; + +/** @public */ +export type Pixels = number; +/** + * A finite number that expresses a ratio + * @public + */ +export type Ratio = number; +/** @public */ +export type SizeRatio = Ratio; +/** @public */ +export type Cartesian = number; +/** @internal */ +export type Coordinate = Cartesian; +/** @public */ +export type Radius = Cartesian; +/** @public */ +export type Radian = Cartesian; // we measure angle in radians, and there's unity between radians and cartesian distances which is the whole point of radians; this is also relevant as we use small-angle approximations +/** @public */ +export type Distance = Cartesian; + +/** @internal */ +export interface PointObject { + x: Coordinate; + y: Coordinate; +} + +/** @internal */ +export type PointTuple = [Coordinate, Coordinate]; + +/** @internal */ +export type PointTuples = [PointTuple, ...PointTuple[]]; // at least one point + +/** @internal */ +export class Circline { + x: Coordinate = NaN; + + y: Coordinate = NaN; + + r: Radius = NaN; +} + +/** @internal */ +export interface CirclinePredicate extends Circline { + inside: boolean; +} + +/** @internal */ +export interface CirclineArc extends Circline { + from: Radian; + to: Radian; +} + +/** @internal */ +type CirclinePredicateSet = CirclinePredicate[]; + +/** @internal */ +export type RingSectorConstruction = CirclinePredicateSet; + +/** @public */ +export type TimeMs = number; + +/** @internal */ +export function wrapToTau(a: Radian) { + if (0 <= a && a <= TAU) return a; // efficient shortcut + if (a < 0) a -= TAU * Math.floor(a / TAU); + return a > TAU ? a % TAU : a; +} + +/** @internal */ +export function diffAngle(a: Radian, b: Radian) { + return ((a - b + Math.PI + TAU) % TAU) - Math.PI; +} + +/** @internal */ +export function meanAngle(a: Radian, b: Radian) { + return (TAU + b + diffAngle(a, b) / 2) % TAU; +} + +/** @internal */ +export function trueBearingToStandardPositionAngle(alphaIn: number) { + return wrapToTau(RIGHT_ANGLE - alphaIn); +} diff --git a/packages/osd-charts/src/common/iterables.ts b/packages/osd-charts/src/common/iterables.ts new file mode 100644 index 000000000000..b87217aa1dd4 --- /dev/null +++ b/packages/osd-charts/src/common/iterables.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// just like [].map except on iterables, to avoid having to materialize both input and output arrays +/** @internal */ +export function map(fun: (arg: InElem, index: number) => OutElem, iterable: Iterable) { + let i = 0; + return (function* mapGenerator() { + for (const next of iterable) yield fun(next, i++); + })(); +} diff --git a/packages/osd-charts/src/common/legend.ts b/packages/osd-charts/src/common/legend.ts new file mode 100644 index 000000000000..2fc80d51d4df --- /dev/null +++ b/packages/osd-charts/src/common/legend.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PrimitiveValue } from '../chart_types/partition_chart/layout/utils/group_by_rollup'; +import { LegendPath } from '../state/actions/legend'; +import { Color } from '../utils/common'; +import { CategoryKey, CategoryLabel } from './category'; +import { SeriesIdentifier } from './series_id'; + +/** @internal */ +export type LegendItemChildId = CategoryKey; + +/** @internal */ +export type LegendItem = { + seriesIdentifiers: SeriesIdentifier[]; + childId?: LegendItemChildId; + depth?: number; + /** + * Path to iterm in hierarchical legend + */ + path: LegendPath; + color: Color; + label: CategoryLabel; + isSeriesHidden?: boolean; + isItemHidden?: boolean; + defaultExtra?: { + raw: number | null; + formatted: number | string | null; + legendSizingLabel: number | string | null; + }; + // TODO: Remove when partition layers are toggleable + isToggleable?: boolean; + keys: Array; +}; + +/** @internal */ +export type LegendItemExtraValues = Map; diff --git a/packages/osd-charts/src/common/math.ts b/packages/osd-charts/src/common/math.ts new file mode 100644 index 000000000000..471900ff2e96 --- /dev/null +++ b/packages/osd-charts/src/common/math.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export function logarithm(base: number, y: number) { + return Math.log(y) / Math.log(base); +} diff --git a/packages/osd-charts/src/common/predicate.ts b/packages/osd-charts/src/common/predicate.ts new file mode 100644 index 000000000000..78bc61254f55 --- /dev/null +++ b/packages/osd-charts/src/common/predicate.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $Values } from 'utility-types'; + +/** @public */ +export const Predicate = Object.freeze({ + NumAsc: 'numAsc' as const, + NumDesc: 'numDesc' as const, + AlphaAsc: 'alphaAsc' as const, + AlphaDesc: 'alphaDesc' as const, + DataIndex: 'dataIndex' as const, +}); + +/** @public */ +export type Predicate = $Values; + +/** @internal */ +export function getPredicateFn(predicate: Predicate, accessor?: keyof T): (a: T, b: T) => number { + switch (predicate) { + case 'alphaAsc': + return (a: T, b: T) => { + const aValue = String(accessor ? a[accessor] : a); + const bValue = String(accessor ? b[accessor] : b); + return aValue.localeCompare(bValue); + }; + case 'alphaDesc': + return (a: T, b: T) => { + const aValue = String(accessor ? a[accessor] : a); + const bValue = String(accessor ? b[accessor] : b); + return bValue.localeCompare(aValue); + }; + case 'numDesc': + return (a: T, b: T) => { + const aValue = Number(accessor ? a[accessor] : a); + const bValue = Number(accessor ? b[accessor] : b); + return bValue - aValue; + }; + default: + case 'dataIndex': + case 'numAsc': + return (a: T, b: T) => { + const aValue = Number(accessor ? a[accessor] : a); + const bValue = Number(accessor ? b[accessor] : b); + return aValue - bValue; + }; + } +} diff --git a/packages/osd-charts/src/common/series_id.ts b/packages/osd-charts/src/common/series_id.ts new file mode 100644 index 000000000000..741c4e7f66cf --- /dev/null +++ b/packages/osd-charts/src/common/series_id.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SpecId } from '../utils/ids'; +import { CategoryKey } from './category'; + +/** + * A string key used to uniquely identify a series + * @public + */ +export type SeriesKey = CategoryKey; + +/** + * A series identifier + * @public + */ +export type SeriesIdentifier = { + /** + * The SpecId, used to identify the spec + */ + specId: SpecId; + /** + * A string key used to uniquely identify a series + */ + key: SeriesKey; +}; diff --git a/packages/osd-charts/src/common/text_utils.ts b/packages/osd-charts/src/common/text_utils.ts new file mode 100644 index 000000000000..1cf480072737 --- /dev/null +++ b/packages/osd-charts/src/common/text_utils.ts @@ -0,0 +1,189 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $Values as Values } from 'utility-types'; + +import { ArrayEntry } from '../chart_types/partition_chart/layout/utils/group_by_rollup'; +import { integerSnap, monotonicHillClimb } from '../solvers/monotonic_hill_climb'; +import { Datum } from '../utils/common'; +import { Pixels } from './geometry'; + +/** @public */ +export const FONT_VARIANTS = Object.freeze(['normal', 'small-caps'] as const); +/** @public */ +export type FontVariant = typeof FONT_VARIANTS[number]; +/** @public */ +export const FONT_WEIGHTS = Object.freeze([ + 100, + 200, + 300, + 400, + 500, + 600, + 700, + 800, + 900, + 'normal', + 'bold', + 'lighter', + 'bolder', + 'inherit', + 'initial', + 'unset', +] as const); +/** @public */ +export type FontWeight = typeof FONT_WEIGHTS[number]; +/** @internal */ +export type NumericFontWeight = number & typeof FONT_WEIGHTS[number]; +/** @public */ +export const FONT_STYLES = Object.freeze(['normal', 'italic', 'oblique', 'inherit', 'initial', 'unset'] as const); +/** @public */ +export type FontStyle = typeof FONT_STYLES[number]; + +/** + * this doesn't include the font size, so it's more like a font face (?) - unfortunately all vague terms + * @public + */ +export interface Font { + fontStyle: FontStyle; + fontVariant: FontVariant; + fontWeight: FontWeight; + fontFamily: FontFamily; + textColor: string; + textOpacity: number; +} + +/** @public */ +export type PartialFont = Partial; +/** @public */ +export const TEXT_ALIGNS = Object.freeze(['start', 'end', 'left', 'right', 'center'] as const); +/** @public */ +export type TextAlign = typeof TEXT_ALIGNS[number]; +/** @public */ +export const TEXT_BASELINE = Object.freeze([ + 'top', + 'hanging', + 'middle', + 'alphabetic', + 'ideographic', + 'bottom', +] as const); +/** @public */ +export type TextBaseline = typeof TEXT_BASELINE[number]; + +/** + * @internal + */ +export interface Box extends Font { + text: string; +} + +/** @internal */ +export type Relation = Array; + +/** @internal */ +export interface Origin { + x0: number; + y0: number; +} + +/** @internal */ +export interface Rectangle extends Origin { + x1: number; + y1: number; +} + +/** @internal */ +export interface Part extends Rectangle { + node: ArrayEntry; +} + +/** + * @internal + */ +export type TextMeasure = (fontSize: number, boxes: Box[]) => TextMetrics[]; + +/** @internal */ +export function cssFontShorthand({ fontStyle, fontVariant, fontWeight, fontFamily }: Font, fontSize: Pixels) { + return `${fontStyle} ${fontVariant} ${fontWeight} ${fontSize}px ${fontFamily}`; +} + +/** @internal */ +export function measureText(ctx: CanvasRenderingContext2D): TextMeasure { + return (fontSize: number, boxes: Box[]): TextMetrics[] => + boxes.map((box: Box) => { + ctx.font = cssFontShorthand(box, fontSize); + return ctx.measureText(box.text); + }); +} + +/** + * todo consider doing tighter control for permissible font families, eg. as in Kibana Canvas - expression language + * - though the same applies for permissible (eg. known available or loaded) font weights, styles, variants... + * @public + */ +export type FontFamily = string; + +/** + * @public + */ +export type TextContrast = boolean | number; + +/** @internal */ +export const VerticalAlignments = Object.freeze({ + top: 'top' as const, + middle: 'middle' as const, + bottom: 'bottom' as const, + alphabetic: 'alphabetic' as const, + hanging: 'hanging' as const, + ideographic: 'ideographic' as const, +}); + +/** @internal */ +export type VerticalAlignments = Values; + +/** @internal */ +export function measureOneBoxWidth(measure: TextMeasure, fontSize: number, box: Box) { + return measure(fontSize, [box])[0].width; +} + +/** @internal */ +export function cutToLength(s: string, maxLength: number) { + return s.length <= maxLength ? s : `${s.slice(0, Math.max(0, maxLength - 1))}…`; // ellipsis is one char +} + +/** @internal */ +export function fitText( + measure: TextMeasure, + desiredText: string, + allottedWidth: number, + fontSize: number, + font: Font, +) { + const desiredLength = desiredText.length; + const response = (v: number) => measure(fontSize, [{ ...font, text: desiredText.slice(0, Math.max(0, v)) }])[0].width; + const visibleLength = monotonicHillClimb(response, desiredLength, allottedWidth, integerSnap); + const text = visibleLength < 2 && desiredLength >= 2 ? '' : cutToLength(desiredText, visibleLength); + const { width, emHeightAscent, emHeightDescent } = measure(fontSize, [{ ...font, text }])[0]; + return { + width, + verticalOffset: -(emHeightDescent + emHeightAscent) / 2, // meaning, `middle` + text, + }; +} diff --git a/packages/osd-charts/src/components/__snapshots__/chart.test.tsx.snap b/packages/osd-charts/src/components/__snapshots__/chart.test.tsx.snap new file mode 100644 index 000000000000..de3a040a84ac --- /dev/null +++ b/packages/osd-charts/src/components/__snapshots__/chart.test.tsx.snap @@ -0,0 +1,127 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Chart should render the legend name test 1`] = ` +" + +
+ + +
+ + + + +
+ + + + +
+ + + + +
+
+
    + +
  • +
    + +
    + + + + + + + +
    +
    + +
  • +
    +
+
+
+
+
+ + + + + + + + + + + +
+ + +
+ + + + + +
+ + + +
+ + + +
+
+ Chart type: +
+
+ bar chart +
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ +" +`; diff --git a/packages/osd-charts/src/components/_container.scss b/packages/osd-charts/src/components/_container.scss new file mode 100644 index 000000000000..739e1771fe99 --- /dev/null +++ b/packages/osd-charts/src/components/_container.scss @@ -0,0 +1,34 @@ +.echChart { + position: relative; + display: flex; + height: 100%; + + &--column { + flex-direction: column; + } +} + +.echContainer { + flex: 1; + position: relative; +} + +.echChartPointerContainer { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + box-sizing: border-box; + user-select: none; +} + +.echChartResizer { + z-index: -10000000; + position: absolute; + bottom: 0; + top: 0; + left: 0; + right: 0; + box-sizing: border-box; +} diff --git a/packages/osd-charts/src/components/_global.scss b/packages/osd-charts/src/components/_global.scss new file mode 100644 index 000000000000..2b680105525f --- /dev/null +++ b/packages/osd-charts/src/components/_global.scss @@ -0,0 +1,16 @@ +.echChartStatus { + visibility: hidden; + pointer-events: none; + z-index: -100000; + width: 0; + height: 0; + position: absolute; +} + +.echChartBackground { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; +} diff --git a/packages/osd-charts/src/components/_index.scss b/packages/osd-charts/src/components/_index.scss new file mode 100644 index 000000000000..ef82d0eff618 --- /dev/null +++ b/packages/osd-charts/src/components/_index.scss @@ -0,0 +1,11 @@ +@import 'global'; +@import 'container'; +@import 'brush/index'; +@import 'tooltip/index'; +@import 'portal/index'; +@import 'icons/index'; +@import 'legend/index'; +@import 'unavailable_chart'; + +@import '../chart_types/xy_chart/renderer/index'; +@import '../chart_types/partition_chart/renderer/index'; diff --git a/packages/osd-charts/src/components/_unavailable_chart.scss b/packages/osd-charts/src/components/_unavailable_chart.scss new file mode 100644 index 000000000000..a64eefad80d2 --- /dev/null +++ b/packages/osd-charts/src/components/_unavailable_chart.scss @@ -0,0 +1,9 @@ +.echReactiveChart_noResults { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + @include euiFontSizeXS; + color: $euiColorDarkShade; +} diff --git a/packages/osd-charts/src/components/accessibility/accessibility.test.tsx b/packages/osd-charts/src/components/accessibility/accessibility.test.tsx new file mode 100644 index 000000000000..09fae35f6532 --- /dev/null +++ b/packages/osd-charts/src/components/accessibility/accessibility.test.tsx @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; + +import { BarSeries, LineSeries, Settings } from '../../specs'; +import { Chart } from '../chart'; + +describe('Accessibility', () => { + it('should include the series types if one type of series', () => { + const wrapper = mount( + + + + , + ); + expect(wrapper.find('dd').first().text()).toBe('bar chart'); + }); + it('should include the series types if multiple types of series', () => { + const wrapper = mount( + + + + + , + ); + expect(wrapper.find('dd').first().text()).toBe('Mixed chart: bar and line chart'); + }); +}); diff --git a/packages/osd-charts/src/components/accessibility/description.tsx b/packages/osd-charts/src/components/accessibility/description.tsx new file mode 100644 index 000000000000..c51c2cc2e02c --- /dev/null +++ b/packages/osd-charts/src/components/accessibility/description.tsx @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { A11ySettings } from '../../state/selectors/get_accessibility_config'; + +/** @internal */ +export function ScreenReaderDescription(props: A11ySettings) { + if (!props.description) return null; + return

{props.description}

; +} diff --git a/packages/osd-charts/src/components/accessibility/index.ts b/packages/osd-charts/src/components/accessibility/index.ts new file mode 100644 index 000000000000..c4b3e8087d18 --- /dev/null +++ b/packages/osd-charts/src/components/accessibility/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* @internal */ +export { ScreenReaderSummary } from './screen_reader_summary'; diff --git a/packages/osd-charts/src/components/accessibility/label.tsx b/packages/osd-charts/src/components/accessibility/label.tsx new file mode 100644 index 000000000000..151ace8cbabb --- /dev/null +++ b/packages/osd-charts/src/components/accessibility/label.tsx @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { A11ySettings } from '../../state/selectors/get_accessibility_config'; + +/** @internal */ +export function ScreenReaderLabel(props: A11ySettings) { + if (!props.label) return null; + const Heading = props.labelHeadingLevel; + return {props.label}; +} diff --git a/packages/osd-charts/src/components/accessibility/screen_reader_summary.tsx b/packages/osd-charts/src/components/accessibility/screen_reader_summary.tsx new file mode 100644 index 000000000000..a2efd1a55a76 --- /dev/null +++ b/packages/osd-charts/src/components/accessibility/screen_reader_summary.tsx @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { memo } from 'react'; +import { connect } from 'react-redux'; + +import { GlobalChartState } from '../../state/chart_state'; +import { + A11ySettings, + DEFAULT_A11Y_SETTINGS, + getA11ySettingsSelector, +} from '../../state/selectors/get_accessibility_config'; +import { getChartTypeDescriptionSelector } from '../../state/selectors/get_chart_type_description'; +import { getInternalIsInitializedSelector, InitStatus } from '../../state/selectors/get_internal_is_intialized'; +import { ScreenReaderDescription } from './description'; +import { ScreenReaderLabel } from './label'; +import { ScreenReaderTypes } from './types'; + +interface ScreenReaderSummaryStateProps { + a11ySettings: A11ySettings; + chartTypeDescription: string; +} + +const ScreenReaderSummaryComponent = ({ a11ySettings, chartTypeDescription }: ScreenReaderSummaryStateProps) => { + return ( +
+ + + +
+ ); +}; + +const DEFAULT_SCREEN_READER_SUMMARY = { + a11ySettings: DEFAULT_A11Y_SETTINGS, + chartTypeDescription: '', +}; + +const mapStateToProps = (state: GlobalChartState): ScreenReaderSummaryStateProps => { + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return DEFAULT_SCREEN_READER_SUMMARY; + } + return { + chartTypeDescription: getChartTypeDescriptionSelector(state), + a11ySettings: getA11ySettingsSelector(state), + }; +}; + +/** @internal */ +export const ScreenReaderSummary = memo(connect(mapStateToProps)(ScreenReaderSummaryComponent)); diff --git a/packages/osd-charts/src/components/accessibility/types.tsx b/packages/osd-charts/src/components/accessibility/types.tsx new file mode 100644 index 000000000000..aca5c91d40ff --- /dev/null +++ b/packages/osd-charts/src/components/accessibility/types.tsx @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { A11ySettings } from '../../state/selectors/get_accessibility_config'; + +interface ScreenReaderTypesProps { + chartTypeDescription: string; +} + +/** @internal */ +export function ScreenReaderTypes(props: A11ySettings & ScreenReaderTypesProps) { + if (!props.defaultSummaryId) return null; + return ( +
+
Chart type:
+
{props.chartTypeDescription}
+
+ ); +} diff --git a/packages/osd-charts/src/components/brush/_brush.scss b/packages/osd-charts/src/components/brush/_brush.scss new file mode 100644 index 000000000000..2401d5bf74c7 --- /dev/null +++ b/packages/osd-charts/src/components/brush/_brush.scss @@ -0,0 +1,10 @@ +.echBrushTool { + position: absolute; + top: 0; + left: 0; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow: hidden; + pointer-events: none; +} diff --git a/packages/osd-charts/src/components/brush/_index.scss b/packages/osd-charts/src/components/brush/_index.scss new file mode 100644 index 000000000000..c72c9d2818b0 --- /dev/null +++ b/packages/osd-charts/src/components/brush/_index.scss @@ -0,0 +1 @@ +@import 'brush'; diff --git a/packages/osd-charts/src/components/brush/brush.tsx b/packages/osd-charts/src/components/brush/brush.tsx new file mode 100644 index 000000000000..e89ef7d8ff4f --- /dev/null +++ b/packages/osd-charts/src/components/brush/brush.tsx @@ -0,0 +1,189 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { RefObject } from 'react'; +import { connect } from 'react-redux'; + +import { renderRect } from '../../chart_types/xy_chart/renderer/canvas/primitives/rect'; +import { RgbObject } from '../../common/color_library_wrappers'; +import { clearCanvas, withContext, withClip } from '../../renderers/canvas'; +import { GlobalChartState } from '../../state/chart_state'; +import { getInternalBrushAreaSelector } from '../../state/selectors/get_internal_brush_area'; +import { getInternalIsBrushingSelector } from '../../state/selectors/get_internal_is_brushing'; +import { getInternalIsBrushingAvailableSelector } from '../../state/selectors/get_internal_is_brushing_available'; +import { getInternalIsInitializedSelector, InitStatus } from '../../state/selectors/get_internal_is_intialized'; +import { getInternalMainProjectionAreaSelector } from '../../state/selectors/get_internal_main_projection_area'; +import { getInternalProjectionContainerAreaSelector } from '../../state/selectors/get_internal_projection_container_area'; +import { Dimensions } from '../../utils/dimensions'; + +interface OwnProps { + fillColor?: RgbObject; +} +interface StateProps { + initialized: boolean; + mainProjectionArea: Dimensions; + projectionContainer: Dimensions; + isBrushing: boolean | undefined; + isBrushAvailable: boolean | undefined; + brushArea: Dimensions | null; + zIndex: number; +} + +const DEFAULT_FILL_COLOR: RgbObject = { + r: 128, + g: 128, + b: 128, + opacity: 0.6, +}; + +type Props = OwnProps & StateProps; + +class BrushToolComponent extends React.Component { + static displayName = 'BrushTool'; + + private readonly devicePixelRatio: number; + + private ctx: CanvasRenderingContext2D | null; + + private canvasRef: RefObject; + + constructor(props: Readonly) { + super(props); + this.ctx = null; + this.devicePixelRatio = window.devicePixelRatio; + this.canvasRef = React.createRef(); + } + + componentDidMount() { + /* + * the DOM element has just been appended, and getContext('2d') is always non-null, + * so we could use a couple of ! non-null assertions but no big plus + */ + this.tryCanvasContext(); + this.drawCanvas(); + } + + componentDidUpdate() { + if (!this.ctx) { + this.tryCanvasContext(); + } + if (this.props.initialized) { + this.drawCanvas(); + } + } + + private drawCanvas = () => { + const { brushArea, mainProjectionArea, fillColor } = this.props; + if (!this.ctx || !brushArea) { + return; + } + const { top, left, width, height } = brushArea; + withContext(this.ctx, (ctx) => { + ctx.scale(this.devicePixelRatio, this.devicePixelRatio); + withClip( + ctx, + { + x: mainProjectionArea.left, + y: mainProjectionArea.top, + width: mainProjectionArea.width, + height: mainProjectionArea.height, + }, + (ctx) => { + clearCanvas(ctx, 200000, 200000); + ctx.translate(mainProjectionArea.left, mainProjectionArea.top); + renderRect( + ctx, + { + x: left, + y: top, + width, + height, + }, + { + color: fillColor ?? DEFAULT_FILL_COLOR, + }, + ); + }, + ); + }); + }; + + private tryCanvasContext() { + const canvas = this.canvasRef.current; + this.ctx = canvas && canvas.getContext('2d'); + } + + render() { + const { initialized, isBrushAvailable, isBrushing, projectionContainer, zIndex } = this.props; + if (!initialized || !isBrushAvailable || !isBrushing) { + this.ctx = null; + return null; + } + const { width, height } = projectionContainer; + return ( + + ); + } +} + +const mapStateToProps = (state: GlobalChartState): StateProps => { + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return { + initialized: false, + projectionContainer: { + width: 0, + height: 0, + left: 0, + top: 0, + }, + mainProjectionArea: { + top: 0, + left: 0, + width: 0, + height: 0, + }, + isBrushing: false, + isBrushAvailable: false, + brushArea: null, + zIndex: 0, + }; + } + return { + initialized: state.specsInitialized, + projectionContainer: getInternalProjectionContainerAreaSelector(state), + mainProjectionArea: getInternalMainProjectionAreaSelector(state), + isBrushAvailable: getInternalIsBrushingAvailableSelector(state), + isBrushing: getInternalIsBrushingSelector(state), + brushArea: getInternalBrushAreaSelector(state), + zIndex: state.zIndex, + }; +}; + +/** @internal */ +export const BrushTool = connect(mapStateToProps)(BrushToolComponent); diff --git a/packages/osd-charts/src/components/chart.snap.test.ts b/packages/osd-charts/src/components/chart.snap.test.ts new file mode 100644 index 000000000000..715a10c73f18 --- /dev/null +++ b/packages/osd-charts/src/components/chart.snap.test.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Chart } from './chart'; + +describe('test getPNGSnapshot in Chart class', () => { + jest.mock('../components/chart'); + it('should be called', () => { + const chart = new Chart({}); + const spy = jest.spyOn(chart, 'getPNGSnapshot'); + chart.getPNGSnapshot({ backgroundColor: 'white', pixelRatio: 1 }); + + expect(spy).toBeCalled(); + }); +}); diff --git a/packages/osd-charts/src/components/chart.test.tsx b/packages/osd-charts/src/components/chart.test.tsx new file mode 100644 index 000000000000..fdc7129b9849 --- /dev/null +++ b/packages/osd-charts/src/components/chart.test.tsx @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; + +import { Settings, BarSeries } from '../specs'; +import { Chart } from './chart'; + +describe('Chart', () => { + it("should render 'No data to display' without series", () => { + const wrapper = mount(); + expect(wrapper.text()).toContain('No data to display'); + }); + + it("should render 'No data to display' with settings but without series", () => { + const wrapper = mount( + + + , + ); + expect(wrapper.text()).toContain('No data to display'); + }); + + it("should render 'No data to display' with an empty dataset", () => { + const wrapper = mount( + + + + , + ); + expect(wrapper.text()).toContain('No data to display'); + }); + + it('should render the legend name test', () => { + const wrapper = mount( + + + + , + ); + expect(wrapper.debug()).toMatchSnapshot(); + }); +}); diff --git a/packages/osd-charts/src/components/chart.tsx b/packages/osd-charts/src/components/chart.tsx new file mode 100644 index 000000000000..89dc4c72a495 --- /dev/null +++ b/packages/osd-charts/src/components/chart.tsx @@ -0,0 +1,191 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import classNames from 'classnames'; +import React, { createRef } from 'react'; +import { Provider } from 'react-redux'; +import { createStore, Store, Unsubscribe, StoreEnhancer, applyMiddleware, Middleware } from 'redux'; +import uuid from 'uuid'; + +import { LegendPositionConfig, PointerEvent } from '../specs'; +import { SpecsParser } from '../specs/specs_parser'; +import { onExternalPointerEvent } from '../state/actions/events'; +import { onComputedZIndex } from '../state/actions/z_index'; +import { chartStoreReducer, GlobalChartState } from '../state/chart_state'; +import { getInternalIsInitializedSelector, InitStatus } from '../state/selectors/get_internal_is_intialized'; +import { getLegendConfigSelector } from '../state/selectors/get_legend_config_selector'; +import { ChartSize, getChartSize } from '../utils/chart_size'; +import { LayoutDirection } from '../utils/common'; +import { ChartBackground } from './chart_background'; +import { ChartContainer } from './chart_container'; +import { ChartResizer } from './chart_resizer'; +import { ChartStatus } from './chart_status'; +import { ErrorBoundary } from './error_boundary'; +import { Legend } from './legend/legend'; +import { getElementZIndex } from './portal/utils'; + +interface ChartProps { + /** + * The type of rendered + * @defaultValue `canvas` + */ + renderer?: 'svg' | 'canvas'; + size?: ChartSize; + className?: string; + id?: string; +} + +interface ChartState { + legendDirection: LegendPositionConfig['direction']; +} + +const getMiddlware = (id: string): StoreEnhancer => { + const middlware: Middleware[] = []; + + if (typeof window !== 'undefined' && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + return (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ + trace: true, + name: `@elastic/charts (id: ${id})`, + })(applyMiddleware(...middlware)); + } + + return applyMiddleware(...middlware); +}; + +/** @public */ +export class Chart extends React.Component { + static defaultProps: ChartProps = { + renderer: 'canvas', + }; + + private unsubscribeToStore: Unsubscribe; + + private chartStore: Store; + + private chartContainerRef: React.RefObject; + + private chartStageRef: React.RefObject; + + constructor(props: ChartProps) { + super(props); + this.chartContainerRef = createRef(); + this.chartStageRef = createRef(); + + const id = props.id ?? uuid.v4(); + const storeReducer = chartStoreReducer(id); + const enhancer = getMiddlware(id); + this.chartStore = createStore(storeReducer, enhancer); + this.state = { + legendDirection: LayoutDirection.Vertical, + }; + this.unsubscribeToStore = this.chartStore.subscribe(() => { + const state = this.chartStore.getState(); + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return; + } + + const { + legendPosition: { direction }, + } = getLegendConfigSelector(state); + if (this.state.legendDirection !== direction) { + this.setState({ + legendDirection: direction, + }); + } + if (state.internalChartState) { + state.internalChartState.eventCallbacks(state); + } + }); + } + + componentDidMount() { + if (this.chartContainerRef.current) { + const zIndex = getElementZIndex(this.chartContainerRef.current, document.body); + this.chartStore.dispatch(onComputedZIndex(zIndex)); + } + } + + componentWillUnmount() { + this.unsubscribeToStore(); + } + + getPNGSnapshot( + // eslint-disable-next-line unicorn/no-object-as-default-parameter + options = { + backgroundColor: 'transparent', + pixelRatio: 2, + }, + ): { + blobOrDataUrl: any; + browser: 'IE11' | 'other'; + } | null { + if (!this.chartStageRef.current) { + return null; + } + const canvas = this.chartStageRef.current; + const backgroundCanvas = document.createElement('canvas'); + backgroundCanvas.width = canvas.width; + backgroundCanvas.height = canvas.height; + const bgCtx = backgroundCanvas.getContext('2d'); + if (!bgCtx) { + return null; + } + bgCtx.fillStyle = options.backgroundColor; + bgCtx.fillRect(0, 0, canvas.width, canvas.height); + bgCtx.drawImage(canvas, 0, 0); + + return { + blobOrDataUrl: backgroundCanvas.toDataURL(), + browser: 'other', + }; + } + + getChartContainerRef = () => this.chartContainerRef; + + dispatchExternalPointerEvent(event: PointerEvent) { + this.chartStore.dispatch(onExternalPointerEvent(event)); + } + + render() { + const { size, className } = this.props; + const containerSizeStyle = getChartSize(size); + const chartClassNames = classNames('echChart', className, { + 'echChart--column': this.state.legendDirection === LayoutDirection.Horizontal, + }); + + return ( + +
+ + + + + {/* TODO: Add renderFn to error boundary */} + + {this.props.children} +
+ +
+
+
+
+ ); + } +} diff --git a/packages/osd-charts/src/components/chart_background.tsx b/packages/osd-charts/src/components/chart_background.tsx new file mode 100644 index 000000000000..09be18348e22 --- /dev/null +++ b/packages/osd-charts/src/components/chart_background.tsx @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { connect } from 'react-redux'; + +import { GlobalChartState } from '../state/chart_state'; +import { getChartThemeSelector } from '../state/selectors/get_chart_theme'; +import { getInternalIsInitializedSelector, InitStatus } from '../state/selectors/get_internal_is_intialized'; + +interface ChartBackgroundProps { + backgroundColor: string; +} + +/** @internal */ +export class ChartBackgroundComponent extends React.Component { + static displayName = 'ChartBackground'; + + render() { + const { backgroundColor } = this.props; + return
; + } +} + +const mapStateToProps = (state: GlobalChartState): ChartBackgroundProps => { + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return { + backgroundColor: 'transparent', + }; + } + return { + backgroundColor: getChartThemeSelector(state).background.color, + }; +}; + +/** @internal */ +export const ChartBackground = connect(mapStateToProps)(ChartBackgroundComponent); diff --git a/packages/osd-charts/src/components/chart_container.tsx b/packages/osd-charts/src/components/chart_container.tsx new file mode 100644 index 000000000000..4beff42f2c89 --- /dev/null +++ b/packages/osd-charts/src/components/chart_container.tsx @@ -0,0 +1,249 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; + +import { DEFAULT_CSS_CURSOR } from '../common/constants'; +import { SettingsSpec } from '../specs'; +import { onKeyPress as onKeyPressAction } from '../state/actions/key'; +import { + onMouseUp as onMouseUpAction, + onMouseDown as onMouseDownAction, + onPointerMove as onPointerMoveAction, +} from '../state/actions/mouse'; +import { GlobalChartState, BackwardRef } from '../state/chart_state'; +import { getInternalChartRendererSelector } from '../state/selectors/get_chart_type_components'; +import { getInternalPointerCursor } from '../state/selectors/get_internal_cursor_pointer'; +import { getInternalIsBrushingSelector } from '../state/selectors/get_internal_is_brushing'; +import { getInternalIsBrushingAvailableSelector } from '../state/selectors/get_internal_is_brushing_available'; +import { getInternalIsInitializedSelector, InitStatus } from '../state/selectors/get_internal_is_intialized'; +import { getSettingsSpecSelector } from '../state/selectors/get_settings_specs'; +import { isInternalChartEmptySelector } from '../state/selectors/is_chart_empty'; +import { deepEqual } from '../utils/fast_deep_equal'; +import { NoResults } from './no_results'; + +interface ChartContainerComponentStateProps { + status: InitStatus; + isChartEmpty?: boolean; + pointerCursor: string; + isBrushing: boolean; + initialized?: boolean; + isBrushingAvailable: boolean; + settings?: SettingsSpec; + internalChartRenderer: ( + containerRef: BackwardRef, + forwardStageRef: React.RefObject, + ) => JSX.Element | null; +} +interface ChartContainerComponentDispatchProps { + onPointerMove: typeof onPointerMoveAction; + onMouseUp: typeof onMouseUpAction; + onMouseDown: typeof onMouseDownAction; + onKeyPress: typeof onKeyPressAction; +} + +interface ChartContainerComponentOwnProps { + getChartContainerRef: BackwardRef; + forwardStageRef: React.RefObject; +} + +type ReactiveChartProps = ChartContainerComponentStateProps & + ChartContainerComponentDispatchProps & + ChartContainerComponentOwnProps; + +class ChartContainerComponent extends React.Component { + static displayName = 'ChartContainer'; + + shouldComponentUpdate(nextProps: ReactiveChartProps) { + return !deepEqual(this.props, nextProps); + } + + handleMouseMove = ({ + nativeEvent: { offsetX, offsetY, timeStamp }, + }: React.MouseEvent) => { + const { isChartEmpty, onPointerMove } = this.props; + if (isChartEmpty) { + return; + } + + onPointerMove( + { + x: offsetX, + y: offsetY, + }, + timeStamp, + ); + }; + + handleMouseLeave = ({ nativeEvent: { timeStamp } }: React.MouseEvent) => { + const { isChartEmpty, onPointerMove, isBrushing } = this.props; + if (isChartEmpty) { + return; + } + if (isBrushing) { + return; + } + onPointerMove({ x: -1, y: -1 }, timeStamp); + }; + + handleMouseDown = ({ + nativeEvent: { offsetX, offsetY, timeStamp }, + }: React.MouseEvent) => { + const { isChartEmpty, onMouseDown, isBrushingAvailable } = this.props; + if (isChartEmpty) { + return; + } + + if (isBrushingAvailable) { + window.addEventListener('mouseup', this.handleBrushEnd); + } + + window.addEventListener('keyup', this.handleKeyUp); + + onMouseDown( + { + x: offsetX, + y: offsetY, + }, + timeStamp, + ); + }; + + handleMouseUp = ({ nativeEvent: { offsetX, offsetY, timeStamp } }: React.MouseEvent) => { + const { isChartEmpty, onMouseUp } = this.props; + if (isChartEmpty) { + return; + } + + window.removeEventListener('keyup', this.handleKeyUp); + + onMouseUp( + { + x: offsetX, + y: offsetY, + }, + timeStamp, + ); + }; + + handleKeyUp = ({ key }: KeyboardEvent) => { + window.removeEventListener('keyup', this.handleKeyUp); + + const { isChartEmpty, onKeyPress } = this.props; + if (isChartEmpty) { + return; + } + + onKeyPress(key); + }; + + handleBrushEnd = () => { + const { onMouseUp } = this.props; + + window.removeEventListener('mouseup', this.handleBrushEnd); + + requestAnimationFrame(() => { + onMouseUp( + { + x: -1, + y: -1, + }, + Date.now(), + ); + }); + }; + + render() { + const { status, isChartEmpty, settings, initialized } = this.props; + + if (!initialized || status === InitStatus.ParentSizeInvalid) { + // TODO: Display error on chart + return null; + } + + if ( + status === InitStatus.ChartNotInitialized || + status === InitStatus.MissingChartType || + status === InitStatus.SpecNotInitialized || + isChartEmpty + ) { + return ; + } + + const { pointerCursor, internalChartRenderer, getChartContainerRef, forwardStageRef } = this.props; + return ( +
+ {internalChartRenderer(getChartContainerRef, forwardStageRef)} +
+ ); + } +} + +const mapDispatchToProps = (dispatch: Dispatch): ChartContainerComponentDispatchProps => + bindActionCreators( + { + onPointerMove: onPointerMoveAction, + onMouseUp: onMouseUpAction, + onMouseDown: onMouseDownAction, + onKeyPress: onKeyPressAction, + }, + dispatch, + ); +const mapStateToProps = (state: GlobalChartState): ChartContainerComponentStateProps => { + const status = getInternalIsInitializedSelector(state); + const settings = getSettingsSpecSelector(state); + const initialized = !state.specParsing && state.specsInitialized; + + if (status !== InitStatus.Initialized) { + return { + status, + initialized, + pointerCursor: DEFAULT_CSS_CURSOR, + isBrushingAvailable: false, + isBrushing: false, + internalChartRenderer: () => null, + settings, + }; + } + + return { + status, + initialized, + isChartEmpty: isInternalChartEmptySelector(state), + pointerCursor: getInternalPointerCursor(state), + isBrushingAvailable: getInternalIsBrushingAvailableSelector(state), + isBrushing: getInternalIsBrushingSelector(state), + internalChartRenderer: getInternalChartRendererSelector(state), + settings, + }; +}; + +/** @internal */ +export const ChartContainer = connect(mapStateToProps, mapDispatchToProps)(ChartContainerComponent); diff --git a/packages/osd-charts/src/components/chart_resizer.tsx b/packages/osd-charts/src/components/chart_resizer.tsx new file mode 100644 index 000000000000..29bbe1abee90 --- /dev/null +++ b/packages/osd-charts/src/components/chart_resizer.tsx @@ -0,0 +1,123 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { RefObject } from 'react'; +import { connect } from 'react-redux'; +import { Dispatch, bindActionCreators } from 'redux'; +import ResizeObserver from 'resize-observer-polyfill'; +import { debounce } from 'ts-debounce'; + +import { updateParentDimensions } from '../state/actions/chart_settings'; +import { GlobalChartState } from '../state/chart_state'; +import { getSettingsSpecSelector } from '../state/selectors/get_settings_specs'; +import { isDefined } from '../utils/common'; +import { Dimensions } from '../utils/dimensions'; + +interface ResizerStateProps { + resizeDebounce: number; +} + +interface ResizerDispatchProps { + updateParentDimensions(dimension: Dimensions): void; +} + +type ResizerProps = ResizerStateProps & ResizerDispatchProps; + +const DEFAULT_RESIZE_DEBOUNCE = 200; + +class Resizer extends React.Component { + private initialResizeComplete = false; + + private containerRef: RefObject; + + private ro: ResizeObserver; + + private animationFrameID: number | null; + + private onResizeDebounced: (entries: ResizeObserverEntry[]) => void; + + constructor(props: ResizerProps) { + super(props); + this.containerRef = React.createRef(); + this.ro = new ResizeObserver(this.handleResize); + this.animationFrameID = null; + this.onResizeDebounced = () => {}; + } + + componentDidMount() { + this.onResizeDebounced = debounce(this.onResize, this.props.resizeDebounce); + if (this.containerRef.current) { + this.ro.observe(this.containerRef.current as Element); + } + } + + componentWillUnmount() { + if (this.animationFrameID) { + window.cancelAnimationFrame(this.animationFrameID); + } + this.ro.disconnect(); + } + + onResize = (entries: ResizeObserverEntry[]) => { + if (!Array.isArray(entries)) { + return; + } + if (entries.length === 0 || !entries[0]) { + return; + } + const { width, height } = entries[0].contentRect; + this.animationFrameID = window.requestAnimationFrame(() => { + this.props.updateParentDimensions({ width, height, top: 0, left: 0 }); + }); + }; + + handleResize = (entries: ResizeObserverEntry[]) => { + if (this.initialResizeComplete) { + this.onResizeDebounced(entries); + } else { + this.initialResizeComplete = true; + this.onResize(entries); + } + }; + + render() { + return
; + } +} + +const mapDispatchToProps = (dispatch: Dispatch): ResizerDispatchProps => + bindActionCreators( + { + updateParentDimensions, + }, + dispatch, + ); + +const mapStateToProps = (state: GlobalChartState): ResizerStateProps => { + const settings = getSettingsSpecSelector(state); + const resizeDebounce = + settings.resizeDebounce === undefined || settings.resizeDebounce === null ? 200 : settings.resizeDebounce; + return { + resizeDebounce: + !isDefined(resizeDebounce) || Number.isNaN(resizeDebounce) ? DEFAULT_RESIZE_DEBOUNCE : resizeDebounce, + }; +}; + +/** @internal */ +export const ChartResizer = connect(mapStateToProps, mapDispatchToProps)(Resizer); diff --git a/packages/osd-charts/src/components/chart_status.tsx b/packages/osd-charts/src/components/chart_status.tsx new file mode 100644 index 000000000000..6fa00baff75d --- /dev/null +++ b/packages/osd-charts/src/components/chart_status.tsx @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { connect } from 'react-redux'; + +import { RenderChangeListener } from '../specs'; +import { GlobalChartState } from '../state/chart_state'; +import { getDebugStateSelector } from '../state/selectors/get_debug_state'; +import { getSettingsSpecSelector } from '../state/selectors/get_settings_specs'; +import { DebugState } from '../state/types'; + +interface ChartStatusStateProps { + rendered: boolean; + renderedCount: number; + onRenderChange?: RenderChangeListener; + debugState: DebugState | null; +} +class ChartStatusComponent extends React.Component { + componentDidMount() { + this.dispatchRenderChange(); + } + + componentDidUpdate() { + this.dispatchRenderChange(); + } + + dispatchRenderChange = () => { + const { onRenderChange, rendered } = this.props; + if (onRenderChange) { + window.requestAnimationFrame(() => { + onRenderChange(rendered); + }); + } + }; + + render() { + const { rendered, renderedCount, debugState } = this.props; + const debugStateString: string | null = debugState && JSON.stringify(debugState); + + return ( +
+ ); + } +} + +const mapStateToProps = (state: GlobalChartState): ChartStatusStateProps => { + const { onRenderChange, debugState } = getSettingsSpecSelector(state); + + return { + rendered: state.chartRendered, + renderedCount: state.chartRenderedCount, + onRenderChange, + debugState: debugState ? getDebugStateSelector(state) : null, + }; +}; + +/** @internal */ +export const ChartStatus = connect(mapStateToProps)(ChartStatusComponent); diff --git a/packages/osd-charts/src/components/error_boundary/error_boundary.tsx b/packages/osd-charts/src/components/error_boundary/error_boundary.tsx new file mode 100644 index 000000000000..482cef0f35ab --- /dev/null +++ b/packages/osd-charts/src/components/error_boundary/error_boundary.tsx @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Component, ReactNode } from 'react'; + +import { SettingsSpecProps } from '../../specs'; +import { NoResults } from '../no_results'; +import { isGracefulError } from './errors'; + +type ErrorBoundaryProps = { + children: ReactNode; + renderFn?: SettingsSpecProps['noResults']; +}; + +interface ErrorBoundaryState { + hasError: boolean; +} + +/** + * Error Boundary to catch and handle custom errors + * @internal + */ +export class ErrorBoundary extends Component { + hasError = false; + + componentDidUpdate() { + if (this.hasError) { + this.hasError = false; + } + } + + componentDidCatch(error: Error) { + if (isGracefulError(error)) { + this.hasError = true; + this.forceUpdate(); + } + } + + render() { + if (this.hasError) { + return ; + } + + return this.props.children; + } +} diff --git a/packages/osd-charts/src/components/error_boundary/errors.ts b/packages/osd-charts/src/components/error_boundary/errors.ts new file mode 100644 index 000000000000..9017fd507efc --- /dev/null +++ b/packages/osd-charts/src/components/error_boundary/errors.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $Values } from 'utility-types'; + +/** @public */ +export const ErrorType = Object.freeze({ + Graceful: 'graceful' as const, +}); +/** @public */ +export type ErrorType = $Values; + +/** + * Error to used to gracefully render empty chart + * @internal + */ +export class GracefulError extends Error { + type = ErrorType.Graceful; +} + +/** @internal */ +export function isGracefulError(error: Error): error is GracefulError { + return (error as GracefulError)?.type === ErrorType.Graceful; +} diff --git a/packages/osd-charts/src/components/error_boundary/index.tsx b/packages/osd-charts/src/components/error_boundary/index.tsx new file mode 100644 index 000000000000..c0ac8af0c06d --- /dev/null +++ b/packages/osd-charts/src/components/error_boundary/index.tsx @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* @internal */ +export { ErrorBoundary } from './error_boundary'; +/* @internal */ +export * from './errors'; diff --git a/packages/osd-charts/src/components/icons/_icon.scss b/packages/osd-charts/src/components/icons/_icon.scss new file mode 100644 index 000000000000..2d4a0c720e08 --- /dev/null +++ b/packages/osd-charts/src/components/icons/_icon.scss @@ -0,0 +1,15 @@ +.echIcon { + flex-shrink: 0; // Ensures it never scales down below it's intended size + display: inline-block; + vertical-align: middle; + fill: currentColor; + + svg { + transform: translate(0, 0); // Hack to fix Firefox "softness" + } + + &:focus { + opacity: 1; // We often hide icons on hover. Make sure they appear on focus. + background: $euiFocusBackgroundColor; + } +} diff --git a/packages/osd-charts/src/components/icons/_index.scss b/packages/osd-charts/src/components/icons/_index.scss new file mode 100644 index 000000000000..673336d14a12 --- /dev/null +++ b/packages/osd-charts/src/components/icons/_index.scss @@ -0,0 +1 @@ +@import 'icon'; diff --git a/packages/osd-charts/src/components/icons/assets/alert.tsx b/packages/osd-charts/src/components/icons/assets/alert.tsx new file mode 100644 index 000000000000..627d847ea330 --- /dev/null +++ b/packages/osd-charts/src/components/icons/assets/alert.tsx @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { IconComponentProps } from '../icon'; + +/** @internal */ +export function AlertIcon(extraProps: IconComponentProps) { + return ( + + + + ); +} diff --git a/packages/osd-charts/src/components/icons/assets/dot.tsx b/packages/osd-charts/src/components/icons/assets/dot.tsx new file mode 100644 index 000000000000..28e78788f8c4 --- /dev/null +++ b/packages/osd-charts/src/components/icons/assets/dot.tsx @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { deepEqual } from '../../../utils/fast_deep_equal'; +import { IconComponentProps } from '../icon'; + +/** @internal */ +export class DotIcon extends React.Component { + shouldComponentUpdate(nextProps: IconComponentProps) { + return !deepEqual(this.props, nextProps); + } + + render() { + return ( + + + + ); + } +} diff --git a/packages/osd-charts/src/components/icons/assets/empty.tsx b/packages/osd-charts/src/components/icons/assets/empty.tsx new file mode 100644 index 000000000000..a5dc26fd6ecf --- /dev/null +++ b/packages/osd-charts/src/components/icons/assets/empty.tsx @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { IconComponentProps } from '../icon'; + +/** @internal */ +export function EmptyIcon(extraProps: IconComponentProps) { + return ; +} diff --git a/packages/osd-charts/src/components/icons/assets/eye.tsx b/packages/osd-charts/src/components/icons/assets/eye.tsx new file mode 100644 index 000000000000..9ff08dceb694 --- /dev/null +++ b/packages/osd-charts/src/components/icons/assets/eye.tsx @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { IconComponentProps } from '../icon'; + +/** @internal */ +export function EyeIcon(extraProps: IconComponentProps) { + return ( + + + + ); +} diff --git a/packages/osd-charts/src/components/icons/assets/eye_closed.tsx b/packages/osd-charts/src/components/icons/assets/eye_closed.tsx new file mode 100644 index 000000000000..eef962d5427f --- /dev/null +++ b/packages/osd-charts/src/components/icons/assets/eye_closed.tsx @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { IconComponentProps } from '../icon'; + +/** @internal */ +export function EyeClosedIcon(extraProps: IconComponentProps) { + return ( + + + + ); +} diff --git a/packages/osd-charts/src/components/icons/assets/list.tsx b/packages/osd-charts/src/components/icons/assets/list.tsx new file mode 100644 index 000000000000..3ae800c6af46 --- /dev/null +++ b/packages/osd-charts/src/components/icons/assets/list.tsx @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { IconComponentProps } from '../icon'; + +/** @internal */ +export function ListIcon(extraProps: IconComponentProps) { + return ( + + + + ); +} diff --git a/packages/osd-charts/src/components/icons/assets/question_in_circle.tsx b/packages/osd-charts/src/components/icons/assets/question_in_circle.tsx new file mode 100644 index 000000000000..88fa99d988ac --- /dev/null +++ b/packages/osd-charts/src/components/icons/assets/question_in_circle.tsx @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { IconComponentProps } from '../icon'; + +/** @internal */ +export function QuestionInCircle(extraProps: IconComponentProps) { + return ( + + + + ); +} diff --git a/packages/osd-charts/src/components/icons/icon.tsx b/packages/osd-charts/src/components/icons/icon.tsx new file mode 100644 index 000000000000..10db98573284 --- /dev/null +++ b/packages/osd-charts/src/components/icons/icon.tsx @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import classNames from 'classnames'; +import React, { SVGAttributes, memo } from 'react'; + +import { deepEqual } from '../../utils/fast_deep_equal'; +import { AlertIcon } from './assets/alert'; +import { DotIcon } from './assets/dot'; +import { EmptyIcon } from './assets/empty'; +import { EyeIcon } from './assets/eye'; +import { EyeClosedIcon } from './assets/eye_closed'; +import { ListIcon } from './assets/list'; +import { QuestionInCircle } from './assets/question_in_circle'; + +const typeToIconMap = { + alert: AlertIcon, + dot: DotIcon, + empty: EmptyIcon, + eye: EyeIcon, + eyeClosed: EyeClosedIcon, + list: ListIcon, + questionInCircle: QuestionInCircle, +}; + +/** @internal */ +export type IconColor = string; + +/** @internal */ +export type IconType = keyof typeof typeToIconMap; + +interface IconProps { + className?: string; + 'aria-label'?: string; + 'data-test-subj'?: string; + type?: IconType; + color?: IconColor; +} + +/** @internal */ +export type IconComponentProps = Omit, 'color' | 'type'> & IconProps; + +function IconComponent({ type, color, className, tabIndex, ...rest }: IconComponentProps) { + let optionalCustomStyles = null; + + if (color) { + optionalCustomStyles = { color }; + } + + const classes = classNames('echIcon', className); + + const Svg = (type && typeToIconMap[type]) || EmptyIcon; + + /* + * This is a fix for IE and Edge, which ignores tabindex="-1" on an SVG, but respects + * focusable="false". + * - If there's no tab index specified, we'll default the icon to not be focusable, + * which is how SVGs behave in Chrome, Safari, and FF. + * - If tab index is -1, then the consumer wants the icon to not be focusable. + * - For all other values, the consumer wants the icon to be focusable. + */ + const focusable = tabIndex == null || tabIndex === -1 ? 'false' : 'true'; + + return ; +} +IconComponent.displayName = 'Icon'; + +/** @internal */ +export const Icon = memo(IconComponent, deepEqual); diff --git a/packages/osd-charts/src/components/index.ts b/packages/osd-charts/src/components/index.ts new file mode 100644 index 000000000000..069293954b71 --- /dev/null +++ b/packages/osd-charts/src/components/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { Chart } from './chart'; +export { Placement, TooltipPortalSettings } from './portal'; diff --git a/packages/osd-charts/src/components/legend/__snapshots__/legend.test.tsx.snap b/packages/osd-charts/src/components/legend/__snapshots__/legend.test.tsx.snap new file mode 100644 index 000000000000..91d1ef49ba19 --- /dev/null +++ b/packages/osd-charts/src/components/legend/__snapshots__/legend.test.tsx.snap @@ -0,0 +1,280 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Legend #legendColorPicker should match snapshot after onChange is called 1`] = ` +" +
  • +
    + + + + +
  • +
    +
  • +
    + + + + +
  • +
    +
  • +
    + + + + +
  • +
    +
  • +
    + + + + +
  • +
    " +`; + +exports[`Legend #legendColorPicker should match snapshot after onClose is called 1`] = ` +" +
  • +
    + + + + +
  • +
    +
  • +
    + + + + +
  • +
    +
  • +
    + + + + +
  • +
    +
  • +
    + + + + +
  • +
    " +`; + +exports[`Legend #legendColorPicker should render colorPicker when color is clicked 1`] = ` +"
    + + Custom Color Picker + + + +
    " +`; + +exports[`Legend #legendColorPicker should render colorPicker when color is clicked 2`] = ` +" +
  • +
    + + + + +
  • + +
    + + Custom Color Picker + + + +
    +
    +
    +
  • +
    + + + + +
  • +
    +
  • +
    + + + + +
  • +
    +
  • +
    + + + + +
  • +
    " +`; diff --git a/packages/osd-charts/src/components/legend/_index.scss b/packages/osd-charts/src/components/legend/_index.scss new file mode 100644 index 000000000000..6b6f3997ff4a --- /dev/null +++ b/packages/osd-charts/src/components/legend/_index.scss @@ -0,0 +1,3 @@ +@import 'variables'; +@import 'legend'; +@import 'legend_item'; diff --git a/packages/osd-charts/src/components/legend/_legend.scss b/packages/osd-charts/src/components/legend/_legend.scss new file mode 100644 index 000000000000..9ae3f20b946d --- /dev/null +++ b/packages/osd-charts/src/components/legend/_legend.scss @@ -0,0 +1,42 @@ +.echLegend { + .echLegendList { + display: grid; + grid-template-columns: minmax(0, 1fr); + } + &--horizontal { + .echLegendList { + grid-column-gap: $echLegendColumnGap; + grid-row-gap: $echLegendRowGap; + margin-top: $echLegendRowGap; + margin-bottom: $echLegendRowGap; + } + } + + &--top, + &--left { + order: 0; + } + + &--bottom, + &--right { + order: 1; + } + + &--debug { + background: rgba(238, 130, 238, 0.2); + position: relative; + } + + .echLegendListContainer { + @include euiYScrollWithShadows; + width: 100%; + overflow-y: auto; + overflow-x: hidden; + + :focus { + @include euiFocusRing; + background-color: $euiFocusBackgroundColor; + border-radius: $euiBorderRadius / 2; + } + } +} diff --git a/packages/osd-charts/src/components/legend/_legend_item.scss b/packages/osd-charts/src/components/legend/_legend_item.scss new file mode 100644 index 000000000000..c70377fae737 --- /dev/null +++ b/packages/osd-charts/src/components/legend/_legend_item.scss @@ -0,0 +1,106 @@ +$legendItemVerticalPadding: $echLegendRowGap / 2; +$legendItemHeight: #{$euiFontSizeXS * $euiLineHeight}; + +.echLegendItem { + color: $euiTextColor; + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; + position: relative; + + > *:not(.background) { + // euiPopover adds a div with height of 19px otherwise + height: $legendItemHeight; // prevents color dot from shifting + margin-left: $euiSizeXS; + + &:last-child:not(.echLegendItem__extra) { + margin-right: $euiSizeXS; + } + } + + &:not(&--hidden) { + .echLegendItem__color--changable { + cursor: pointer; + } + } + + &:hover .background { + background-color: $euiColorLightestShade; + } + + .background { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: -1; + } + + &__action { + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + max-width: calc(#{$legendItemHeight} + #{$euiSizeXS * 2}); + + .euiPopover, + .euiPopover__anchor, + .euiPopover__anchor > *:first-child { + // makes custom buttons in eui popover take full action size + height: 100%; + width: 100%; + } + } + + &__color { + display: flex; + line-height: 1.5; + align-items: center; + } + + &__label { + @include euiFontSizeXS; + @include euiTextTruncate; + flex: 1 1 auto; + text-align: left; + vertical-align: baseline; + letter-spacing: unset; + align-items: center; + + &--clickable { + &:hover { + cursor: pointer; + text-decoration: underline; + } + } + } + + &__extra { + @include euiFontSizeXS; + text-align: right; + flex: 0 0 auto; + margin-left: $euiSizeXS; + font-feature-settings: 'tnum'; + letter-spacing: unset; + } + + &--vertical { + padding-top: $legendItemVerticalPadding / 2; + padding-bottom: $legendItemVerticalPadding / 2; + + &:first-of-type { + margin-top: $legendItemVerticalPadding / 2; + } + + .background { + margin-top: $legendItemVerticalPadding / 2; + margin-bottom: $legendItemVerticalPadding / 2; + } + } + + &--hidden { + color: $euiColorDarkShade; + } +} diff --git a/packages/osd-charts/src/components/legend/_variables.scss b/packages/osd-charts/src/components/legend/_variables.scss new file mode 100644 index 000000000000..c8543808be60 --- /dev/null +++ b/packages/osd-charts/src/components/legend/_variables.scss @@ -0,0 +1,3 @@ +$echLegendMaxWidth: 200px; +$echLegendRowGap: 8px; +$echLegendColumnGap: 24px; diff --git a/packages/osd-charts/src/components/legend/color.tsx b/packages/osd-charts/src/components/legend/color.tsx new file mode 100644 index 000000000000..0ca937c53009 --- /dev/null +++ b/packages/osd-charts/src/components/legend/color.tsx @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { MouseEventHandler, forwardRef, memo } from 'react'; + +import { Icon } from '../icons/icon'; + +interface ColorProps { + color: string; + seriesName: string; + hasColorPicker: boolean; + isSeriesHidden?: boolean; + onClick?: MouseEventHandler; +} + +/** + * Color component used by the legend item + * @internal + */ +export const Color = memo( + forwardRef( + ({ color, seriesName, isSeriesHidden = false, hasColorPicker, onClick }, ref) => { + if (isSeriesHidden) { + return ( +
    + {/* changing the default viewBox for the eyeClosed icon to keep the same dimensions */} + +
    + ); + } + + if (hasColorPicker) { + return ( + + ); + } + + return ( +
    + +
    + ); + }, + ), +); +Color.displayName = 'Color'; diff --git a/packages/osd-charts/src/components/legend/extra.tsx b/packages/osd-charts/src/components/legend/extra.tsx new file mode 100644 index 000000000000..93a29b900032 --- /dev/null +++ b/packages/osd-charts/src/components/legend/extra.tsx @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +/** + * @internal + * @param extra + * @param isSeriesHidden + */ +export function renderExtra(extra: string | number) { + return ( +
    + {extra} +
    + ); +} diff --git a/packages/osd-charts/src/components/legend/label.tsx b/packages/osd-charts/src/components/legend/label.tsx new file mode 100644 index 000000000000..e51303a42e62 --- /dev/null +++ b/packages/osd-charts/src/components/legend/label.tsx @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import classNames from 'classnames'; +import React, { MouseEventHandler } from 'react'; + +interface LabelProps { + label: string; + isSeriesHidden?: boolean; + isToggleable?: boolean; + onClick?: MouseEventHandler; +} +/** + * Label component used to display text in legend item + * @internal + */ +export function Label({ label, isToggleable, onClick, isSeriesHidden }: LabelProps) { + const labelClassNames = classNames('echLegendItem__label', { + 'echLegendItem__label--clickable': Boolean(onClick), + }); + + return isToggleable ? ( + + ) : ( +
    + {label} +
    + ); +} diff --git a/packages/osd-charts/src/components/legend/legend.test.tsx b/packages/osd-charts/src/components/legend/legend.test.tsx new file mode 100644 index 000000000000..179f5b2ddb4c --- /dev/null +++ b/packages/osd-charts/src/components/legend/legend.test.tsx @@ -0,0 +1,310 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mount, ReactWrapper } from 'enzyme'; +import React, { Component } from 'react'; + +import { SeededDataGenerator } from '../../mocks/utils'; +import { ScaleType } from '../../scales/constants'; +import { Settings, BarSeries, LegendColorPicker } from '../../specs'; +import { Chart } from '../chart'; +import { Legend } from './legend'; +import { LegendListItem } from './legend_item'; + +const dg = new SeededDataGenerator(); + +describe('Legend', () => { + it('shall render the all the series names', () => { + const wrapper = mount( + + + + , + ); + const legendWrapper = wrapper.find(Legend); + expect(legendWrapper.exists).toBeTruthy(); + const legendItems = legendWrapper.find(LegendListItem); + expect(legendItems.exists).toBeTruthy(); + expect(legendItems).toHaveLength(4); + legendItems.forEach((legendItem, i) => { + // the legend item shows also the value as default parameter + expect(legendItem.text()).toBe(`group${i}123`); + }); + }); + it('shall render the all the series names without the data value', () => { + const wrapper = mount( + + + + , + ); + const legendWrapper = wrapper.find(Legend); + expect(legendWrapper.exists).toBeTruthy(); + const legendItems = legendWrapper.find(LegendListItem); + expect(legendItems.exists).toBeTruthy(); + expect(legendItems).toHaveLength(4); + legendItems.forEach((legendItem, i) => { + // the legend item shows also the value as default parameter + expect(legendItem.text()).toBe(`group${i}`); + }); + }); + it('shall call the over and out listeners for every list item', () => { + const onLegendItemOver = jest.fn(); + const onLegendItemOut = jest.fn(); + const numberOfSeries = 4; + const data = dg.generateGroupedSeries(10, numberOfSeries, 'split'); + const wrapper = mount( + + + + , + ); + const legendWrapper = wrapper.find(Legend); + expect(legendWrapper.exists).toBeTruthy(); + const legendItems = legendWrapper.find(LegendListItem); + expect(legendItems.exists).toBeTruthy(); + legendItems.forEach((legendItem, i) => { + legendItem.simulate('mouseenter'); + expect(onLegendItemOver).toBeCalledTimes(i + 1); + legendItem.simulate('mouseleave'); + expect(onLegendItemOut).toBeCalledTimes(i + 1); + }); + }); + it('shall call click listener for every list item', () => { + const onLegendItemClick = jest.fn(); + const numberOfSeries = 4; + const data = dg.generateGroupedSeries(10, numberOfSeries, 'split'); + const wrapper = mount( + + + + , + ); + const legendWrapper = wrapper.find(Legend); + expect(legendWrapper.exists).toBeTruthy(); + const legendItems = legendWrapper.find(LegendListItem); + expect(legendItems.exists).toBeTruthy(); + expect(legendItems).toHaveLength(4); + legendItems.forEach((legendItem, i) => { + // the click is only enabled on the title + legendItem.find('.echLegendItem__label').simulate('click'); + expect(onLegendItemClick).toBeCalledTimes(i + 1); + }); + }); + + describe('#legendColorPicker', () => { + class LegendColorPickerMock extends Component< + { onLegendItemClick: () => void; customColor: string }, + { colors: string[] } + > { + state = { + colors: ['red'], + }; + + data = dg.generateGroupedSeries(10, 4, 'split'); + + legendColorPickerFn: LegendColorPicker = ({ onClose }) => ( +
    + Custom Color Picker + + +
    + ); + + render() { + return ( + + + + + ); + } + } + + let wrapper: ReactWrapper; + const customColor = '#0c7b93'; + const onLegendItemClick = jest.fn(); + + beforeEach(() => { + wrapper = mount(); + }); + + const clickFirstColor = () => { + const legendWrapper = wrapper.find(Legend); + expect(legendWrapper.exists).toBeTruthy(); + const legendItems = legendWrapper.find(LegendListItem); + expect(legendItems.exists).toBeTruthy(); + expect(legendItems).toHaveLength(4); + legendItems.first().find('.echLegendItem__color').simulate('click'); + }; + + it('should render colorPicker when color is clicked', () => { + clickFirstColor(); + expect(wrapper.find('#colorPicker').debug()).toMatchSnapshot(); + expect( + wrapper + .find(LegendListItem) + .map((e) => e.debug()) + .join(''), + ).toMatchSnapshot(); + }); + + it('should match snapshot after onChange is called', () => { + clickFirstColor(); + wrapper.find('#change').simulate('click').first(); + + expect( + wrapper + .find(LegendListItem) + .map((e) => e.debug()) + .join(''), + ).toMatchSnapshot(); + }); + + it('should set isOpen to false after onChange is called', () => { + clickFirstColor(); + wrapper.find('#change').simulate('click').first(); + expect(wrapper.find('#colorPicker').exists()).toBe(false); + }); + + it('should set color after onChange is called', () => { + clickFirstColor(); + wrapper.find('#change').simulate('click').first(); + const dot = wrapper.find('.echLegendItem__color svg'); + expect(dot.exists(`[color="${customColor}"]`)).toBe(true); + }); + + it('should match snapshot after onClose is called', () => { + clickFirstColor(); + wrapper.find('#close').simulate('click').first(); + expect( + wrapper + .find(LegendListItem) + .map((e) => e.debug()) + .join(''), + ).toMatchSnapshot(); + }); + + it('should set isOpen to false after onClose is called', () => { + clickFirstColor(); + wrapper.find('#close').simulate('click').first(); + expect(wrapper.find('#colorPicker').exists()).toBe(false); + }); + + it('should call click listener for every list item', () => { + const legendWrapper = wrapper.find(Legend); + expect(legendWrapper.exists).toBeTruthy(); + const legendItems = legendWrapper.find(LegendListItem); + expect(legendItems.exists).toBeTruthy(); + expect(legendItems).toHaveLength(4); + legendItems.forEach((legendItem, i) => { + // toggle click is only enabled on the title + legendItem.find('.echLegendItem__label').simulate('click'); + expect(onLegendItemClick).toBeCalledTimes(i + 1); + }); + }); + }); + describe('disable toggle and click for one legend item', () => { + it('should not be able to click or focus if there is only one legend item in total legend items', () => { + const onLegendItemClick = jest.fn(); + const data = [{ x: 2, y: 5 }]; + const wrapper = mount( + + + + , + ); + const legendItems = wrapper.find(LegendListItem); + expect(legendItems.length).toBe(1); + legendItems.forEach((legendItem) => { + // the click is only enabled on the title + legendItem.find('.echLegendItem__label').simulate('click'); + expect(onLegendItemClick).toBeCalledTimes(0); + }); + }); + }); +}); diff --git a/packages/osd-charts/src/components/legend/legend.tsx b/packages/osd-charts/src/components/legend/legend.tsx new file mode 100644 index 000000000000..ca975151df21 --- /dev/null +++ b/packages/osd-charts/src/components/legend/legend.tsx @@ -0,0 +1,176 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import classNames from 'classnames'; +import React from 'react'; +import { connect } from 'react-redux'; +import { Dispatch, bindActionCreators } from 'redux'; + +import { LegendItem, LegendItemExtraValues } from '../../common/legend'; +import { DEFAULT_LEGEND_CONFIG, LegendSpec } from '../../specs'; +import { clearTemporaryColors, setTemporaryColor, setPersistedColor } from '../../state/actions/colors'; +import { + onToggleDeselectSeriesAction, + onLegendItemOutAction, + onLegendItemOverAction, +} from '../../state/actions/legend'; +import { GlobalChartState } from '../../state/chart_state'; +import { getChartThemeSelector } from '../../state/selectors/get_chart_theme'; +import { getInternalIsInitializedSelector, InitStatus } from '../../state/selectors/get_internal_is_intialized'; +import { getInternalMainProjectionAreaSelector } from '../../state/selectors/get_internal_main_projection_area'; +import { getInternalProjectionContainerAreaSelector } from '../../state/selectors/get_internal_projection_container_area'; +import { getLegendConfigSelector } from '../../state/selectors/get_legend_config_selector'; +import { getLegendItemsSelector } from '../../state/selectors/get_legend_items'; +import { getLegendExtraValuesSelector } from '../../state/selectors/get_legend_items_values'; +import { getLegendSizeSelector } from '../../state/selectors/get_legend_size'; +import { getSettingsSpecSelector } from '../../state/selectors/get_settings_specs'; +import { BBox } from '../../utils/bbox/bbox_calculator'; +import { HorizontalAlignment, LayoutDirection, VerticalAlignment } from '../../utils/common'; +import { Dimensions } from '../../utils/dimensions'; +import { LIGHT_THEME } from '../../utils/themes/light_theme'; +import { Theme } from '../../utils/themes/theme'; +import { LegendItemProps, renderLegendItem } from './legend_item'; +import { getLegendPositionConfig, legendPositionStyle } from './position_style'; +import { getLegendStyle, getLegendListStyle } from './style_utils'; + +interface LegendStateProps { + debug: boolean; + chartDimensions: Dimensions; + containerDimensions: Dimensions; + chartTheme: Theme; + size: BBox; + config: LegendSpec; + items: LegendItem[]; + extraValues: Map; +} + +interface LegendDispatchProps { + onItemOutAction: typeof onLegendItemOutAction; + onItemOverAction: typeof onLegendItemOverAction; + onToggleDeselectSeriesAction: typeof onToggleDeselectSeriesAction; + clearTemporaryColors: typeof clearTemporaryColors; + setTemporaryColor: typeof setTemporaryColor; + setPersistedColor: typeof setPersistedColor; +} + +function LegendComponent(props: LegendStateProps & LegendDispatchProps) { + const { + items, + size, + debug, + chartTheme: { chartMargins, legend }, + chartDimensions, + containerDimensions, + config, + } = props; + + if (items.length === 0 || items.every(({ isItemHidden }) => isItemHidden)) { + return null; + } + + const positionConfig = getLegendPositionConfig(config.legendPosition); + const containerStyle = getLegendStyle(positionConfig, size, legend.margin); + const listStyle = getLegendListStyle(positionConfig, chartMargins, legend, items.length); + + const legendClasses = classNames('echLegend', { + 'echLegend--debug': debug, + 'echLegend--horizontal': positionConfig.direction === LayoutDirection.Horizontal, + 'echLegend--vertical': positionConfig.direction === LayoutDirection.Vertical, + 'echLegend--left': positionConfig.hAlign === HorizontalAlignment.Left, + 'echLegend--right': positionConfig.hAlign === HorizontalAlignment.Right, + 'echLegend--top': positionConfig.vAlign === VerticalAlignment.Top, + 'echLegend--bottom': positionConfig.vAlign === VerticalAlignment.Bottom, + }); + + const itemProps: Omit = { + positionConfig, + totalItems: items.length, + extraValues: props.extraValues, + showExtra: config.showLegendExtra, + onMouseOut: config.onLegendItemOut, + onMouseOver: config.onLegendItemOver, + onClick: config.onLegendItemClick, + clearTemporaryColorsAction: props.clearTemporaryColors, + setPersistedColorAction: props.setPersistedColor, + setTemporaryColorAction: props.setTemporaryColor, + mouseOutAction: props.onItemOutAction, + mouseOverAction: props.onItemOverAction, + toggleDeselectSeriesAction: props.onToggleDeselectSeriesAction, + colorPicker: config.legendColorPicker, + action: config.legendAction, + }; + const positionStyle = legendPositionStyle(config, size, chartDimensions, containerDimensions); + return ( +
    +
    +
      + {items.map((item, index) => renderLegendItem(item, itemProps, items.length, index))} +
    +
    +
    + ); +} + +const mapDispatchToProps = (dispatch: Dispatch): LegendDispatchProps => + bindActionCreators( + { + onToggleDeselectSeriesAction, + onItemOutAction: onLegendItemOutAction, + onItemOverAction: onLegendItemOverAction, + clearTemporaryColors, + setTemporaryColor, + setPersistedColor, + }, + dispatch, + ); + +const EMPTY_DEFAULT_STATE: LegendStateProps = { + chartDimensions: { width: 0, height: 0, left: 0, top: 0 }, + containerDimensions: { width: 0, height: 0, left: 0, top: 0 }, + items: [], + extraValues: new Map(), + debug: false, + chartTheme: LIGHT_THEME, + size: { width: 0, height: 0 }, + config: DEFAULT_LEGEND_CONFIG, +}; + +const mapStateToProps = (state: GlobalChartState): LegendStateProps => { + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return EMPTY_DEFAULT_STATE; + } + const config = getLegendConfigSelector(state); + if (!config.showLegend) { + return EMPTY_DEFAULT_STATE; + } + const { debug } = getSettingsSpecSelector(state); + return { + debug, + chartDimensions: getInternalMainProjectionAreaSelector(state), + containerDimensions: getInternalProjectionContainerAreaSelector(state), + chartTheme: getChartThemeSelector(state), + size: getLegendSizeSelector(state), + items: getLegendItemsSelector(state), + extraValues: getLegendExtraValuesSelector(state), + config, + }; +}; + +/** @internal */ +export const Legend = connect(mapStateToProps, mapDispatchToProps)(LegendComponent); diff --git a/packages/osd-charts/src/components/legend/legend_item.tsx b/packages/osd-charts/src/components/legend/legend_item.tsx new file mode 100644 index 000000000000..f378b4c169c7 --- /dev/null +++ b/packages/osd-charts/src/components/legend/legend_item.tsx @@ -0,0 +1,238 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import classNames from 'classnames'; +import React, { Component, createRef, MouseEventHandler } from 'react'; + +import { LegendItem, LegendItemExtraValues } from '../../common/legend'; +import { SeriesIdentifier } from '../../common/series_id'; +import { + LegendItemListener, + BasicListener, + LegendColorPicker, + LegendAction, + LegendPositionConfig, +} from '../../specs/settings'; +import { + clearTemporaryColors as clearTemporaryColorsAction, + setTemporaryColor as setTemporaryColorAction, + setPersistedColor as setPersistedColorAction, +} from '../../state/actions/colors'; +import { + onLegendItemOutAction, + onLegendItemOverAction, + onToggleDeselectSeriesAction, +} from '../../state/actions/legend'; +import { Color, LayoutDirection } from '../../utils/common'; +import { deepEqual } from '../../utils/fast_deep_equal'; +import { Color as ItemColor } from './color'; +import { renderExtra } from './extra'; +import { Label as ItemLabel } from './label'; +import { getExtra } from './utils'; + +/** @internal */ +export const LEGEND_HIERARCHY_MARGIN = 10; + +/** @internal */ +export interface LegendItemProps { + item: LegendItem; + totalItems: number; + positionConfig: LegendPositionConfig; + extraValues: Map; + showExtra: boolean; + colorPicker?: LegendColorPicker; + action?: LegendAction; + onClick?: LegendItemListener; + onMouseOut?: BasicListener; + onMouseOver?: LegendItemListener; + mouseOutAction: typeof onLegendItemOutAction; + mouseOverAction: typeof onLegendItemOverAction; + clearTemporaryColorsAction: typeof clearTemporaryColorsAction; + setTemporaryColorAction: typeof setTemporaryColorAction; + setPersistedColorAction: typeof setPersistedColorAction; + toggleDeselectSeriesAction: typeof onToggleDeselectSeriesAction; +} + +interface LegendItemState { + isOpen: boolean; + actionActive: boolean; +} + +/** @internal */ +export class LegendListItem extends Component { + static displayName = 'LegendItem'; + + shouldClearPersistedColor = false; + + colorRef = createRef(); + + state: LegendItemState = { + isOpen: false, + actionActive: false, + }; + + shouldComponentUpdate(nextProps: LegendItemProps, nextState: LegendItemState) { + return !deepEqual(this.props, nextProps) || !deepEqual(this.state, nextState); + } + + handleColorClick = (changeable: boolean): MouseEventHandler | undefined => + changeable + ? (event) => { + event.stopPropagation(); + this.toggleIsOpen(); + } + : undefined; + + toggleIsOpen = () => { + this.setState(({ isOpen }) => ({ isOpen: !isOpen })); + }; + + onLegendItemMouseOver = () => { + const { onMouseOver, mouseOverAction, item } = this.props; + // call the settings listener directly if available + if (onMouseOver) { + onMouseOver(item.seriesIdentifiers); + } + mouseOverAction(item.path); + }; + + onLegendItemMouseOut = () => { + const { onMouseOut, mouseOutAction } = this.props; + // call the settings listener directly if available + if (onMouseOut) { + onMouseOut(); + } + mouseOutAction(); + }; + + /** + * Returns click function only if toggleable or click listern is provided + */ + handleLabelClick = (legendItemId: SeriesIdentifier[]): MouseEventHandler | undefined => { + const { item, onClick, toggleDeselectSeriesAction, totalItems } = this.props; + if (totalItems <= 1 || (!item.isToggleable && !onClick)) { + return; + } + + return ({ shiftKey }) => { + if (onClick) { + onClick(legendItemId); + } + + if (item.isToggleable) { + toggleDeselectSeriesAction(legendItemId, shiftKey); + } + }; + }; + + renderColorPicker() { + const { + colorPicker: ColorPicker, + item, + clearTemporaryColorsAction, + setTemporaryColorAction, + setPersistedColorAction, + } = this.props; + const { seriesIdentifiers, color } = item; + const seriesKeys = seriesIdentifiers.map(({ key }) => key); + const handleClose = () => { + setPersistedColorAction(seriesKeys, this.shouldClearPersistedColor ? null : color); + clearTemporaryColorsAction(); + this.toggleIsOpen(); + }; + const handleChange = (c: Color | null) => { + this.shouldClearPersistedColor = c === null; + setTemporaryColorAction(seriesKeys, c); + }; + if (ColorPicker && this.state.isOpen && this.colorRef.current) { + return ( + + ); + } + } + + render() { + const { extraValues, item, showExtra, colorPicker, totalItems, action: Action, positionConfig } = this.props; + const { color, isSeriesHidden, isItemHidden, seriesIdentifiers, label } = item; + + if (isItemHidden) return null; + + const itemClassNames = classNames('echLegendItem', { + 'echLegendItem--hidden': isSeriesHidden, + 'echLegendItem--vertical': positionConfig.direction === LayoutDirection.Vertical, + }); + const hasColorPicker = Boolean(colorPicker); + const extra = showExtra && getExtra(extraValues, item, totalItems); + const style = item.depth + ? { + marginLeft: LEGEND_HIERARCHY_MARGIN * (item.depth ?? 0), + } + : undefined; + return ( + <> +
  • +
    + + 1 && item.isToggleable} + onClick={this.handleLabelClick(seriesIdentifiers)} + isSeriesHidden={isSeriesHidden} + /> + {extra && !isSeriesHidden && renderExtra(extra)} + {Action && ( +
    + +
    + )} +
  • + {this.renderColorPicker()} + + ); + } +} + +/** @internal */ +export function renderLegendItem( + item: LegendItem, + props: Omit, + totalItems: number, + index: number, +) { + return ; +} diff --git a/packages/osd-charts/src/components/legend/position_style.ts b/packages/osd-charts/src/components/legend/position_style.ts new file mode 100644 index 000000000000..69500774f7cf --- /dev/null +++ b/packages/osd-charts/src/components/legend/position_style.ts @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CSSProperties } from 'react'; + +import { LegendSpec, LegendPositionConfig } from '../../specs/settings'; +import { BBox } from '../../utils/bbox/bbox_calculator'; +import { LayoutDirection, Position } from '../../utils/common'; +import { Dimensions } from '../../utils/dimensions'; + +const INSIDE_PADDING = 10; + +/** @internal */ +export function legendPositionStyle( + { legendPosition }: LegendSpec, + legendSize: BBox, + chart: Dimensions, + container: Dimensions, +): CSSProperties { + const { vAlign, hAlign, direction, floating } = getLegendPositionConfig(legendPosition); + // non-float legend doesn't need a special handling + if (!floating) { + return {}; + } + + const { Left, Right, Top, Bottom } = Position; + + if (direction === LayoutDirection.Vertical) { + return { + position: 'absolute', + zIndex: 1, + right: hAlign === Right ? container.width - chart.width - chart.left + INSIDE_PADDING : undefined, + left: hAlign === Left ? chart.left + INSIDE_PADDING : undefined, + top: vAlign === Top ? chart.top : undefined, + bottom: vAlign === Bottom ? container.height - chart.top - chart.height : undefined, + height: !floating && legendSize.height >= chart.height ? chart.height : undefined, + }; + } + + return { + position: 'absolute', + zIndex: 1, + right: INSIDE_PADDING, + left: chart.left + INSIDE_PADDING, + top: vAlign === Top ? chart.top : undefined, + bottom: vAlign === Bottom ? container.height - chart.top - chart.height : undefined, + height: !floating && legendSize.height >= chart.height ? chart.height : undefined, + }; +} + +/** @internal */ +export const LEGEND_TO_FULL_CONFIG: Record = { + [Position.Left]: { + vAlign: Position.Top, + hAlign: Position.Left, + direction: LayoutDirection.Vertical, + floating: false, + floatingColumns: 1, + }, + [Position.Top]: { + vAlign: Position.Top, + hAlign: Position.Left, + direction: LayoutDirection.Horizontal, + floating: false, + floatingColumns: 1, + }, + [Position.Bottom]: { + vAlign: Position.Bottom, + hAlign: Position.Left, + direction: LayoutDirection.Horizontal, + floating: false, + floatingColumns: 1, + }, + [Position.Right]: { + vAlign: Position.Top, + hAlign: Position.Right, + direction: LayoutDirection.Vertical, + floating: false, + floatingColumns: 1, + }, +}; + +/** + * @internal + */ +export function getLegendPositionConfig(position: LegendSpec['legendPosition']): LegendPositionConfig { + return typeof position === 'object' ? position : LEGEND_TO_FULL_CONFIG[position]; +} diff --git a/packages/osd-charts/src/components/legend/style_utils.ts b/packages/osd-charts/src/components/legend/style_utils.ts new file mode 100644 index 000000000000..53d57289cd60 --- /dev/null +++ b/packages/osd-charts/src/components/legend/style_utils.ts @@ -0,0 +1,98 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegendPositionConfig } from '../../specs/settings'; +import { BBox } from '../../utils/bbox/bbox_calculator'; +import { clamp, LayoutDirection } from '../../utils/common'; +import { Margins } from '../../utils/dimensions'; +import { LegendStyle as ThemeLegendStyle } from '../../utils/themes/theme'; + +/** @internal */ +export type LegendStyle = + | { + width?: string; + maxWidth?: string; + marginLeft?: number; + marginRight?: number; + } + | { + height?: string; + maxHeight?: string; + marginTop?: number; + marginBottom?: number; + }; + +/** @internal */ +export interface LegendListStyle { + paddingTop?: number | string; + paddingBottom?: number | string; + paddingLeft?: number | string; + paddingRight?: number | string; + gridTemplateColumns?: string; +} +/** + * Get the legend list style + * @internal + */ +export function getLegendListStyle( + { direction, floating, floatingColumns }: LegendPositionConfig, + chartMargins: Margins, + legendStyle: ThemeLegendStyle, + totalItems: number, +): LegendListStyle { + const { top: paddingTop, bottom: paddingBottom, left: paddingLeft, right: paddingRight } = chartMargins; + + if (direction === LayoutDirection.Horizontal) { + return { + paddingLeft, + paddingRight, + gridTemplateColumns: `repeat(auto-fill, minmax(${legendStyle.verticalWidth}px, 1fr))`, + }; + } + + return { + paddingTop, + paddingBottom, + ...(floating && { + gridTemplateColumns: `repeat(${clamp(floatingColumns ?? 1, 1, totalItems)}, auto)`, + }), + }; +} +/** + * Get the legend global style + * @internal + */ +export function getLegendStyle({ direction, floating }: LegendPositionConfig, size: BBox, margin: number): LegendStyle { + if (direction === LayoutDirection.Vertical) { + const width = `${size.width}px`; + return { + width: floating ? undefined : width, + maxWidth: floating ? undefined : width, + marginLeft: margin, + marginRight: margin, + }; + } + const height = `${size.height}px`; + return { + height, + maxHeight: height, + marginTop: margin, + marginBottom: margin, + }; +} diff --git a/packages/osd-charts/src/components/legend/utils.ts b/packages/osd-charts/src/components/legend/utils.ts new file mode 100644 index 000000000000..08628085b195 --- /dev/null +++ b/packages/osd-charts/src/components/legend/utils.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegendItemExtraValues, LegendItem } from '../../common/legend'; + +/** @internal */ +export function getExtra(extraValues: Map, item: LegendItem, totalItems: number) { + const { seriesIdentifiers, defaultExtra, childId, path } = item; + // don't show extra if the legend item is associated with multiple series + if (extraValues.size === 0 || seriesIdentifiers.length > 1) { + return defaultExtra?.formatted ?? ''; + } + const [{ key }] = seriesIdentifiers; + const extraValueKey = path.map(({ index }) => index).join('__'); + const itemExtraValues = extraValues.has(extraValueKey) ? extraValues.get(extraValueKey) : extraValues.get(key); + const actionExtra = (childId && itemExtraValues?.get(childId)) ?? null; + if (extraValues.size !== totalItems) { + if (actionExtra != null) { + return actionExtra; + } + return ''; + } + return actionExtra !== null ? actionExtra : defaultExtra?.formatted ?? ''; +} diff --git a/packages/osd-charts/src/components/no_results.tsx b/packages/osd-charts/src/components/no_results.tsx new file mode 100644 index 000000000000..7012c65975cd --- /dev/null +++ b/packages/osd-charts/src/components/no_results.tsx @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC, Suspense } from 'react'; + +import { SettingsSpecProps } from '../specs'; + +interface NoResultsProps { + renderFn?: SettingsSpecProps['noResults']; +} + +/** @internal */ +export const NoResults: FC = ({ renderFn }) => ( + null}> +
    {renderFn ??

    No data to display

    }
    +
    +); diff --git a/packages/osd-charts/src/components/portal/_index.scss b/packages/osd-charts/src/components/portal/_index.scss new file mode 100644 index 000000000000..962b697877b3 --- /dev/null +++ b/packages/osd-charts/src/components/portal/_index.scss @@ -0,0 +1 @@ +@import 'portal'; diff --git a/packages/osd-charts/src/components/portal/_portal.scss b/packages/osd-charts/src/components/portal/_portal.scss new file mode 100644 index 000000000000..924e80a136f7 --- /dev/null +++ b/packages/osd-charts/src/components/portal/_portal.scss @@ -0,0 +1,15 @@ +[id^='echTooltipPortal'] { + pointer-events: none; +} + +[id^='echAnchor'] { + position: absolute; + pointer-events: none; +} + +.echTooltipPortal__invisible { + position: fixed; + visibility: hidden; + width: 0; + height: 0; +} diff --git a/packages/osd-charts/src/components/portal/index.ts b/packages/osd-charts/src/components/portal/index.ts new file mode 100644 index 000000000000..9e2e599d2f39 --- /dev/null +++ b/packages/osd-charts/src/components/portal/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './tooltip_portal'; +export * from './types'; +export * from './utils'; diff --git a/packages/osd-charts/src/components/portal/tooltip_portal.tsx b/packages/osd-charts/src/components/portal/tooltip_portal.tsx new file mode 100644 index 000000000000..e9dc102d2170 --- /dev/null +++ b/packages/osd-charts/src/components/portal/tooltip_portal.tsx @@ -0,0 +1,236 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createPopper, Instance } from '@popperjs/core'; +import { useRef, useEffect, useCallback, ReactNode, useMemo } from 'react'; +import { createPortal } from 'react-dom'; + +import { mergePartial, isDefined } from '../../utils/common'; +import { Padding } from '../../utils/dimensions'; +import { TooltipPortalSettings, PortalAnchorRef } from './types'; +import { DEFAULT_POPPER_SETTINGS, getOrCreateNode, isHTMLElement } from './utils'; + +/** + * @todo make this type conditional to use PortalAnchorProps or PortalAnchorRefProps + */ +type PortalTooltipProps = { + zIndex: number; + /** + * String used to designate unique portal + */ + scope: string; + /** + * children to render inside the tooltip + */ + children: ReactNode; + /** + * Used to determine if tooltip is visible + */ + visible: boolean; + /** + * Settings to control portal positioning + */ + settings?: TooltipPortalSettings; + /** + * Anchor element to use as position reference + */ + anchor: HTMLElement | PortalAnchorRef | null; + /** + * Chart Id to add new anchor for each chart on the page + */ + chartId: string; +}; + +function addToPadding(padding: Partial | number = 0, extra: number = 0): Padding | number | undefined { + if (typeof padding === 'number') return padding + extra; + + const { top = 0, right = 0, bottom = 0, left = 0 } = padding; + + return { + top: top + extra, + right: right + extra, + bottom: bottom + extra, + left: left + extra, + }; +} + +const TooltipPortalComponent = ({ + anchor, + scope, + settings, + children, + visible, + chartId, + zIndex, +}: PortalTooltipProps) => { + /** + * Anchor element used to position tooltip + */ + const anchorNode = useRef( + isHTMLElement(anchor) + ? anchor + : getOrCreateNode(`echAnchor${scope}__${chartId}`, undefined, anchor?.ref ?? undefined), + ); + + /** + * This must not be removed from DOM throughout life of this component. + * Otherwise the portal will loose reference to the correct node. + */ + const portalNodeElement = getOrCreateNode( + `echTooltipPortal${scope}__${chartId}`, + 'echTooltipPortal__invisible', + undefined, + zIndex, + ); + + const portalNode = useRef(portalNodeElement); + + /** + * Popper instance used to manage position of tooltip. + */ + const popper = useRef(null); + + const popperSettings = useMemo( + () => mergePartial(DEFAULT_POPPER_SETTINGS, settings, { mergeOptionalPartialValues: true }), + + [settings], + ); + + const anchorPosition = (anchor as PortalAnchorRef)?.position; + const position = useMemo(() => (isHTMLElement(anchor) ? null : anchorPosition), [anchor, anchorPosition]); + + const destroyPopper = useCallback(() => { + if (popper.current) { + popper.current.destroy(); + popper.current = null; + } + }, []); + + const setPopper = useCallback(() => { + if (!isDefined(anchorNode.current) || !visible) { + return; + } + + const { fallbackPlacements, placement, boundary, offset, boundaryPadding } = popperSettings; + popper.current = createPopper(anchorNode.current, portalNode.current, { + strategy: 'absolute', + placement, + modifiers: [ + { + name: 'offset', + options: { + offset: [0, offset], + }, + }, + { + name: 'preventOverflow', + options: { + boundary, + padding: boundaryPadding, + }, + }, + { + name: 'flip', + options: { + // Note: duplicate values causes lag + fallbackPlacements: fallbackPlacements.filter((p) => p !== placement), + boundary, + // checks main axis overflow before trying to flip + altAxis: false, + padding: addToPadding(boundaryPadding, offset), + }, + }, + ], + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + visible, + popperSettings.fallbackPlacements, + popperSettings.placement, + popperSettings.boundary, + popperSettings.offset, + ]); + + useEffect(() => { + setPopper(); + const nodeCopy = portalNode.current; + + return () => { + if (nodeCopy.parentNode) { + nodeCopy.parentNode.removeChild(nodeCopy); + } + + destroyPopper(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + destroyPopper(); + setPopper(); + }, [destroyPopper, setPopper, popperSettings]); + + useEffect(() => { + if (!visible) { + destroyPopper(); + } else if (!popper.current) { + setPopper(); + } + }, [destroyPopper, setPopper, visible]); + + const updateAnchorDimensions = useCallback(() => { + if (!position || !visible) { + return; + } + + const { x, y, width, height } = position; + anchorNode.current.style.transform = `translate(${x}px, ${y}px)`; + + if (isDefined(width)) { + anchorNode.current.style.width = `${width}px`; + } + + if (isDefined(height)) { + anchorNode.current.style.height = `${height}px`; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [visible, anchorNode, position?.x, position?.y, position?.width, position?.height]); + + useEffect(() => { + if (!position) { + portalNode.current.classList.add('echTooltipPortal__invisible'); + return; + } + portalNode.current.classList.remove('echTooltipPortal__invisible'); + }, [position]); + + useEffect(() => { + if (popper.current) { + updateAnchorDimensions(); + void popper.current.update(); + } + }, [updateAnchorDimensions, popper]); + + return createPortal(children, portalNode.current); +}; + +TooltipPortalComponent.displayName = 'TooltipPortal'; + +/** @internal */ +export const TooltipPortal = TooltipPortalComponent; diff --git a/packages/osd-charts/src/components/portal/types.ts b/packages/osd-charts/src/components/portal/types.ts new file mode 100644 index 000000000000..e369cb1d1015 --- /dev/null +++ b/packages/osd-charts/src/components/portal/types.ts @@ -0,0 +1,129 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $Values } from 'utility-types'; + +import { Padding } from '../../utils/dimensions'; + +/** + * Placement used in positioning tooltip + * @public + */ +export const Placement = Object.freeze({ + Top: 'top' as const, + Bottom: 'bottom' as const, + Left: 'left' as const, + Right: 'right' as const, + TopStart: 'top-start' as const, + TopEnd: 'top-end' as const, + BottomStart: 'bottom-start' as const, + BottomEnd: 'bottom-end' as const, + RightStart: 'right-start' as const, + RightEnd: 'right-end' as const, + LeftStart: 'left-start' as const, + LeftEnd: 'left-end' as const, + Auto: 'auto' as const, + AutoStart: 'auto-start' as const, + AutoEnd: 'auto-end' as const, +}); + +/** + * {@inheritDoc (Placement:variable)} + * @public + */ +export type Placement = $Values; + +/** @internal */ +export type AnchorPosition = { + /** + * the right position of anchor + */ + x: number; + /** + * the top position of the anchor + */ + y: number; + /** + * the width of the anchor + */ + width: number; + /** + * the height of the anchor + */ + height: number; +}; + +/** + * Used to position tooltip relative to invisible anchor via ref element + * + * @internal + */ +export interface PortalAnchorRef { + /** + * Positioning values relative to `anchorRef`. Return `null` if tooltip is not visible. + */ + position: AnchorPosition | null; + /** + * Anchor ref element to use as position reference + * + * @defaultValue document.body + */ + ref: HTMLElement | null; +} + +/** + * Tooltip portal settings + * + * @public + */ +export interface TooltipPortalSettings { + /** + * Preferred placement of tooltip relative to anchor. + * + * This may not be the final placement given the positioning fallbacks. + * + * @defaultValue `right` {@link (Placement:type) | Placement.Right} + */ + placement?: Placement; + /** + * If given tooltip placement is not suitable, these `Placement`s will + * be used as fallback placements. + */ + fallbackPlacements?: Placement[]; + /** + * Boundary element to contain tooltip within + * + * `'chart'` will use the chart container as the boundary + * + * @defaultValue parent scroll container + */ + boundary?: HTMLElement | B; + /** + * Boundary element padding. + * Used to reduce extents of boundary placement when margins or paddings are used on boundary + * + * @defaultValue 0 + */ + boundaryPadding?: Partial | number; + /** + * Custom tooltip offset + * @defaultValue 10 + */ + offset?: number; +} diff --git a/packages/osd-charts/src/components/portal/utils.ts b/packages/osd-charts/src/components/portal/utils.ts new file mode 100644 index 000000000000..0e8482486940 --- /dev/null +++ b/packages/osd-charts/src/components/portal/utils.ts @@ -0,0 +1,129 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Required } from 'utility-types'; + +import { TooltipPortalSettings, Placement } from './types'; + +/** @internal */ +export const DEFAULT_POPPER_SETTINGS: Required = { + fallbackPlacements: [Placement.Right, Placement.Left, Placement.Top, Placement.Bottom], + placement: Placement.Right, + offset: 10, +}; + +/** + * Creates new dom element with given id and attaches to parent + * + * @internal + */ +export function getOrCreateNode( + id: string, + className?: string, + parent: HTMLElement = document.body, + zIndex: number = 0, +): HTMLDivElement { + // eslint-disable-next-line unicorn/prefer-query-selector + const node = document.getElementById(id); + if (node) { + return node as HTMLDivElement; + } + + const newNode = document.createElement('div'); + newNode.id = id; + if (className) { + newNode.classList.add(className); + } + newNode.style.zIndex = `${zIndex}`; + parent.appendChild(newNode); + return newNode; +} + +/** + * @link https://stackoverflow.com/questions/254302/how-can-i-determine-the-type-of-an-html-element-in-javascript + * @internal + */ +export function isHTMLElement(value: any): value is HTMLElement { + return typeof value === 'object' && value !== null && value.hasOwnProperty('nodeName'); +} + +/** + * Returns the top-most defined z-index in the element's ancestor hierarchy + * relative to the `target` element; if no z-index is defined, returns 0 + * @param element {HTMLElement} + * @param cousin {HTMLElement} + * @returns {number} + * @internal + */ +export function getElementZIndex(element: HTMLElement, cousin: HTMLElement): number { + /** + * finding the z-index of `element` is not the full story + * its the CSS stacking context that is important + * take this DOM for example: + * body + * section[z-index: 1000] + * p[z-index: 500] + * button + * div + * + * what z-index does the `div` need to display next to `button`? + * the `div` and `section` are where the stacking context splits + * so `div` needs to copy `section`'s z-index in order to + * appear next to / over `button` + * + * calculate this by starting at `button` and finding its offsetParents + * then walk the parents from top -> down until the stacking context + * split is found, or if there is no split then a specific z-index is unimportant + */ + + // build the array of the element + its offset parents + const nodesToInspect: HTMLElement[] = []; + while (true) { + nodesToInspect.push(element); + + // AFAICT this is a valid cast - the libdefs appear wrong + element = element.offsetParent as HTMLElement; + + // stop if there is no parent + if (element == null) { + break; + } + + // stop if the parent contains the related element + // as this is the z-index ancestor + if (element.contains(cousin)) { + break; + } + } + + // reverse the nodes to walk from top -> element + for (let i = nodesToInspect.length - 1; i >= 0; i--) { + const node = nodesToInspect[i]; + // get this node's z-index css value + const zIndex = window.document.defaultView!.getComputedStyle(node).getPropertyValue('z-index'); + + // if the z-index is not a number (e.g. "auto") return null, else the value + const parsedZIndex = parseInt(zIndex, 10); + if (!isNaN(parsedZIndex)) { + return parsedZIndex; + } + } + + return 0; +} diff --git a/packages/osd-charts/src/components/tooltip/_index.scss b/packages/osd-charts/src/components/tooltip/_index.scss new file mode 100644 index 000000000000..0a06e6f1bb10 --- /dev/null +++ b/packages/osd-charts/src/components/tooltip/_index.scss @@ -0,0 +1 @@ +@import 'tooltip'; diff --git a/packages/osd-charts/src/components/tooltip/_tooltip.scss b/packages/osd-charts/src/components/tooltip/_tooltip.scss new file mode 100644 index 000000000000..9e53efaa0001 --- /dev/null +++ b/packages/osd-charts/src/components/tooltip/_tooltip.scss @@ -0,0 +1,69 @@ +.echTooltip { + @include euiToolTipStyle; + @include euiFontSizeXS; + padding: 0; + transition: opacity $euiAnimSpeedNormal; + pointer-events: none; + user-select: none; + max-width: 256px; + + &__list { + padding: $euiSizeXS; + } + + &__header { + @include euiToolTipTitle; + margin-bottom: 0; + padding: $euiSizeXS ($euiSizeXS * 2); + } + + &__item { + display: flex; + min-width: 1px; + + &--container { + display: flex; + flex: 1 1 auto; + padding: 3px; + padding-left: 0; + min-width: 1px; + } + + &--backgroundColor { + position: relative; + width: $euiSizeXS; + margin-right: 3px; + flex-shrink: 0; + } + + &--color { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + } + + &__label { + overflow-wrap: break-word; + word-wrap: break-word; + min-width: 1px; + flex: 1 1 auto; + } + + &__value { + font-weight: $euiFontWeightBold; + text-align: right; + font-feature-settings: 'tnum'; + margin-left: $euiSizeS; + } + + &__rowHighlighted { + background-color: transparentize($euiColorGhost, 0.9); + } + + &--hidden { + opacity: 0; + } +} diff --git a/packages/osd-charts/src/components/tooltip/get_tooltip_settings.ts b/packages/osd-charts/src/components/tooltip/get_tooltip_settings.ts new file mode 100644 index 000000000000..6b134e80e358 --- /dev/null +++ b/packages/osd-charts/src/components/tooltip/get_tooltip_settings.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TooltipSettings, isTooltipType, SettingsSpec } from '../../specs/settings'; + +/** @internal */ +export function getTooltipSettings(settings: SettingsSpec, isExternalTooltipVisible: boolean): TooltipSettings { + if (!isExternalTooltipVisible) { + return settings.tooltip; + } + if (isTooltipType(settings.tooltip)) { + return { + type: settings.tooltip, + ...settings.externalPointerEvents.tooltip, + }; + } + return { + ...settings.tooltip, + ...settings.externalPointerEvents.tooltip, + }; +} diff --git a/packages/osd-charts/src/components/tooltip/index.ts b/packages/osd-charts/src/components/tooltip/index.ts new file mode 100644 index 000000000000..842fc85eb8fd --- /dev/null +++ b/packages/osd-charts/src/components/tooltip/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export { Tooltip } from './tooltip'; diff --git a/packages/osd-charts/src/components/tooltip/tooltip.tsx b/packages/osd-charts/src/components/tooltip/tooltip.tsx new file mode 100644 index 000000000000..0f0e06e714e9 --- /dev/null +++ b/packages/osd-charts/src/components/tooltip/tooltip.tsx @@ -0,0 +1,251 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import chroma from 'chroma-js'; +import classNames from 'classnames'; +import React, { memo, useCallback, useMemo, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; + +import { isColorValid } from '../../common/color_calcs'; +import { TooltipValueFormatter, TooltipSettings, TooltipValue } from '../../specs'; +import { onPointerMove as onPointerMoveAction } from '../../state/actions/mouse'; +import { GlobalChartState, BackwardRef } from '../../state/chart_state'; +import { getChartRotationSelector } from '../../state/selectors/get_chart_rotation'; +import { getChartThemeSelector } from '../../state/selectors/get_chart_theme'; +import { getInternalIsInitializedSelector, InitStatus } from '../../state/selectors/get_internal_is_intialized'; +import { getInternalIsTooltipVisibleSelector } from '../../state/selectors/get_internal_is_tooltip_visible'; +import { getInternalTooltipAnchorPositionSelector } from '../../state/selectors/get_internal_tooltip_anchor_position'; +import { getInternalTooltipInfoSelector } from '../../state/selectors/get_internal_tooltip_info'; +import { getSettingsSpecSelector } from '../../state/selectors/get_settings_specs'; +import { getTooltipHeaderFormatterSelector } from '../../state/selectors/get_tooltip_header_formatter'; +import { Rotation, isDefined } from '../../utils/common'; +import { TooltipPortal, TooltipPortalSettings, AnchorPosition, Placement } from '../portal'; +import { getTooltipSettings } from './get_tooltip_settings'; +import { TooltipInfo } from './types'; + +interface TooltipDispatchProps { + onPointerMove: typeof onPointerMoveAction; +} + +interface TooltipStateProps { + zIndex: number; + visible: boolean; + position: AnchorPosition | null; + info?: TooltipInfo; + headerFormatter?: TooltipValueFormatter; + settings?: TooltipSettings; + rotation: Rotation; + chartId: string; + backgroundColor: string; +} + +interface TooltipOwnProps { + getChartContainerRef: BackwardRef; +} + +type TooltipProps = TooltipDispatchProps & TooltipStateProps & TooltipOwnProps; + +const TooltipComponent = ({ + info, + zIndex, + headerFormatter, + position, + getChartContainerRef, + settings, + visible, + rotation, + chartId, + onPointerMove, + backgroundColor, +}: TooltipProps) => { + const chartRef = getChartContainerRef(); + + const handleScroll = () => { + // TODO: handle scroll cursor update + onPointerMove({ x: -1, y: -1 }, Date.now()); + }; + + useEffect(() => { + window.addEventListener('scroll', handleScroll, true); + return () => window.removeEventListener('scroll', handleScroll, true); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const renderHeader = useCallback( + (header: TooltipValue | null) => { + if (!header || !header.isVisible) { + return null; + } + + return ( +
    {headerFormatter ? headerFormatter(header) : header.formattedValue}
    + ); + }, + [headerFormatter], + ); + + const renderValues = (values: TooltipValue[]) => ( +
    + {values.map( + ( + { + seriesIdentifier, + valueAccessor, + label, + markValue, + formattedValue, + formattedMarkValue, + color, + isHighlighted, + isVisible, + }, + index, + ) => { + if (!isVisible) { + return null; + } + + const classes = classNames('echTooltip__item', { + echTooltip__rowHighlighted: isHighlighted, + }); + const adjustedBGColor = isColorValid(color) && chroma(color).alpha() === 0 ? 'transparent' : backgroundColor; + + return ( +
    +
    +
    +
    + +
    + {label} + {formattedValue} + {isDefined(markValue) &&  ({formattedMarkValue})} +
    +
    + ); + }, + )} +
    + ); + + const renderTooltip = () => { + if (!info || !visible) { + return null; + } + + if (typeof settings !== 'string' && settings?.customTooltip) { + const CustomTooltip = settings.customTooltip; + return ; + } + + return ( +
    + {renderHeader(info.header)} + {renderValues(info.values)} +
    + ); + }; + + const popperSettings = useMemo((): TooltipPortalSettings | undefined => { + if (!settings || typeof settings === 'string') { + return; + } + + const { placement, fallbackPlacements, boundary, ...rest } = settings; + + return { + ...rest, + placement: placement ?? (rotation === 0 || rotation === 180 ? Placement.Right : Placement.Top), + fallbackPlacements: + fallbackPlacements ?? + (rotation === 0 || rotation === 180 + ? [Placement.Right, Placement.Left, Placement.Top, Placement.Bottom] + : [Placement.Top, Placement.Bottom, Placement.Right, Placement.Left]), + boundary: boundary === 'chart' ? chartRef.current ?? undefined : boundary, + }; + }, [settings, chartRef, rotation]); + + if (!visible) { + return null; + } + return ( + + {renderTooltip()} + + ); +}; + +TooltipComponent.displayName = 'Tooltip'; + +const HIDDEN_TOOLTIP_PROPS = { + zIndex: 0, + visible: false, + info: undefined, + position: null, + headerFormatter: undefined, + settings: {}, + rotation: 0 as Rotation, + chartId: '', + backgroundColor: 'transparent', +}; + +const mapDispatchToProps = (dispatch: Dispatch): TooltipDispatchProps => + bindActionCreators({ onPointerMove: onPointerMoveAction }, dispatch); + +const mapStateToProps = (state: GlobalChartState): TooltipStateProps => { + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return HIDDEN_TOOLTIP_PROPS; + } + const { visible, isExternal } = getInternalIsTooltipVisibleSelector(state); + + const settingsSpec = getSettingsSpecSelector(state); + const settings = getTooltipSettings(settingsSpec, isExternal); + return { + visible, + zIndex: state.zIndex, + info: getInternalTooltipInfoSelector(state), + position: getInternalTooltipAnchorPositionSelector(state), + headerFormatter: getTooltipHeaderFormatterSelector(state), + settings, + rotation: getChartRotationSelector(state), + chartId: state.chartId, + backgroundColor: getChartThemeSelector(state).background.color, + }; +}; + +/** @internal */ +export const Tooltip = memo(connect(mapStateToProps, mapDispatchToProps)(TooltipComponent)); diff --git a/packages/osd-charts/src/components/tooltip/types.ts b/packages/osd-charts/src/components/tooltip/types.ts new file mode 100644 index 000000000000..bb7bfebba5ab --- /dev/null +++ b/packages/osd-charts/src/components/tooltip/types.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ComponentType } from 'react'; + +import { TooltipValue } from '../../specs'; + +/** + * The set of info used to render the a tooltip. + * @public + */ +export interface TooltipInfo { + /** + * The TooltipValue for the header. On XYAxis chart the x value + */ + header: TooltipValue | null; + /** + * The array of {@link TooltipValue}s to show on the tooltip. + * On XYAxis chart correspond to the set of y values for each series + */ + values: TooltipValue[]; +} + +/** + * The react component used to render a custom tooltip + * with the {@link TooltipInfo} props + * @public + */ +export type CustomTooltip = ComponentType; diff --git a/packages/osd-charts/src/geoms/types.ts b/packages/osd-charts/src/geoms/types.ts new file mode 100644 index 000000000000..fd4d7a8f0bef --- /dev/null +++ b/packages/osd-charts/src/geoms/types.ts @@ -0,0 +1,102 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RgbObject } from '../common/color_library_wrappers'; +import { Radian } from '../common/geometry'; +import { TexturedStyles } from '../utils/themes/theme'; + +/** @internal */ +export interface Text { + text: string; + x: number; + y: number; +} +/** @internal */ +export interface Line { + x1: number; + y1: number; + x2: number; + y2: number; +} + +/** @internal */ +export interface Rect { + x: number; + y: number; + width: number; + height: number; +} + +/** @internal */ +export interface Arc { + x: number; + y: number; + radius: number; + startAngle: Radian; + endAngle: Radian; +} + +/** @internal */ +export interface Circle { + x: number; + y: number; + radius: number; +} + +/** + * render options for texture + * @public + */ +export interface Texture extends Pick { + /** + * patern to apply to canvas fill + */ + pattern: CanvasPattern; +} + +/** + * Fill style for every geometry + * @public + */ +export interface Fill { + /** + * fill color in rgba + */ + color: RgbObject; + texture?: Texture; +} + +/** + * Stroke style for every geometry + * @public + */ +export interface Stroke { + /** + * stroke rgba + */ + color: RgbObject; + /** + * stroke width + */ + width: number; + /** + * stroke dash array + */ + dash?: number[]; +} diff --git a/packages/osd-charts/src/index.ts b/packages/osd-charts/src/index.ts new file mode 100644 index 000000000000..91ac04aeb922 --- /dev/null +++ b/packages/osd-charts/src/index.ts @@ -0,0 +1,106 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './components'; +export { ChartType } from './chart_types'; +export { ChartSize, ChartSizeArray, ChartSizeObject } from './utils/chart_size'; + +export { SpecId, GroupId, AxisId, AnnotationId } from './utils/ids'; + +// Everything related to the specs types and react-components +export * from './specs'; +export { DebugState } from './state/types'; +export { toEntries } from './utils/common'; +export { CurveType } from './utils/curves'; +export { ContinuousDomain, OrdinalDomain } from './utils/domain'; +export { SimplePadding, Padding } from './utils/dimensions'; +export { timeFormatter, niceTimeFormatter, niceTimeFormatByDay } from './utils/data/formatters'; +export { SeriesIdentifier, SeriesKey } from './common/series_id'; +export { XYChartSeriesIdentifier, DataSeriesDatum, FilledValues } from './chart_types/xy_chart/utils/series'; +export { + AnnotationTooltipFormatter, + CustomAnnotationTooltip, + ComponentWithAnnotationDatum, +} from './chart_types/xy_chart/annotations/types'; +export { GeometryValue, BandedAccessorType } from './utils/geometry'; +export { LegendPath, LegendPathElement } from './state/actions/legend'; +export { CategoryKey } from './common/category'; +export { + Config as PartitionConfig, + FillLabelConfig as PartitionFillLabel, + PartitionLayout, +} from './chart_types/partition_chart/layout/types/config_types'; +export { Layer as PartitionLayer } from './chart_types/partition_chart/specs/index'; +export * from './chart_types/goal_chart/specs/index'; +export * from './chart_types/wordcloud/specs/index'; + +export { + Accessor, + AccessorFn, + IndexedAccessorFn, + UnaryAccessorFn, + AccessorObjectKey, + AccessorArrayIndex, +} from './utils/accessor'; +export { CustomTooltip, TooltipInfo } from './components/tooltip/types'; + +// scales +export { ScaleType } from './scales/constants'; +export { ScaleContinuousType, ScaleOrdinalType, ScaleBandType, LogBase, LogScaleOptions } from './scales'; + +// theme +export * from './utils/themes/theme'; +export * from './utils/themes/theme_common'; +export { LIGHT_THEME } from './utils/themes/light_theme'; +export { DARK_THEME } from './utils/themes/dark_theme'; + +// partition +export * from './chart_types/partition_chart/layout/types/viewmodel_types'; +export * from './chart_types/partition_chart/layout/utils/group_by_rollup'; + +// heatmap +export { Cell } from './chart_types/heatmap/layout/types/viewmodel_types'; +export { Config as HeatmapConfig, HeatmapBrushEvent } from './chart_types/heatmap/layout/types/config_types'; + +// utilities +export { + Datum, + Position, + Rendering, + Rotation, + VerticalAlignment, + HorizontalAlignment, + RecursivePartial, + NonAny, + IsAny, + IsUnknown, + ColorVariant, + Color, + LabelAccessor, + ShowAccessor, + ValueAccessor, + ValueFormatter, + LayoutDirection, +} from './utils/common'; +export { DataGenerator } from './utils/data_generators/data_generator'; +export * from './utils/themes/merge_utils'; +export { MODEL_KEY } from './chart_types/partition_chart/layout/config'; +export { LegendStrategy } from './chart_types/partition_chart/layout/utils/highlighted_geoms'; +export { Ratio } from './common/geometry'; +export { AdditiveNumber } from './utils/accessor'; diff --git a/packages/osd-charts/src/mocks/annotations/annotations.ts b/packages/osd-charts/src/mocks/annotations/annotations.ts new file mode 100644 index 000000000000..2228930afbd1 --- /dev/null +++ b/packages/osd-charts/src/mocks/annotations/annotations.ts @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getAnnotationLinePropsId } from '../../chart_types/xy_chart/annotations/line/dimensions'; +import { AnnotationLineProps } from '../../chart_types/xy_chart/annotations/line/types'; +import { AnnotationRectProps } from '../../chart_types/xy_chart/annotations/rect/types'; +import { mergePartial, RecursivePartial } from '../../utils/common'; + +/** @internal */ +export class MockAnnotationLineProps { + private static readonly base: AnnotationLineProps = { + id: getAnnotationLinePropsId('spec1', { dataValue: 0 }, 0), + specId: 'spec1', + linePathPoints: { + x1: 0, + y1: 0, + x2: 0, + y2: 0, + }, + panel: { top: 0, left: 0, width: 100, height: 100 }, + datum: { dataValue: 0 }, + markers: [], + }; + + static default(partial?: RecursivePartial, smVerticalValue?: any, smHorizontalValue?: any) { + const id = getAnnotationLinePropsId( + partial?.specId ?? MockAnnotationLineProps.base.specId, + { + ...MockAnnotationLineProps.base.datum, + ...partial?.datum, + }, + 0, + smVerticalValue, + smHorizontalValue, + ); + return mergePartial( + MockAnnotationLineProps.base, + { id, ...partial }, + { + mergeOptionalPartialValues: true, + }, + ); + } + + static fromPoints(x1 = 0, y1 = 0, x2 = 0, y2 = 0): AnnotationLineProps { + return MockAnnotationLineProps.default({ + linePathPoints: { + x1, + y1, + x2, + y2, + }, + }); + } + + static fromPartialAndId(partial?: RecursivePartial) { + return mergePartial(MockAnnotationLineProps.base, partial, { + mergeOptionalPartialValues: true, + }); + } +} + +/** @internal */ +export class MockAnnotationRectProps { + private static readonly base: AnnotationRectProps = { + datum: { coordinates: { x0: 0, x1: 1, y0: 0, y1: 1 } }, + rect: { + x: 0, + y: 0, + width: 0, + height: 0, + }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, + }; + + static default(partial?: RecursivePartial) { + return mergePartial(MockAnnotationRectProps.base, partial, { + mergeOptionalPartialValues: true, + }); + } + + static fromValues(x = 0, y = 0, width = 0, height = 0): AnnotationRectProps { + return MockAnnotationRectProps.default({ + rect: { + x, + y, + width, + height, + }, + }); + } +} diff --git a/packages/osd-charts/src/mocks/canvas.ts b/packages/osd-charts/src/mocks/canvas.ts new file mode 100644 index 000000000000..2f57d2b4e53e --- /dev/null +++ b/packages/osd-charts/src/mocks/canvas.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const getMockCanvasContext2D = (): CanvasRenderingContext2D => { + const ctx = document.createElement('canvas').getContext('2d'); + if (ctx) return ctx; + + throw new Error('Unable to create mock context'); +}; + +/** @internal */ +export const getMockCanvas = (): HTMLCanvasElement => { + return document.createElement('canvas'); +}; diff --git a/packages/osd-charts/src/mocks/geometries.ts b/packages/osd-charts/src/mocks/geometries.ts new file mode 100644 index 000000000000..9a0ca6687333 --- /dev/null +++ b/packages/osd-charts/src/mocks/geometries.ts @@ -0,0 +1,201 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { omit } from 'lodash'; + +import { buildPointGeometryStyles } from '../chart_types/xy_chart/rendering/point_style'; +import { mergePartial, RecursivePartial } from '../utils/common'; +import { AreaGeometry, PointGeometry, BarGeometry, LineGeometry, BubbleGeometry } from '../utils/geometry'; +import { LIGHT_THEME } from '../utils/themes/light_theme'; +import { PointShape } from '../utils/themes/theme'; +import { MockSeriesIdentifier } from './series/series_identifiers'; + +const DEFAULT_MOCK_POINT_COLOR = 'red'; +const { barSeriesStyle, lineSeriesStyle, areaSeriesStyle, bubbleSeriesStyle } = LIGHT_THEME; + +/** @internal */ +export class MockPointGeometry { + private static readonly base: PointGeometry = { + x: 0, + y: 0, + radius: lineSeriesStyle.point.radius, + color: DEFAULT_MOCK_POINT_COLOR, + seriesIdentifier: MockSeriesIdentifier.default(), + style: { + shape: PointShape.Circle, + fill: { + color: { + r: 255, + g: 255, + b: 255, + opacity: 1, + }, + }, + stroke: { + color: { + r: 255, + g: 0, + b: 0, + opacity: 1, + }, + width: 1, + }, + }, + value: { + accessor: 'y0', + x: 0, + y: 0, + mark: null, + datum: { x: 0, y: 0 }, + }, + transform: { + x: 0, + y: 0, + }, + panel: { + width: 100, + height: 100, + left: 0, + top: 0, + }, + orphan: false, + }; + + static default(partial?: RecursivePartial) { + const color = partial?.color ?? DEFAULT_MOCK_POINT_COLOR; + const style = buildPointGeometryStyles(color, lineSeriesStyle.point); + return mergePartial(MockPointGeometry.base, partial, { mergeOptionalPartialValues: true }, [ + { style }, + ]); + } + + static fromBaseline(baseline: RecursivePartial, omitKeys: string[] | string = []) { + return (partial?: RecursivePartial) => { + return omit( + mergePartial(MockPointGeometry.base, partial, { mergeOptionalPartialValues: true }, [baseline]), + omitKeys, + ); + }; + } +} + +/** @internal */ +export class MockBarGeometry { + private static readonly base: BarGeometry = { + x: 0, + y: 0, + width: 0, + height: 0, + color: DEFAULT_MOCK_POINT_COLOR, + displayValue: undefined, + seriesIdentifier: MockSeriesIdentifier.default(), + value: { + accessor: 'y0', + x: 0, + y: 0, + mark: null, + datum: { x: 0, y: 0 }, + }, + seriesStyle: barSeriesStyle, + transform: { + x: 0, + y: 0, + }, + panel: { + width: 100, + height: 100, + left: 0, + top: 0, + }, + }; + + static default(partial?: RecursivePartial) { + return mergePartial(MockBarGeometry.base, partial, { mergeOptionalPartialValues: true }); + } + + static fromBaseline(baseline: RecursivePartial, omitKeys: string[] | string = []) { + return (partial?: RecursivePartial) => { + const geo = mergePartial(MockBarGeometry.base, partial, { mergeOptionalPartialValues: true }, [ + baseline, + ]); + return omit(geo, omitKeys); + }; + } +} + +/** @internal */ +export class MockLineGeometry { + private static readonly base: LineGeometry = { + line: '', + points: [], + color: DEFAULT_MOCK_POINT_COLOR, + transform: { + x: 0, + y: 0, + }, + seriesIdentifier: MockSeriesIdentifier.default(), + seriesLineStyle: lineSeriesStyle.line, + seriesPointStyle: lineSeriesStyle.point, + clippedRanges: [], + }; + + static default(partial?: RecursivePartial) { + return mergePartial(MockLineGeometry.base, partial, { mergeOptionalPartialValues: true }); + } +} + +/** @internal */ +export class MockAreaGeometry { + private static readonly base: AreaGeometry = { + area: '', + lines: [], + points: [], + color: DEFAULT_MOCK_POINT_COLOR, + transform: { + x: 0, + y: 0, + }, + seriesIdentifier: MockSeriesIdentifier.default(), + seriesAreaStyle: areaSeriesStyle.area, + seriesAreaLineStyle: areaSeriesStyle.line, + seriesPointStyle: areaSeriesStyle.point, + isStacked: false, + clippedRanges: [], + }; + + static default(partial?: RecursivePartial) { + return mergePartial(MockAreaGeometry.base, partial, { mergeOptionalPartialValues: true }); + } +} + +/** @internal */ +export class MockBubbleGeometry { + private static readonly base: BubbleGeometry = { + points: [], + color: DEFAULT_MOCK_POINT_COLOR, + seriesIdentifier: MockSeriesIdentifier.default(), + seriesPointStyle: bubbleSeriesStyle.point, + }; + + static default(partial?: RecursivePartial) { + return mergePartial(MockBubbleGeometry.base, partial, { + mergeOptionalPartialValues: true, + }); + } +} diff --git a/packages/osd-charts/src/mocks/hierarchical/cpu_profile_tree_mock.json b/packages/osd-charts/src/mocks/hierarchical/cpu_profile_tree_mock.json new file mode 100644 index 000000000000..d98b6f6d0ed2 --- /dev/null +++ b/packages/osd-charts/src/mocks/hierarchical/cpu_profile_tree_mock.json @@ -0,0 +1,7541 @@ +{ + "dictionary": [ + "root", + "github.com/elastic/go-structform/gotype.foldInterfaceValue", + "github.com/elastic/apm-server/publish.NewPublisher.func1", + "github.com/elastic/apm-server/publish.(*Publisher).run", + "github.com/elastic/apm-server/publish.(*Publisher).processPendingReq", + "github.com/elastic/beats/v7/libbeat/publisher/pipeline.(*netClientWorker).run", + "github.com/elastic/beats/v7/libbeat/publisher/pipeline.(*netClientWorker).publishBatch", + "github.com/elastic/beats/v7/libbeat/outputs.(*backoffClient).Publish", + "github.com/elastic/go-structform/gotype.foldAnyReflect", + "runtime.mallocgc", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.(*Client).Publish", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.(*Client).publishEvents", + "github.com/elastic/apm-server/publish.transformTransformable", + "github.com/elastic/go-structform/gotype.foldMapInlineInterface", + "net/http.HandlerFunc.ServeHTTP", + "github.com/elastic/beats/v7/libbeat/esleg/eslegclient.(*Connection).Bulk", + "github.com/elastic/beats/v7/libbeat/esleg/eslegclient.bulkEncode", + "github.com/elastic/go-structform/gotype.makeStructFold.func1", + "github.com/elastic/go-structform/gotype.(*Iterator).Fold", + "github.com/elastic/beats/v7/libbeat/monitoring.(*Registry).Visit", + "github.com/elastic/beats/v7/libbeat/monitoring.(*Registry).doVisit", + "github.com/elastic/go-structform/gotype.makeFieldsFold.func1", + "github.com/elastic/go-structform/gotype.foldMapInterface", + "github.com/elastic/go-structform/gotype.makeFieldInlineFold.func1", + "net/http.(*conn).serve", + "github.com/elastic/apm-server/model.PprofProfile.Transform", + "net/http.serverHandler.ServeHTTP", + "runtime.newobject", + "net/http.(*ServeMux).ServeHTTP", + "github.com/elastic/apm-server/beater/request.(*ContextPool).HTTPHandler.func1", + "runtime.systemstack", + "github.com/elastic/apm-server/beater/middleware.LogMiddleware.func1.1", + "github.com/elastic/apm-server/beater/middleware.ResponseHeadersMiddleware.func1.1", + "github.com/elastic/apm-server/beater/middleware.RecoverPanicMiddleware.func1.1", + "github.com/elastic/apm-server/beater/middleware.MonitoringMiddleware.func1.2", + "github.com/elastic/apm-server/beater/middleware.RequestTimeMiddleware.func1.1", + "github.com/elastic/apm-server/beater/middleware.AuthorizationMiddleware.func1.1", + "github.com/elastic/apm-server/beater/middleware.SystemMetadataMiddleware.func1.1", + "runtime.(*mcache).nextFree", + "runtime.(*mcache).refill", + "runtime.(*mcentral).cacheSpan", + "github.com/elastic/beats/v7/libbeat/monitoring/report/elasticsearch.makeSnapshot", + "github.com/elastic/beats/v7/libbeat/monitoring.(*Func).Visit", + "runtime.(*mheap).alloc", + "runtime.(*mcentral).grow", + "github.com/elastic/gmux.(*mux).withGRPCInsecure.func1", + "go.elastic.co/apm/module/apmhttp.(*handler).ServeHTTP", + "runtime.memclrNoHeapPointers", + "github.com/elastic/go-structform/json.writer.write", + "github.com/json-iterator/go.(*structFieldDecoder).Decode", + "github.com/elastic/apm-server/beater/api/profile.Handler.func2", + "github.com/elastic/go-structform/json.(*Visitor).OnString", + "runtime.mcall", + "github.com/elastic/apm-server/beater/api/profile.Handler.func1", + "github.com/elastic/apm-server/beater/api/intake.Handler.func1", + "github.com/elastic/go-structform/gotype.getReflectFoldSlice.func1", + "runtime.schedule", + "github.com/google/pprof/profile.Parse", + "runtime.makeslice", + "github.com/elastic/apm-server/processor/stream.(*Processor).HandleStream", + "go.elastic.co/apm.(*profilingState).start.func1", + "github.com/elastic/go-structform/gotype.getReflectFoldMap.func1", + "github.com/google/pprof/profile.decodeMessage", + "go.elastic.co/apm.(*profilingState).profile", + "go.elastic.co/apm.newLookupProfilingState.func1", + "runtime/pprof.(*Profile).WriteTo", + "runtime/pprof.writeHeap", + "runtime/pprof.writeHeapInternal", + "github.com/elastic/beats/v7/libbeat/cmd/instance/metrics.getBeatProcessState", + "runtime.mapassign_faststr", + "runtime.memmove", + "runtime/pprof.writeHeapProto", + "github.com/elastic/beats/v7/libbeat/metric/system/process.(*Stats).GetOne", + "github.com/elastic/apm-server/utility.update", + "bytes.(*Buffer).Write", + "github.com/elastic/beats/v7/libbeat/publisher/pipeline.(*client).publish", + "github.com/elastic/apm-server/model.(*Metadata).Set", + "io/ioutil.readAll", + "bytes.(*Buffer).ReadFrom", + "github.com/google/pprof/profile.ParseData", + "github.com/elastic/apm-server/processor/stream.(*Processor).readBatch", + "github.com/elastic/beats/v7/libbeat/publisher/pipeline.(*client).PublishAll", + "github.com/elastic/go-structform/json.(*Visitor).OnKey", + "runtime.makemap_small", + "github.com/elastic/beats/v7/libbeat/publisher/processing.(*group).Run", + "runtime.park_m", + "runtime.heapBitsSetType", + "runtime.futex", + "runtime.nanotime", + "runtime.(*mheap).alloc.func1", + "github.com/elastic/apm-server/decoder.(*NDJSONStreamDecoder).Decode", + "syscall.Syscall", + "github.com/elastic/apm-server/model.(*mapStr).set", + "io/ioutil.ReadAll", + "runtime.findrunnable", + "github.com/elastic/apm-server/utility.Set", + "github.com/elastic/beats/v7/libbeat/cmd/instance/metrics.reportBeatCgroups", + "github.com/elastic/beats/v7/libbeat/metric/system/process.(*Stats).getSingleProcess", + "github.com/json-iterator/go.(*generalStructDecoder).Decode", + "github.com/json-iterator/go.(*generalStructDecoder).decodeOneField", + "github.com/elastic/go-structform/gotype.getFoldConvert", + "runtime.futexwakeup", + "runtime.gcBgMarkWorker", + "github.com/json-iterator/go.(*Decoder).Decode", + "runtime.notewakeup", + "runtime.(*mheap).allocSpan", + "github.com/elastic/beats/v7/libbeat/esleg/eslegclient.(*Connection).sendBulkRequest", + "github.com/json-iterator/go.(*Iterator).ReadVal", + "internal/poll.ignoringEINTR", + "github.com/google/pprof/profile.unmarshal", + "github.com/elastic/apm-server/model.(*Span).Transform", + "github.com/json-iterator/go.(*oneFieldStructDecoder).Decode", + "os.Open", + "os.OpenFile", + "github.com/elastic/beats/v7/libbeat/monitoring/report/elasticsearch.(*publishClient).Publish", + "github.com/elastic/beats/v7/libbeat/monitoring/report/elasticsearch.(*publishClient).publishBulk", + "runtime.convTstring", + "runtime.slicebytetostring", + "runtime.startm", + "runtime/pprof.(*profileBuilder).appendLocsForStack", + "github.com/elastic/beats/v7/libbeat/common.MapStr.Clone", + "compress/flate.(*decompressor).Read", + "github.com/elastic/go-structform/gotype.makeFieldFold.func1", + "compress/gzip.(*Reader).Read", + "runtime.wakep", + "bytes.(*Buffer).tryGrowByReslice", + "github.com/elastic/go-structform/gotype.foldString", + "runtime.gcBgMarkWorker.func2", + "github.com/elastic/beats/v7/libbeat/metric/system/process.(*Process).getDetails", + "runtime.nextFreeFast", + "fmt.Sprintf", + "runtime.gcDrain", + "bufio.(*Reader).fill", + "github.com/elastic/beats/v7/libbeat/common.MapStr.deepUpdateMap", + "github.com/elastic/beats/v7/libbeat/cmd/instance/metrics.reportMemStats", + "github.com/elastic/beats/v7/libbeat/common.(*GenericEventConverter).normalizeMap", + "runtime.growslice", + "compress/flate.(*compressor).encSpeed", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.bulkCollectPublishFails", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.bulkReadItemStatus", + "github.com/elastic/apm-server/model/modeldecoder/v2.DecodeNestedSpan", + "aeshashbody", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.itemStatusInner", + "github.com/elastic/beats/v7/libbeat/cmd/instance/metrics.reportFDUsage", + "github.com/elastic/beats/v7/libbeat/cmd/instance/metrics.getFDUsage", + "syscall.Syscall6", + "github.com/elastic/beats/v7/libbeat/common.(*GenericEventConverter).normalizeValue", + "runtime.newarray", + "runtime.makeBucketArray", + "internal/poll.(*FD).Read", + "syscall.Read", + "syscall.read", + "runtime.selectgo", + "github.com/elastic/go-structform/json.(*Visitor).writeByte", + "github.com/elastic/beats/v7/libbeat/cmd/instance/metrics.getRSSSize", + "time.Now", + "runtime.mapiternext", + "github.com/elastic/beats/v7/libbeat/processors/actions.(*addFields).Run", + "github.com/elastic/gosigar.readFile", + "os.openFileNolog", + "os.newFile", + "github.com/elastic/gosigar/cgroup.(*Reader).GetStatsForProcess", + "runtime.hashGrow", + "runtime.scanobject", + "compress/flate.(*Writer).Write", + "compress/flate.(*compressor).write", + "runtime.lockWithRank", + "github.com/elastic/apm-server/model.(*Stacktrace).transform", + "github.com/elastic/apm-server/model.(*Stacktrace).transformFrames", + "github.com/elastic/beats/v7/libbeat/common.MapStr.DeepUpdateNoOverwrite", + "runtime.bgscavenge", + "runtime.goready", + "runtime.goready.func1", + "github.com/google/pprof/profile.glob..func2", + "runtime.lock2", + "github.com/elastic/apm-server/model.(*Error).Transform", + "net/http.(*persistConn).writeLoop", + "github.com/elastic/beats/v7/libbeat/metric/system/process.(*Stats).getProcessEvent", + "io/ioutil.ReadFile", + "github.com/json-iterator/go.(*funcDecoder).Decode", + "github.com/json-iterator/go.(*Iterator).ReadString", + "runtime.gentraceback", + "bytes.(*Buffer).grow", + "runtime.convTslice", + "github.com/elastic/beats/v7/libbeat/cmd/instance/metrics.reportBeatCPU", + "github.com/elastic/beats/v7/libbeat/cmd/instance/metrics.getCPUUsage", + "runtime.bgsweep", + "compress/flate.(*decompressor).huffmanBlock", + "runtime.ready", + "net/http.(*conn).readRequest", + "runtime/pprof.(*profileBuilder).emitLocation", + "runtime/pprof.(*profileBuilder).flush", + "runtime.mapiterinit", + "reflect.Value.Convert", + "reflect.haveIdenticalUnderlyingType", + "net/http.(*cancelTimerBody).Read", + "runtime.checkTimers", + "runtime.goschedImpl", + "go.elastic.co/apm.(*Tracer).loop", + "github.com/elastic/beats/v7/libbeat/metric/system/memory.Get", + "github.com/elastic/gosigar.(*Mem).Get", + "github.com/elastic/gosigar.parseMeminfo", + "os.(*File).read", + "go.elastic.co/apm.(*Tracer).gatherMetrics.func1", + "go.elastic.co/apm.gatherMetrics", + "go.elastic.co/apm.(*builtinMetricsGatherer).GatherMetrics", + "runtime.sweepone", + "runtime.profilealloc", + "runtime.getitab", + "compress/flate.(*decompressor).nextBlock", + "runtime.acquirem", + "io.Copy", + "io.copyBuffer", + "reflect.convertOp", + "go.elastic.co/apm.(*builtinMetricsGatherer).gatherSystemMetrics", + "go.elastic.co/apm.gatherSysMetrics", + "runtime.mProf_Malloc", + "bufio.(*Reader).ReadSlice", + "runtime.send", + "github.com/google/pprof/profile.ParseUncompressed", + "runtime.mallocgc.func1", + "runtime.largeAlloc", + "compress/flate.(*huffmanBitWriter).writeBlockDynamic", + "net/http.(*Client).Do", + "net/http.(*Client).do", + "net/http.(*Client).send", + "net/http.send", + "runtime.(*mspan).sweep", + "runtime.asyncPreempt", + "github.com/elastic/apm-server/model.(*Span).fields", + "reflect.MapOf", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.bulkEncodePublishRequest", + "net/http.(*gzipReader).Read", + "runtime.gosched_m", + "runtime.callers", + "runtime.callers.func1", + "github.com/elastic/apm-server/model/modeldecoder/v2.DecodeNestedTransaction", + "github.com/google/pprof/profile.(*Profile).postDecode", + "runtime.heapBitsForAddr", + "bytes.makeSlice", + "github.com/elastic/apm-server/model.(*mapStr).maybeSetString", + "github.com/cespare/xxhash/v2.(*Digest).Sum", + "runtime.mapaccess1", + "github.com/elastic/go-structform/json.(*Visitor).onFieldNext", + "github.com/elastic/beats/v7/libbeat/esleg/eslegclient.(*Connection).execHTTPRequest", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.(*jsonReader).step", + "net/http.(*Request).write", + "runtime.runtimer", + "net/http.(*persistConn).readLoop", + "github.com/elastic/beats/v7/libbeat/metric/system/process.getProcFDUsage", + "github.com/elastic/gosigar.(*ProcFDUsage).Get", + "github.com/elastic/go-sysinfo.Host", + "github.com/elastic/go-sysinfo/providers/linux.linuxSystem.Host", + "github.com/json-iterator/go.(*sliceDecoder).Decode", + "github.com/json-iterator/go.(*sliceDecoder).doDecode", + "sync.(*Pool).Get", + "bufio.(*Writer).Flush", + "github.com/elastic/apm-server/model.(*Transaction).Transform", + "github.com/elastic/apm-server/model.(*Error).fields", + "net/http.(*transferWriter).writeBody", + "runtime.runOneTimer", + "runtime.mstart", + "runtime.mstart1", + "internal/poll.(*FD).Init", + "internal/poll.(*pollDesc).init", + "internal/poll.runtime_pollOpen", + "runtime.netpollopen", + "runtime.epollctl", + "syscall.Open", + "syscall.openat", + "github.com/elastic/gosigar.readProcFile", + "github.com/elastic/beats/v7/libbeat/metric/system/process.newProcess", + "github.com/elastic/apm-server/decoder.(*NDJSONStreamDecoder).ReadAhead", + "github.com/elastic/apm-server/decoder.(*LineReader).ReadLine", + "compress/flate.(*decompressor).huffSym", + "runtime.growWork_faststr", + "sync.(*Map).Load", + "runtime.mapaccess2", + "runtime.(*spanSet).push", + "runtime.(*mheap).allocMSpanLocked", + "runtime.(*fixalloc).alloc", + "runtime.(*itabTableType).find", + "runtime.releasem", + "runtime.add", + "runtime.arenaIndex", + "runtime.unlockWithRank", + "runtime.unlock2", + "github.com/elastic/go-structform/json.(*Visitor).tryElemNext", + "github.com/elastic/go-structform/json.(*Visitor).OnInt64", + "github.com/elastic/go-structform/json.(*Visitor).onInt", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.createEventBulkMeta", + "runtime.goexit0", + "github.com/elastic/beats/v7/libbeat/publisher/queue/memqueue.NewQueue.func1", + "github.com/elastic/gosigar.(*ProcState).Get", + "github.com/elastic/gosigar/cgroup.parseUintFromFile", + "runtime.evacuate_faststr", + "compress/gzip.NewReader", + "runtime.pcvalue", + "runtime/pprof.(*profileBuilder).build", + "runtime/pprof.(*profileBuilder).pbSample", + "github.com/elastic/apm-server/model.(*Service).AgentFields", + "github.com/elastic/apm-server/model.(*Metricset).Transform", + "reflect.haveIdenticalType", + "reflect.(*rtype).Name", + "github.com/elastic/go-structform/json.(*Visitor).OnObjectStart", + "github.com/elastic/beats/v7/libbeat/publisher/queue/memqueue.(*bufferingEventLoop).run", + "github.com/elastic/gosigar/cgroup.(*MemorySubsystem).get", + "runtime.gcWriteBarrier", + "github.com/elastic/apm-server/processor/stream.(*Processor).readMetadata", + "github.com/elastic/apm-server/model/modeldecoder/v2.DecodeNestedMetadata", + "github.com/elastic/apm-server/model/modeldecoder/v2.decodeMetadata", + "github.com/elastic/apm-server/model/modeldecoder/v2.decodeIntoMetadataRoot", + "runtime.(*mcentral).uncacheSpan", + "github.com/google/pprof/profile.glob..func4", + "runtime.typehash", + "internal/poll.(*FD).Write", + "runtime.mapaccess2_faststr", + "runtime.typedmemmove", + "github.com/elastic/apm-server/model.(*Error).addException", + "runtime.chansend", + "github.com/elastic/go-structform/internal/unsafe.Str2Bytes", + "github.com/elastic/go-structform/gotype.foldInt64", + "runtime.(*pageAlloc).scavenge", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.(*jsonReader).ignoreNext", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.(*jsonReader).nextFieldName", + "encoding/json.(*decodeState).value", + "net/http.persistConnWriter.Write", + "runtime.futexsleep", + "runtime.gcMarkDone", + "runtime.sysmon", + "github.com/elastic/gosigar/cgroup.NewReaderOptions", + "runtime.mapaccess1_faststr", + "net.(*conn).Read", + "github.com/google/pprof/profile.glob..func17", + "github.com/google/pprof/profile.decodeUint64s", + "github.com/elastic/beats/v7/libbeat/logp.(*Logger).With", + "runtime.newstack", + "runtime.assertE2I2", + "go.uber.org/zap.(*SugaredLogger).Info", + "syscall.Write", + "syscall.write", + "go.elastic.co/apm.(*Tracer).StartTransactionOptions", + "runtime.gcAssistAlloc", + "runtime.gcAssistAlloc.func1", + "sync.(*Pool).pin", + "net.(*conn).Write", + "net.(*netFD).Write", + "net/http.(*Transport).RoundTrip", + "net/http.(*Transport).roundTrip", + "runtime.heapBits.initSpan", + "go.elastic.co/apm.(*Tracer).StartTransaction", + "github.com/elastic/go-structform/gotype.foldArrString", + "github.com/elastic/go-structform.extArrVisitor.OnStringArray", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.(*jsonReader).doStepDict", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.(*jsonReader).skipWS", + "bufio.(*Writer).ReadFrom", + "runtime.bgscavenge.func2", + "github.com/prometheus/procfs.FS.NewStat", + "golang.org/x/sync/errgroup.(*Group).Go.func1", + "github.com/elastic/beats/v7/libbeat/monitoring/report/elasticsearch.(*reporter).snapshotLoop", + "github.com/elastic/apm-server/model/modeldecoder/nullable.init.0.func1", + "runtime.(*mspan).nextFreeIndex", + "github.com/json-iterator/go.(*eightFieldsStructDecoder).Decode", + "bufio.(*Reader).ReadByte", + "github.com/json-iterator/go.(*nineFieldsStructDecoder).Decode", + "net.(*netFD).Read", + "compress/gzip.(*Reader).Reset", + "compress/gzip.(*Reader).readHeader", + "runtime.step", + "go.uber.org/zap.(*SugaredLogger).With", + "runtime.pcdatavalue", + "runtime.(*spanSet).pop", + "runtime.gcAssistAlloc1", + "runtime.gcDrainN", + "net/http.(*response).finishRequest", + "net/http.readRequest", + "sort.quickSort", + "runtime.(*mspan).init", + "runtime.wbBufFlush", + "runtime.findObject", + "github.com/elastic/go-structform/json.(*Visitor).writeString", + "github.com/elastic/go-structform/gotype.liftUserValueFn.func1", + "runtime.chansend1", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.(*jsonReader).stepDictValueEnd", + "github.com/elastic/beats/v7/libbeat/common/streambuf.(*Buffer).IgnoreSymbols", + "encoding/json.(*decodeState).object", + "runtime.exitsyscall", + "runtime.stopm", + "github.com/elastic/gosigar/cgroup.(*CPUSubsystem).get", + "github.com/elastic/gosigar/cgroup.(*BlockIOSubsystem).get", + "github.com/elastic/gosigar/cgroup.blkioThrottle", + "github.com/elastic/go-sysinfo/providers/linux.newHost", + "github.com/prometheus/procfs.FS.Stat", + "github.com/elastic/beats/v7/libbeat/publisher/pipeline.(*client).Publish", + "compress/flate.(*decompressor).readHuffman", + "compress/flate.(*dictDecoder).init", + "github.com/google/pprof/profile.decodeVarint", + "runtime.makemap", + "github.com/elastic/apm-server/beater/middleware.loggerWithRequestContext", + "go.uber.org/zap.(*SugaredLogger).log", + "reflect.(*rtype).Kind", + "runtime.(*Frames).Next", + "runtime.findfunc", + "bufio.(*Reader).Peek", + "runtime.pageIndexOf", + "go.elastic.co/apm.StartSpan", + "runtime.walltime", + "net/textproto.(*Reader).ReadMIMEHeader", + "compress/flate.(*deflateFast).encode", + "compress/flate.(*huffmanBitWriter).writeTokens", + "runtime.isEmpty", + "runtime.(*mspan).refillAllocCache", + "github.com/elastic/apm-server/model.(*ServiceNode).fields", + "runtime.fastrand", + "runtime.wbBufFlush.func1", + "runtime.wbBufFlush1", + "sync.(*Pool).Put", + "github.com/cespare/xxhash/v2.(*Digest).WriteString", + "go.elastic.co/apm.(*Span).End", + "time.Since", + "github.com/elastic/go-structform/gotype.getFoldGoTypes", + "github.com/elastic/go-structform/json.(*Visitor).OnObjectFinished", + "github.com/elastic/beats/v7/libbeat/outputs/codec.MakeUTCOrLocalTimestampEncoder.func1", + "github.com/elastic/beats/v7/libbeat/outputs/outil.Selector.Select", + "github.com/elastic/beats/v7/libbeat/outputs/outil.(*listSelector).sel", + "github.com/elastic/beats/v7/libbeat/outputs/outil.(*condSelector).sel", + "github.com/elastic/beats/v7/libbeat/conditions.Matcher.Check", + "github.com/elastic/beats/v7/libbeat/common.MapStr.GetValue", + "github.com/elastic/beats/v7/libbeat/common.mapFind", + "io.(*LimitedReader).Read", + "go.elastic.co/apm/module/apmelasticsearch.(*roundTripper).RoundTrip", + "github.com/elastic/beats/v7/libbeat/common/transport.(*statsConn).Write", + "runtime.resetspinning", + "runtime.bgscavenge.func1", + "runtime.resettimer", + "runtime.gcMarkTermination", + "fmt.Fscan", + "bufio.(*Scanner).Scan", + "github.com/elastic/gosigar/cgroup.memoryData", + "github.com/elastic/beats/v7/libbeat/monitoring.(*structSnapshotVisitor).setValue", + "os.(*File).Readdir", + "github.com/elastic/beats/v7/libbeat/cmd/instance/metrics.reportSystemLoadAverage", + "github.com/elastic/apm-server/beater.(*httpServer).start", + "net/http.(*Server).Serve", + "runtime.(*mheap).freeSpan", + "memeqbody", + "net/http.(*body).Read", + "net/http.(*body).readLocked", + "github.com/json-iterator/go.(*Iterator).Read", + "net/http.(*connReader).Read", + "main.runServerWithProcessors.func1", + "time.when", + "bufio.NewReaderSize", + "go.uber.org/zap.(*SugaredLogger).sweetenFields", + "github.com/google/pprof/profile.decodeField", + "github.com/google/pprof/profile.glob..func19", + "indexbytebody", + "net/http.(*chunkWriter).writeHeader", + "internal/poll.(*FD).Close", + "internal/poll.(*FD).decref", + "internal/poll.(*FD).destroy", + "syscall.Close", + "runtime.entersyscall", + "runtime.reentersyscall", + "compress/flate.(*huffmanBitWriter).indexTokens", + "runtime/pprof.allFrames", + "compress/flate.(*Writer).Close", + "compress/flate.(*compressor).close", + "runtime.mSysStatInc", + "github.com/elastic/apm-server/model.(*System).fields", + "runtime.(*gcBitsArena).tryAlloc", + "fmt.newPrinter", + "runtime.convT64", + "runtime.procyield", + "sync.(*Mutex).Lock", + "runtime.interhash", + "runtime.(*pageAlloc).scavengeOne", + "github.com/elastic/go-structform/gotype.makeNonEmptyFieldFold.func1", + "net/http.setRequestCancel", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.(*jsonReader).stepDictValue", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.(*jsonReader).tryStepPrimitive", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.(*jsonReader).stepMapKey", + "runtime.casgstatus", + "runtime.netpoll", + "runtime.nanotime1", + "runtime.notesleep", + "runtime.(*mcache).prepareForSweep", + "github.com/elastic/beats/v7/libbeat/publisher/queue/memqueue.(*batchBuffer).init", + "net/http.(*persistConn).readResponse", + "net/http.ReadResponse", + "go.elastic.co/apm.(*modelWriter).writeSpan", + "runtime.SetFinalizer", + "github.com/elastic/gosigar.parseMeminfo.func1", + "github.com/elastic/gosigar.(*ProcArgs).Get", + "github.com/elastic/gosigar.(*ProcMem).Get", + "syscall.Readlink", + "syscall.readlinkat", + "fmt.(*ss).scanOne", + "strings.Fields", + "github.com/elastic/gosigar/cgroup.readBlkioValues", + "os.(*File).Readdirnames", + "os.(*File).readdirnames", + "github.com/elastic/beats/v7/libbeat/metric/system/cpu.Load", + "runtime/pprof.profileWriter", + "github.com/elastic/beats/v7/libbeat/publisher/processing.(*processorFn).Run", + "github.com/elastic/beats/v7/libbeat/publisher/processing.newGeneralizeProcessor.func1", + "github.com/elastic/beats/v7/libbeat/common.(*GenericEventConverter).Convert", + "runtime.morestack", + "github.com/json-iterator/go.(*Iterator).nextToken", + "github.com/elastic/apm-server/model/modeldecoder/v2.DecodeNestedError", + "runtime.assertE2I", + "runtime.publicationBarrier", + "github.com/elastic/apm-server/processor/stream.(*Processor).IdentifyEventType", + "compress/flate.(*huffmanDecoder).init", + "runtime.newproc", + "runtime.newproc.func1", + "github.com/elastic/apm-server/beater/api/intake.bodyReader", + "runtime.readvarint", + "github.com/elastic/apm-server/beater/request.(*Context).Write", + "runtime.nilinterhash", + "github.com/elastic/apm-server/beater/middleware.SetRumFlagMiddleware.func1.1", + "github.com/elastic/apm-server/beater/middleware.SetIPRateLimitMiddleware.func1.1", + "github.com/elastic/apm-server/beater/middleware.CORSMiddleware.func2.1", + "github.com/elastic/apm-server/beater/middleware.KillSwitchMiddleware.func1.1", + "github.com/elastic/apm-server/beater/middleware.UserMetadataMiddleware.func1.1", + "go.uber.org/zap.Any", + "go.uber.org/zap/zapcore.(*CheckedEntry).Write", + "runtime.CallersFrames", + "go.elastic.co/apm.NewTraceState", + "go.elastic.co/apm.(*TraceState).parseElasticTracestate", + "runtime.add1", + "compress/zlib.(*reader).Read", + "runtime.duffcopy", + "go.elastic.co/apm.StartSpanOptions", + "runtime.entersyscall_sysmon", + "compress/flate.(*deflateFast).matchLen", + "compress/flate.(*huffmanBitWriter).writeCode", + "compress/gzip.(*Writer).Write", + "compress/flate.NewWriter", + "runtime.memhash64", + "runtime.gostringnocopy", + "runtime.findnull", + "net/http.(*Transport).getConn", + "net/http.(*Transport).queueForIdleConn", + "github.com/elastic/apm-server/model.maybeSetLabels", + "runtime.(*pageAlloc).update", + "runtime.strhash", + "fmt.(*pp).doPrintf", + "fmt.(*pp).printArg", + "runtime.procPin", + "fmt.(*pp).free", + "runtime.spanOf", + "runtime.greyobject", + "runtime.newAllocBits", + "github.com/elastic/apm-server/utility.DeepUpdate", + "github.com/elastic/apm-server/utility.AddID", + "github.com/elastic/beats/v7/libbeat/common.MapStr.DeepUpdate", + "github.com/elastic/beats/v7/libbeat/publisher/queue/memqueue.(*ackProducer).Publish", + "github.com/elastic/beats/v7/libbeat/publisher/queue/memqueue.(*openState).publish", + "sync.(*Mutex).lockSlow", + "github.com/elastic/go-structform/json.str2Bytes", + "runtime.(*pageAlloc).scavengeRangeLocked", + "github.com/elastic/beats/v7/libbeat/common/dtfmt.(*Formatter).AppendTo", + "github.com/elastic/beats/v7/libbeat/common/dtfmt.(*Formatter).appendTo", + "runtime.convT2E", + "github.com/elastic/beats/v7/libbeat/beat.(*Event).GetValue", + "net/http.(*persistConn).roundTrip", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.ignoreKind", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.(*jsonReader).stepString", + "github.com/elastic/beats/v7/libbeat/monitoring/report/elasticsearch.logBulkFailures", + "runtime.epollwait", + "time.sendTime", + "runtime.acquirep", + "runtime.write", + "runtime.write1", + "runtime.markroot", + "runtime.preemptM", + "runtime.signalM", + "runtime.tgkill", + "runtime.startTheWorldWithSema", + "runtime.forEachP", + "runtime.notetsleep", + "fmt.(*ss).doScan", + "github.com/elastic/gosigar/cgroup.SubsystemMountpoints", + "path/filepath.Join", + "path/filepath.join", + "github.com/elastic/gosigar/cgroup.cpuCFS", + "os.(*File).Close", + "os.(*file).close", + "internal/poll.(*FD).ReadDirent", + "syscall.ReadDirent", + "syscall.Getdents", + "runtime.ReadMemStats", + "github.com/elastic/gosigar.(*LoadAverage).Get", + "github.com/elastic/beats/v7/libbeat/monitoring.(*Int).Visit", + "github.com/elastic/beats/v7/libbeat/metric/system/host.ReportInfo", + "net.(*TCPListener).Accept", + "net.(*TCPListener).accept", + "runtime.(*mheap).freeSpan.func1", + "runtime.gopreempt_m", + "github.com/elastic/beats/v7/libbeat/publisher/queue/memqueue.NewQueue.func2", + "github.com/elastic/beats/v7/libbeat/publisher/queue/memqueue.(*ackLoop).run", + "github.com/elastic/apm-server/model/modeldecoder/v2.mapToSpanModel", + "github.com/json-iterator/go.(*Iterator).readStringSlowPath", + "runtime.memequal", + "runtime.memclrHasPointers", + "github.com/json-iterator/go.(*tenFieldsStructDecoder).Decode", + "net/http/internal.(*chunkedReader).Read", + "github.com/elastic/apm-server/model/modeldecoder/nullable.init.0.func7", + "github.com/json-iterator/go.(*Iterator).ReadMapCB", + "github.com/json-iterator/go.(*Iterator).Read.func2", + "github.com/json-iterator/go.(*frozenConfig).getDecoderFromCache", + "compress/flate.(*decompressor).moreBits", + "github.com/elastic/apm-server/publish.(*Publisher).Send", + "github.com/elastic/apm-server/decoder.CompressedRequestReader", + "compress/flate.(*dictDecoder).tryWriteCopy", + "github.com/google/pprof/profile.glob..func5", + "github.com/google/pprof/profile.(*Profile).CheckValid", + "github.com/elastic/apm-server/beater/api/root.Handler.func1", + "github.com/elastic/apm-server/beater/request.(*Context).writeJSON", + "encoding/json.(*Encoder).Encode", + "encoding/json.(*encodeState).reflectValue", + "github.com/elastic/apm-server/processor/stream.(*Processor).getStreamReader", + "runtime.copystack", + "runtime.adjustframe", + "runtime.getStackMap", + "strconv.AppendInt", + "go.elastic.co/ecszap.core.Write", + "go.uber.org/zap/zapcore.(*ioCore).Write", + "go.uber.org/zap.(*Logger).Check", + "go.uber.org/zap.(*Logger).check", + "go.elastic.co/apm/module/apmhttp.StartTransaction", + "bytes.(*Buffer).empty", + "compress/flate.(*dictDecoder).writeCopy", + "io/ioutil.devNull.ReadFrom", + "bytes.IndexByte", + "runtime.convI2I", + "go.elastic.co/apm.newDroppedSpan", + "runtime.(*headTailIndex).incTail", + "net/http.(*chunkWriter).close", + "net/http.(*conn).setState", + "net/textproto.(*Reader).readLineSlice", + "bufio.(*Reader).ReadLine", + "time.AfterFunc", + "runtime.funcline1", + "runtime/pprof.(*profileBuilder).stringIndex", + "compress/flate.(*byFreq).sort", + "sort.Sort", + "runtime.spanClass.sizeclass", + "runtime.(*mcentral).fullSwept", + "runtime.(*mspan).base", + "runtime.gcWriteBarrierCX", + "sync.runtime_procPin", + "runtime.mapdelete_faststr", + "reflect.Value.Len", + "github.com/elastic/apm-server/model.(*StacktraceFrame).transform", + "strings.Split", + "strings.genSplit", + "github.com/elastic/apm-server/utility.TimeAsMicros", + "sync.(*Mutex).Unlock", + "go.elastic.co/apm.(*Transaction).StartSpanOptions", + "strconv.AppendFloat", + "strconv.genericFtoa", + "runtime.isDirectIface", + "github.com/elastic/go-structform/json.(*Visitor).onNumber", + "github.com/elastic/go-structform/json.(*boolStack).push", + "github.com/elastic/go-structform/gotype.foldInt", + "runtime.nilinterequal", + "runtime.efaceeq", + "reflect.name.name", + "reflect.(*rtype).nameOff", + "reflect.resolveNameOff", + "reflect.(*rtype).String", + "reflect.(*rtype).Key", + "reflect.(*rtype).Elem", + "github.com/elastic/go-structform/gotype.reFoldString", + "net/http.(*persistConn).Read", + "github.com/elastic/beats/v7/libbeat/common/streambuf.(*Buffer).Collect", + "github.com/elastic/beats/v7/libbeat/common/streambuf.(*Buffer).Snapshot", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.(*jsonReader).nextInt", + "encoding/json.Unmarshal", + "fmt.Fprintf", + "runtime.wakeScavenger", + "runtime.injectglist", + "runtime.injectglist.func1", + "runtime.usleep", + "runtime.runqget", + "net/http.(*persistConn).readLoop.func2", + "net/http.(*Transport).tryPutIdleConn", + "runtime.modtimer", + "runtime.gcMarkTermination.func3", + "runtime.(*mcache).releaseAll", + "runtime.notetsleep_internal", + "os.(*File).Stat", + "github.com/elastic/gosigar.getProcStatus", + "github.com/elastic/beats/v7/libbeat/metric/system/process.GetSelfPid", + "github.com/elastic/beats/v7/libbeat/monitoring.ReportNamespace", + "os.(*File).readdir", + "os.Lstat", + "runtime.startTheWorld", + "runtime.startTheWorld.func1", + "github.com/elastic/go-sysinfo/providers/linux.(*reader).network", + "github.com/elastic/go-sysinfo/providers/shared.Network", + "syscall.NetlinkRIB", + "github.com/elastic/go-sysinfo/providers/linux.OperatingSystem", + "github.com/elastic/go-sysinfo/providers/linux.getOSInfo", + "github.com/elastic/go-sysinfo/providers/linux.findDistribRelease", + "path/filepath.Glob", + "net.(*netFD).accept", + "internal/poll.(*FD).Accept", + "internal/poll.accept", + "syscall.Accept4", + "runtime._System", + "runtime.(*mheap).freeSpanLocked", + "runtime.(*pageAlloc).free", + "runtime.(*mheap).nextSpanForSweep", + "net/http.(*connReader).backgroundRead", + "go.elastic.co/apm/transport.(*HTTPTransport).SendProfile.func2", + "github.com/elastic/apm-server/model/modeldecoder/v2.mapToStracktraceModel", + "github.com/elastic/apm-server/model/modeldecoder/v2.DecodeNestedMetricset", + "github.com/json-iterator/go.(*placeholderDecoder).Decode", + "net/http/internal.(*chunkedReader).beginChunk", + "net/http/internal.readChunkLine", + "github.com/json-iterator/go.(*sevenFieldsStructDecoder).Decode", + "net/textproto.MIMEHeader.Add", + "github.com/json-iterator/go.(*frozenConfig).useNumber.func1", + "bufio.(*Reader).Read", + "time.After", + "time.NewTimer", + "runtime.funcspdelta", + "github.com/google/pprof/profile.decodeString", + "runtime.mapaccess1_fast64", + "runtime.convT2I", + "encoding/json.(*encodeState).marshal", + "encoding/json.valueEncoder", + "encoding/json.typeEncoder", + "sync.(*entry).load", + "net/http.Header.Get", + "net/textproto.MIMEHeader.Get", + "go.uber.org/zap.(*Logger).With", + "github.com/elastic/beats/v7/libbeat/logp.multiCore.With", + "go.elastic.co/ecszap.core.With", + "go.uber.org/zap/zapcore.(*ioCore).With", + "go.uber.org/zap/zapcore.addFields", + "go.uber.org/zap/zapcore.Field.AddTo", + "github.com/elastic/beats/v7/libbeat/logp.NewLogger", + "go.uber.org/zap.(*Logger).clone", + "go.uber.org/zap/zapcore.(*lockedWriteSyncer).Write", + "os.(*File).write", + "regexp.(*Regexp).doMatch", + "regexp.(*Regexp).doExecute", + "runtime.mapassign_fast64", + "github.com/google/pprof/profile.glob..func37", + "github.com/google/pprof/profile.checkType", + "github.com/google/pprof/profile.glob..func18", + "github.com/google/pprof/profile.decodeInt64s", + "hash/crc32.Update", + "hash/crc32.archUpdateIEEE", + "hash/crc32.ieeeCLMUL", + "mime/multipart.(*Part).Read", + "mime/multipart.partReader.Read", + "runtime.recv", + "runtime.typedslicecopy", + "github.com/elastic/apm-server/model/modeldecoder/v2.mapToTransactionModel", + "github.com/elastic/apm-server/model/modeldecoder/v2.mapToClientModel", + "github.com/elastic/apm-server/utility.ExtractIPFromHeader", + "github.com/elastic/apm-server/model/modeldecoder/nullable.init.0.func3", + "github.com/json-iterator/go.(*Iterator).ReadFloat64", + "github.com/json-iterator/go.(*Iterator).readPositiveFloat64", + "github.com/json-iterator/go.(*Iterator).readFloat64SlowPath", + "runtime.funcInfo.valid", + "net/http.(*chunkWriter).Write", + "net/http.writeStatusLine", + "runtime.mapaccess2_fast64", + "io/ioutil.glob..func1", + "time.Time.Date", + "time.Time.date", + "net/http.(*conn).serve.func1", + "runtime.mapdelete_fast64", + "net/http.(*conn).readRequest.func1", + "net.(*pipeDeadline).set", + "time.Until", + "net/textproto.canonicalMIMEHeaderKey", + "net/textproto.(*Reader).ReadLine", + "context.WithCancel", + "compress/flate.token.length", + "compress/flate.hash", + "runtime/pprof.(*protobuf).endMessage", + "runtime/pprof.(*protobuf).length", + "runtime.FuncForPC", + "sort.insertionSort", + "runtime/pprof.(*protobuf).string", + "runtime.(*Func).Name", + "internal/bytealg.IndexByteString", + "go.elastic.co/apm/transport.(*HTTPTransport).SendProfile", + "github.com/elastic/apm-server/model.(*Process).fields", + "runtime.bool2int", + "runtime.mSysStatDec", + "runtime.divRoundUp", + "runtime.(*mspan).objIndex", + "runtime.heapBits.forwardOrBoundary", + "fmt.(*fmt).fmtBx", + "fmt.(*fmt).fmtSbx", + "runtime.(*mSpanStateBox).get", + "github.com/cespare/xxhash/v2.round", + "github.com/cespare/xxhash/v2.(*Digest).Write", + "runtime.tophash", + "runtime.bucketShift", + "runtime.bucketMask", + "runtime.tooManyOverflowBuckets", + "runtime.(*headTailIndex).cas", + "runtime.(*mheap).tryAllocMSpan", + "runtime.selectnbsend", + "github.com/elastic/beats/v7/libbeat/common.deepUpdateValue", + "github.com/elastic/beats/v7/libbeat/common.tryToMapStr", + "github.com/elastic/beats/v7/libbeat/publisher/pipeline.(*client).onNewEvent", + "go.elastic.co/apm.TransactionFromContext", + "go.elastic.co/apm/internal/apmcontext.DefaultTransactionFromContext", + "go.elastic.co/apm.(*Transaction).StartSpan", + "runtime.(*maptype).hashMightPanic", + "runtime.(*bmap).overflow", + "runtime.(*maptype).indirectelem", + "reflect.toType", + "runtime.duffzero", + "github.com/elastic/go-structform/json.(*boolStack).pop", + "github.com/elastic/go-structform/json.(*Visitor).OnArrayStart", + "github.com/elastic/go-structform/gotype.(*typeFoldRegistry).find", + "runtime.sysUnused", + "runtime.madvise", + "reflect.Value.Interface", + "reflect.valueInterface", + "runtime.resolveNameOff", + "reflect.unpackEface", + "reflect.name.isExported", + "github.com/elastic/beats/v7/libbeat/common/dtfmt.prog.eval", + "github.com/elastic/beats/v7/libbeat/common/dtfmt.appendPadded", + "github.com/elastic/beats/v7/libbeat/esleg/eslegclient.(*jsonEncoder).AddRaw", + "github.com/elastic/beats/v7/libbeat/beat/events.GetOpType", + "sync.(*Once).Do", + "sync.(*Once).doSlow", + "net/http.setRequestCancel.func3.1", + "runtime.closechan", + "net/http.(*bodyEOFSignal).condfn", + "net/http.(*persistConn).readLoop.func4", + "context.WithDeadline", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.(*jsonReader).stepDict", + "github.com/elastic/beats/v7/libbeat/common/streambuf.(*Buffer).ReadByte", + "github.com/elastic/beats/v7/libbeat/common/streambuf.(*Buffer).PeekByteFrom", + "runtime.makechan", + "encoding/json.(*decodeState).unmarshal", + "encoding/json.(*decodeState).array", + "runtime.exitsyscallfast", + "bytes.(*Buffer).Read", + "runtime.runqsteal", + "runtime.runqgrab", + "runtime.runqempty", + "runtime.shouldStealTimers", + "runtime.execute", + "runtime.wakeNetPoller", + "runtime.(*pageAlloc).scavengeReserve", + "runtime.offAddr.lessThan", + "runtime.gopark", + "runtime.markroot.func1", + "runtime.scanstack", + "runtime.scanstack.func1", + "runtime.scanframeworker", + "runtime.scanblock", + "runtime.gcMarkTermination.func4", + "runtime.preemptall", + "runtime.gcMarkDone.func1", + "runtime.retake", + "go.elastic.co/apm/internal/ringbuffer.(*Buffer).WriteBlockTo", + "compress/flate.(*huffmanEncoder).generate", + "compress/flate.(*huffmanEncoder).bitCounts", + "strconv.ParseUint", + "sort.quickSort_func", + "github.com/elastic/gosigar.(*ProcTime).Get", + "fmt.(*ss).scanInt", + "internal/poll.(*FD).Fstat", + "syscall.Fstat", + "strings.(*Builder).grow", + "github.com/elastic/gosigar/cgroup.cpuRT", + "syscall.SetNonblock", + "syscall.fcntl", + "github.com/elastic/gosigar/cgroup.memoryStats", + "github.com/elastic/gosigar/cgroup.parseCgroupParamKeyValue", + "github.com/elastic/gosigar/cgroup.parseUint", + "github.com/elastic/beats/v7/libbeat/monitoring.ReportInt", + "fmt.newScanState", + "syscall.Lstat", + "syscall.fstatat", + "runtime.newMarkBits", + "fmt.(*ss).convertString", + "runtime.SetFinalizer.func2", + "runtime.addfinalizer", + "syscall.accept4", + "syscall.anyToSockaddr", + "github.com/elastic/apm-server/beater.runServerWithTracerServer.func1.1", + "github.com/elastic/apm-server/beater.(*tracerServer).serve", + "runtime.(*waitq).dequeue", + "net.Interfaces", + "net.interfaceTable", + "fmt.(*ss).SkipSpace", + "github.com/elastic/go-sysinfo/providers/linux.(*host).Memory", + "runtime/pprof.readProfile", + "runtime.(*profBuf).read", + "runtime.notetsleepg", + "github.com/elastic/beats/v7/libbeat/publisher/queue/memqueue.(*ackLoop).handleBatchSig", + "io.(*PipeWriter).Write", + "go.elastic.co/apm/transport.(*HTTPTransport).SendProfile.func1", + "github.com/modern-go/reflect2.(*UnsafeStructField).UnsafeGet", + "github.com/elastic/apm-server/model/modeldecoder/nullable.init.0.func4", + "github.com/modern-go/reflect2.(*UnsafeSliceType).UnsafeMakeSlice", + "reflect.unsafe_NewArray", + "github.com/elastic/apm-server/model/modeldecoder/v2.(*metricsetRoot).Reset", + "github.com/elastic/apm-server/model/modeldecoder/v2.(*metricset).Reset", + "runtime.mapclear", + "github.com/json-iterator/go.(*fiveFieldsStructDecoder).Decode", + "github.com/json-iterator/go.(*mapDecoder).Decode", + "github.com/json-iterator/go.(*stringCodec).Decode", + "github.com/elastic/apm-server/model/modeldecoder/v2.mapToErrorModel", + "github.com/elastic/apm-server/model/modeldecoder/v2.mapToExceptionModel", + "github.com/json-iterator/go.(*Iterator).WhatIsNext", + "net/http/internal.isASCIISpace", + "runtime.itabHashFunc", + "github.com/json-iterator/go.(*threeFieldsStructDecoder).Decode", + "bytes.TrimLeft", + "bytes.makeCutsetFunc", + "bytes.Index", + "net/http.(*connReader).startBackgroundRead", + "compress/flate.NewReader", + "runtime.sendDirect", + "github.com/google/pprof/profile.decodeStrings", + "encoding/json.mapEncoder.encode", + "encoding/json.interfaceEncoder", + "encoding/json.Indent", + "github.com/elastic/apm-server/utility.RemoteAddr", + "net/textproto.validHeaderFieldByte", + "runtime.adjustdefers", + "runtime.tracebackdefers", + "go.uber.org/zap/zapcore.(*jsonEncoder).AddInt64", + "go.uber.org/zap/buffer.(*Buffer).AppendInt", + "github.com/elastic/apm-server/beater/middleware.loggerWithTraceContext", + "go.uber.org/zap/zapcore.(*jsonEncoder).AddString", + "go.uber.org/zap/zapcore.(*jsonEncoder).AppendString", + "go.uber.org/zap/zapcore.(*jsonEncoder).safeAddString", + "go.uber.org/zap/buffer.(*Buffer).AppendByte", + "github.com/elastic/apm-server/beater/middleware.loggerWithResult", + "fmt.Sprint", + "fmt.(*pp).doPrint", + "go.uber.org/zap/zapcore.(*jsonEncoder).EncodeEntry", + "go.uber.org/zap.getCallerFrame", + "go.elastic.co/apm/module/apmhttp.ParseTracestateHeader", + "regexp.(*Regexp).get", + "math/rand.(*Rand).Uint64", + "github.com/google/pprof/profile.glob..func40", + "github.com/google/pprof/profile.glob..func22", + "runtime.(*mcentral).partialSwept", + "github.com/google/pprof/profile.getString", + "bytes.(*Buffer).ReadByte", + "compress/flate.(*dictDecoder).writeByte", + "compress/flate.(*dictDecoder).availWrite", + "github.com/elastic/apm-server/beater.newTracerServer.func1", + "github.com/elastic/apm-server/decoder.(*LimitedReader).Read", + "mime/multipart.scanUntilBoundary", + "bytes.LastIndexByte", + "mime/multipart.(*Reader).NextPart", + "mime/multipart.(*stickyErrorReader).Read", + "net.(*pipe).Read", + "net.(*pipe).read", + "net/http.(*response).WriteHeader", + "net/http.Header.Clone", + "github.com/elastic/apm-server/utility.ParseIP", + "github.com/json-iterator/go.(*Iterator).readNumberAsString", + "strconv.ParseFloat", + "strconv.parseFloatPrefix", + "strconv.atof64", + "strconv.readFloat", + "github.com/elastic/apm-server/model/modeldecoder/v2.(*spanRoot).validate", + "github.com/elastic/apm-server/model/modeldecoder/v2.(*span).validate", + "unicode/utf8.RuneCountInString", + "github.com/elastic/apm-server/decoder.NewNDJSONStreamDecoder", + "github.com/elastic/apm-server/decoder.(*NDJSONStreamDecoder).resetDecoder", + "hash/adler32.(*digest).Write", + "hash/adler32.update", + "compress/zlib.NewReader", + "compress/zlib.NewReaderDict", + "compress/zlib.(*reader).Reset", + "go.uber.org/multierr.Append", + "net/http.(*ServeMux).Handler", + "sync.(*RWMutex).RLock", + "net/http.checkConnErrorWriter.Write", + "bufio.(*Writer).WriteString", + "net/http.extraHeader.Write", + "bufio.(*Writer).Write", + "time.Time.abs", + "net/http.(*connReader).abortPendingRead", + "sync.(*Cond).Wait", + "sync.runtime_notifyListWait", + "runtime.acquireSudog", + "net/http.(*conn).close", + "net.(*conn).Close", + "net.(*netFD).Close", + "net/http.(*Server).trackConn", + "internal/poll.(*FD).SetWriteDeadline", + "internal/poll.setDeadlineImpl", + "net.(*pipe).SetWriteDeadline", + "net/url.ParseRequestURI", + "net/url.parse", + "net/http.readRequest.func1", + "net/http.readTransfer", + "net/http.numLeadingCRorLF", + "context.propagateCancel", + "sync.(*copyChecker).check", + "net.(*pipe).SetReadDeadline", + "compress/flate.(*huffmanBitWriter).writeBits", + "compress/flate.(*compressor).init", + "compress/flate.newDeflateFast", + "runtime.(*pageAlloc).alloc", + "runtime.(*pallocBits).find", + "runtime.(*pallocBits).findSmallN", + "compress/flate.newHuffmanBitWriter", + "runtime.growWork_fast64", + "runtime.evacuate_fast64", + "runtime/pprof.(*profileBuilder).pbLine", + "runtime/pprof.(*pcDeck).tryAdd", + "runtime.pcdatavalue1", + "runtime/pprof.scaleHeapSample", + "compress/flate.emitLiteral", + "compress/flate.load32", + "compress/flate.load64", + "runtime/pprof.writeHeapProto.func1", + "runtime/pprof.(*protobuf).int64s", + "runtime/pprof.(*protobuf).uint64s", + "runtime.funcline", + "runtime.(*MemProfileRecord).Stack", + "runtime/pprof.newProfileBuilder", + "runtime/pprof.(*profileBuilder).readMapping", + "runtime/pprof.parseProcSelfMaps", + "runtime.MemProfile", + "runtime.(*MemProfileRecord).InUseObjects", + "mime/multipart.NewWriter", + "io.ReadFull", + "io.ReadAtLeast", + "crypto/rand.(*devReader).Read", + "crypto/rand.batched.func1", + "crypto/rand.getRandomBatch", + "internal/syscall/unix.GetRandom", + "go.elastic.co/apm/transport.(*HTTPTransport).sendProfileRequest", + "net/http.(*cancelTimerBody).Close", + "context.WithDeadline.func3", + "context.(*timerCtx).cancel", + "context.removeChild", + "context.parentCancelCtx", + "runtime.headTailIndex.head", + "net.IP.String", + "net.IP.To4", + "net.isZeros", + "github.com/elastic/apm-server/model.(*Service).Fields", + "github.com/elastic/apm-server/model.(*User).Fields", + "runtime.(*mheap).allocNeedsZero", + "runtime.(*pageAlloc).allocToCache", + "runtime.(*pageAlloc).allocRange", + "runtime.gcTrigger.test", + "fmt.(*pp).fmtBytes", + "sync.runtime_procUnpin", + "runtime.gcmarknewobject", + "runtime.makeSpanClass", + "github.com/cespare/xxhash/v2.(*Digest).Sum64", + "github.com/cespare/xxhash/v2.mergeRound", + "runtime.roundupsize", + "github.com/cespare/xxhash/v2.rol12", + "github.com/cespare/xxhash/v2.rol11", + "github.com/cespare/xxhash/v2.writeBlocks", + "encoding/binary.littleEndian.Uint64", + "github.com/cespare/xxhash/v2.u64", + "runtime.osyield", + "github.com/elastic/apm-server/model.(*System).name", + "github.com/elastic/apm-server/model.(*mapStr).maybeSetMapStr", + "strings.Count", + "countbody", + "github.com/elastic/beats/v7/libbeat/logp.newLogger", + "runtime.(*pageCache).alloc", + "github.com/elastic/apm-server/model.(*Cloud).fields", + "runtime.gcWriteBarrierR8", + "github.com/elastic/apm-server/model.(*Sample).set", + "github.com/elastic/apm-server/model.(*MetricsetSpan).fields", + "github.com/elastic/apm-server/model.(*DestinationService).fields", + "github.com/elastic/apm-server/model.sanitizeLabelKey", + "strings.Map", + "github.com/elastic/apm-server/utility.MillisAsMicros", + "runtime.stkbucket", + "github.com/elastic/apm-server/model.(*Error).calcGroupingKey", + "github.com/elastic/apm-server/model.(*groupingKey).String", + "crypto/md5.(*digest).Sum", + "go.elastic.co/apm.(*Span).enqueue", + "runtime.bulkBarrierPreWrite", + "runtime.selectgo.func2", + "github.com/elastic/beats/v7/libbeat/publisher/queue/memqueue.(*ackProducer).makeRequest", + "sync.(*Mutex).unlockSlow", + "sync.runtime_Semrelease", + "runtime.semrelease1", + "runtime.readyWithTime", + "github.com/elastic/beats/v7/libbeat/monitoring.(*Uint).Inc", + "github.com/elastic/beats/v7/libbeat/publisher/pipeline.(*metricsObserver).newEvent", + "go.elastic.co/apm.(*Transaction).End", + "sync.runtime_canSpin", + "sync.runtime_doSpin", + "sync.runtime_SemacquireMutex", + "go.elastic.co/apm.ContextWithSpan", + "go.elastic.co/apm/internal/apmcontext.DefaultContextWithSpan", + "context.WithValue", + "go.elastic.co/apm.(*Span).reportSelfTime", + "go.elastic.co/apm.spanTimingsMap.add", + "runtime.mapassign", + "go.elastic.co/apm.(*Tracer).startSpan", + "sync.(*poolChain).popHead", + "sync.(*poolDequeue).popHead", + "strings.IndexRune", + "go.elastic.co/apm.(*TraceStateEntry).Validate", + "go.elastic.co/apm.(*TraceStateEntry).validateValue", + "unicode.In", + "regexp.(*machine).init", + "sync.(*Pool).getSlow", + "sync.(*poolChain).popTail", + "sync.(*poolDequeue).popTail", + "math/rand.NewSource", + "math/rand.seedrand", + "go.elastic.co/apm.formatElasticTracestateValue", + "fmt.(*pp).fmtFloat", + "strconv.(*extFloat).FixedDecimal", + "github.com/elastic/go-structform/gotype.foldBool", + "github.com/elastic/go-structform/json.(*Visitor).OnBool", + "reflect.cvtDirect", + "reflect.TypeOf", + "reflect.Value.Pointer", + "reflect.maplen", + "reflect.arrayAt", + "reflect.flag.ro", + "reflect.Value.Index", + "reflect.add", + "github.com/elastic/go-structform/gotype.foldFloat64", + "github.com/elastic/go-structform/json.(*Visitor).OnFloat64", + "strconv.formatDigits", + "runtime.(*mheap).grow", + "runtime.memequal_varlen", + "runtime.(*maptype).indirectkey", + "sync/atomic.(*Value).Load", + "reflect.packEface", + "runtime.(*hmap).growing", + "reflect.Value.Field", + "reflect.Value.String", + "strconv.formatBits", + "github.com/elastic/beats/v7/libbeat/common/dtfmt.(*ctx).initTime", + "time.absDate", + "github.com/elastic/go-structform/json.(*Visitor).OnStringRef", + "github.com/elastic/beats/v7/libbeat/common/dtfmt.newCtx", + "strings.HasPrefix", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.getPipeline", + "github.com/elastic/beats/v7/libbeat/beat/events.GetMetaStringValue", + "net/http.(*bodyEOFSignal).Read", + "time.startTimer", + "runtime.addtimer", + "runtime.cleantimers", + "runtime.releaseSudog", + "net/http.basicAuth", + "github.com/elastic/beats/v7/libbeat/common/streambuf.(*Buffer).Avail", + "github.com/elastic/beats/v7/libbeat/outputs/elasticsearch.(*jsonReader).stepNumber", + "github.com/elastic/beats/v7/libbeat/common/streambuf.(*Buffer).CollectWhile", + "github.com/elastic/beats/v7/libbeat/common/streambuf.(*Buffer).IndexByteFrom", + "github.com/elastic/beats/v7/libbeat/common/streambuf.(*Buffer).Err", + "github.com/elastic/beats/v7/libbeat/common/streambuf.(*Buffer).IntASCII", + "github.com/elastic/beats/v7/libbeat/common/streambuf.(*Buffer).asciiFindNumberEnd", + "go.uber.org/zap.(*SugaredLogger).Debugf", + "github.com/elastic/beats/v7/libbeat/logp.multiCore.Enabled", + "github.com/elastic/beats/v7/libbeat/monitoring/report/elasticsearch.getMonitoringIndexName", + "encoding/json.(*decodeState).objectInterface", + "encoding/json.(*decodeState).valueInterface", + "encoding/json.(*decodeState).literalInterface", + "encoding/json.(*decodeState).rescanLiteral", + "encoding/json.checkValid", + "encoding/json.state1", + "internal/poll.(*FD).Write.func1", + "internal/poll.(*FD).writeLock", + "internal/poll.(*fdMutex).rwlock", + "github.com/elastic/beats/v7/libbeat/outputs.(*Stats).WriteBytes", + "go.elastic.co/apm/internal/iochan.(*Reader).Read", + "io.(*PipeReader).Read", + "io.(*pipe).Read", + "net.(*pipe).Write", + "net.(*pipe).write", + "runtime.chanrecv1", + "runtime.chanrecv", + "net/http.Header.write", + "net/http.Header.writeSubset", + "runtime.mget", + "runtime.read", + "runtime.(*gList).push", + "runtime.dodeltimer0", + "runtime.siftdownTimer", + "runtime.goroutineReady", + "runtime.stackcache_clear", + "runtime.wirep", + "runtime.pidleput", + "runtime.globrunqget", + "runtime.mput", + "runtime.checkdead", + "runtime.releasep", + "runtime.(*randomOrder).start", + "runtime.(*randomEnum).next", + "runtime.(*randomEnum).done", + "runtime.stoplockedm", + "runtime.clearDeletedTimers", + "runtime.puintptr.ptr", + "runtime.chanparkcommit", + "runtime.selparkcommit", + "runtime.parkunlock_c", + "runtime.isSystemGoroutine", + "runtime.exitsyscall0", + "github.com/elastic/beats/v7/libbeat/publisher/queue/memqueue.(*directEventLoop).run", + "runtime.(*hchan).sortkey", + "github.com/elastic/beats/v7/libbeat/common/transport.(*statsConn).Read", + "net/http.(*Transport).setReqCanceler", + "time.(*Timer).Reset", + "net/textproto.(*Reader).readContinuedLineSlice", + "net/textproto.(*Reader).closeDot", + "net/textproto.(*Reader).upcomingHeaderNewlines", + "strings.TrimLeft", + "strings.makeCutsetFunc", + "runtime.doaddtimer", + "runtime.siftupTimer", + "runtime.(*addrRanges).removeLast", + "runtime.(*addrRanges).removeGreaterEqual", + "runtime.(*addrRanges).findSucc", + "runtime.(*pageBits).setRange", + "runtime.pallocSum.max", + "runtime.(*pageAlloc).scavengeUnreserve", + "runtime.(*addrRanges).add", + "runtime.scavengeSleep", + "runtime.goparkunlock", + "runtime.(*gcWork).tryGetFast", + "runtime.markBits.isMarked", + "runtime.(*gcBits).bitp", + "runtime.(*gcBits).bytep", + "runtime.(*gcWork).put", + "runtime.(*gcWork).putFast", + "runtime.addb", + "runtime.spanClass.noscan", + "runtime.heapBits.next", + "runtime.heapBits.bits", + "runtime.spanOfUnchecked", + "runtime.pollWork", + "runtime.(*stackScanState).addObject", + "runtime.markrootSpans", + "runtime.markrootBlock", + "runtime.(*gcWork).tryGet", + "runtime.trygetfull", + "runtime.(*workbuf).checknonempty", + "runtime.gcFlushBgCredit", + "runtime.(*gcWork).balance", + "runtime.(*gcControllerState).enlistWorker", + "runtime.procresize", + "runtime.gcMarkTermination.func4.1", + "runtime.freeStackSpans", + "runtime.stopTheWorldWithSema", + "runtime.timeSleepUntil", + "go.elastic.co/apm.(*SpanData).reset", + "sync.(*poolChain).pushHead", + "go.elastic.co/apm/model.(*Span).MarshalFastJSON", + "go.elastic.co/apm/model.(*SpanID).MarshalFastJSON", + "go.elastic.co/apm/model.writeHex", + "go.elastic.co/apm.(*modelWriter).buildModelSpan", + "math/bits.LeadingZeros64", + "go.elastic.co/apm/internal/ringbuffer.(*Buffer).WriteBlock", + "runtime.selunlock", + "go.elastic.co/fastjson.(*Writer).RawString", + "go.elastic.co/apm.(*modelWriter).writeTransaction", + "go.elastic.co/apm.(*modelWriter).buildModelTransaction", + "go.elastic.co/apm.(*Context).build", + "go.elastic.co/apm.sanitizeRequest", + "go.elastic.co/apm.sanitizeHeaders", + "go.elastic.co/apm/internal/wildcard.Matchers.MatchAny", + "go.elastic.co/apm/internal/wildcard.(*Matcher).Match", + "go.elastic.co/apm/internal/wildcard.hasPrefixLower", + "unicode/utf8.DecodeRuneInString", + "go.elastic.co/apm.(*Tracer).metadataReader", + "go.elastic.co/apm.(*breakdownMetrics).recordTransaction", + "github.com/elastic/beats/v7/libbeat/common.MapStr.Put", + "runtime.walltime1", + "github.com/elastic/gosigar.(*ProcFDUsage).Get.func1", + "io/ioutil.ReadDir", + "sort.Slice", + "sort.insertionSort_func", + "io/ioutil.ReadDir.func1", + "cmpbody", + "github.com/elastic/gosigar.(*ProcExe).Get", + "fmt.(*ss).accept", + "fmt.(*ss).consume", + "fmt.indexRune", + "bytes.Fields", + "github.com/elastic/gosigar/cgroup.parseMountinfoLine", + "github.com/elastic/gosigar/cgroup.SupportedSubsystems", + "path/filepath.(*lazybuf).string", + "runtime.concatstring2", + "runtime.concatstrings", + "bytes.(*Buffer).Grow", + "github.com/elastic/gosigar/cgroup.cpuStat", + "internal/poll.(*FD).eofError", + "github.com/elastic/gosigar/cgroup.(*CPUAccountingSubsystem).get", + "github.com/elastic/gosigar/cgroup.cpuacctUsagePerCPU", + "github.com/elastic/gosigar/cgroup.ProcessCgroupPaths", + "github.com/elastic/gosigar/cgroup.parseBlkioValue", + "os.Getenv", + "syscall.Getenv", + "github.com/elastic/beats/v7/libbeat/cmd/instance/metrics.reportBeatCgroups.func1", + "github.com/elastic/beats/v7/libbeat/cmd/instance/metrics.reportBeatCgroups.func1.2", + "github.com/elastic/beats/v7/libbeat/cmd/instance/metrics.reportBeatCgroups.func1.2.1", + "runtime.createfing", + "runtime.funcdata", + "fmt.(*ss).getBase", + "runtime.assertI2I2", + "os.lstatNolog", + "os.fillFileStatFromSys", + "github.com/elastic/beats/v7/libbeat/monitoring.(*structSnapshotVisitor).OnInt", + "runtime.ReadMemStats.func1", + "runtime.readmemstats_m", + "runtime.updatememstats", + "runtime.flushallmcaches", + "runtime.flushmcache", + "github.com/elastic/gosigar.strtoull", + "fmt.(*ss).notEOF", + "fmt.(*ss).getRune", + "fmt.(*ss).ReadRune", + "runtime.addspecial", + "github.com/elastic/beats/v7/libbeat/metric/system/process.GetProcMemPercentage", + "github.com/elastic/beats/v7/libbeat/common.Round", + "math.pow", + "math.frexp", + "github.com/elastic/beats/v7/libbeat/monitoring.ReportFloat", + "github.com/elastic/beats/v7/libbeat/cmd/instance/metrics.reportInfo", + "runtime.chunkIdx.l2", + "net.(*Interface).Addrs", + "net.interfaceAddrTable", + "syscall.Socket", + "syscall.socket", + "syscall.RawSyscall", + "github.com/prometheus/procfs/internal/util.ReadFileNoStat", + "net.(*netFD).setAddr", + "runtime.spanHasSpecials", + "syscall.Recvfrom", + "syscall.Sendto", + "syscall.sendto", + "github.com/elastic/go-sysinfo/providers/linux.(*reader).containerized", + "github.com/elastic/go-sysinfo/providers/linux.IsContainerized", + "fmt.Sscanf", + "fmt.Fscanf", + "fmt.(*ss).doScanf", + "github.com/elastic/go-sysinfo.Self", + "github.com/elastic/go-sysinfo/providers/linux.linuxSystem.Self", + "github.com/prometheus/procfs.FS.Self", + "github.com/prometheus/procfs.Proc.NewStat", + "github.com/prometheus/procfs.Proc.Stat", + "fmt.isSpace", + "go.elastic.co/apm.(*builtinMetricsGatherer).gatherMemStatsMetrics", + "runtime.gogo", + "runtime/pprof.(*profileBuilder).addCPUData", + "runtime/pprof.(*profMap).lookup", + "runtime.entersyscallblock", + "runtime.entersyscallblock_handoff", + "runtime.handoffp", + "time.Sleep", + "runtime.(*pallocBits).summarize", + "runtime.mergeSummaries", + "runtime/internal/sys.LeadingZeros8", + "runtime.(*mheap).freeMSpanLocked", + "runtime.(*mspan).countAlloc", + "runtime.(*sweepClass).update", + "runtime.(*waitq).dequeueSudoG", + "github.com/elastic/beats/v7/libbeat/publisher/queue/memqueue.(*forgetfulProducer).Publish", + "internal/poll.(*pollDesc).prepareRead", + "internal/poll.(*pollDesc).prepare", + "runtime.preemptPark", + "github.com/elastic/beats/v7/libbeat/publisher/queue/memqueue.(*directEventLoop).processACK", + "github.com/elastic/beats/v7/libbeat/publisher/queue/memqueue.(*bufferingEventLoop).processACK", + "github.com/elastic/beats/v7/libbeat/publisher/queue/memqueue.(*chanList).count", + "go.elastic.co/apm/internal/iochan.(*ReadRequest).Respond", + "bytes.(*Buffer).WriteTo", + "mime/multipart.(*part).Write", + "io.(*pipe).Write", + "mime/multipart.(*Writer).Close", + "mime/multipart.(*Writer).CreatePart", + "net/http.Header.get", + "net/http.(*Transport).dialConnFor", + "net/http.(*Transport).dialConn", + "github.com/patrickmn/go-cache.(*janitor).Run", + "go.elastic.co/apm/transport.(*HTTPTransport).WatchConfig.func1" + ], + "facts": [ + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 80, 140, 602, 718, 27, 9] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 80, 140, 602, 718, 307] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 140, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 253, + 254, + 98, + 99, + 49, + 910 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 140, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 253, + 254, + 98, + 99, + 49, + 179, + 360, + 180, + 508 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 140, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 253, + 254, + 98, + 99, + 49, + 179, + 360, + 180, + 603 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 140, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 253, + 254, + 98, + 99, + 49, + 179, + 911 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 140, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 253, + 254, + 98, + 99, + 331 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 140, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 253, + 254, + 98, + 99, + 180, + 117, + 9, + 38, + 361 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 140, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 253, + 254, + 98, + 99, + 180, + 117, + 70 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 140, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 253, + 254, + 912, + 913, + 147, + 9, + 38, + 39, + 40, + 44, + 43, + 47 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 140, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 253, + 254, + 508 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 140, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 331, + 445 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 140, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 331, + 604 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 140, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 180, + 117, + 9, + 207, + 216, + 234, + 30, + 235, + 181 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 140, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 180, + 117, + 9, + 38, + 361 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 719, + 914, + 915, + 916, + 148, + 605, + 47 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 719, + 90, + 103, + 107, + 111, + 49, + 917, + 49, + 918, + 919, + 180 + ] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 80, 509, 920, 921, 58, 9] + }, + { + "value": 20000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 509, + 90, + 103, + 107, + 111, + 49, + 606, + 49, + 362, + 49, + 720, + 253, + 254, + 98, + 99, + 180 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 509, + 90, + 103, + 107, + 111, + 49, + 606, + 49, + 362, + 49, + 720, + 253, + 254, + 98, + 99, + 49, + 179, + 360, + 922, + 508 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 509, + 90, + 103, + 107, + 111, + 49, + 606, + 49, + 179, + 360, + 180, + 508 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 272, + 273, + 217, + 132, + 123, + 121, + 187, + 274, + 363, + 132, + 446, + 447, + 607, + 721, + 722, + 923 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 236, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 179, + 360, + 180, + 117, + 9 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 236, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 364, + 49, + 723, + 49, + 179, + 724, + 69, + 275, + 295, + 605, + 47 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 236, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 364, + 49, + 723, + 49, + 179, + 608, + 448, + 609, + 610, + 611, + 510, + 208, + 924 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 236, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 364, + 49, + 362, + 49, + 179, + 608, + 448, + 609, + 610, + 107, + 179, + 725, + 448, + 180, + 603, + 136, + 9, + 511 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 236, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 364, + 49, + 362, + 49, + 179, + 608, + 448, + 609, + 610, + 107, + 179, + 725, + 448, + 180, + 603, + 136, + 47 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 236, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 364, + 49, + 362, + 49, + 179, + 724, + 69, + 162, + 148, + 147, + 9, + 38, + 39, + 40, + 44, + 43, + 47 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 236, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 364, + 49, + 925 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 236, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 180, + 117, + 9 + ] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 80, 236, 90, 103, 611, 276, 277] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 80, 512, 926, 927, 27, 9] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 80, 512, 928] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 27, + 9, + 38, + 39, + 40, + 44, + 43, + 47 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 308, + 309, + 310, + 311, + 90, + 272, + 273, + 217, + 132, + 123, + 121, + 209, + 187, + 274, + 363, + 132, + 446, + 447, + 607, + 726, + 449, + 332, + 365, + 149, + 108, + 150, + 151, + 91 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 308, + 309, + 310, + 311, + 90, + 272, + 273, + 217, + 132, + 123, + 121, + 209, + 394, + 513 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 308, + 309, + 310, + 311, + 90, + 272, + 273, + 217, + 132, + 123, + 121, + 209, + 612, + 363, + 132, + 446, + 447, + 929, + 514, + 30, + 515, + 124, + 118, + 104, + 101, + 87 + ] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 308, 309, 310, 311, 90, 103] + }, + { + "value": 20000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 450, 727, 728, 451, 88] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 450, + 613, + 152, + 218, + 171, + 30, + 172, + 188, + 124, + 118, + 104, + 101, + 87 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 516, + 614, + 296, + 366, + 367, + 395, + 58, + 9, + 38, + 39, + 312, + 278 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 516, + 614, + 296, + 366, + 367, + 930, + 27, + 9, + 38, + 39, + 40, + 44, + 43, + 47 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 516, + 614, + 296, + 452, + 58, + 9, + 38, + 39, + 40, + 44, + 43, + 47 + ] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 450, 613, 152, 218, 931, 70] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 93, + 77, + 78, + 123, + 121, + 209, + 615 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 93, + 77, + 78, + 123, + 121, + 187, + 274 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 79, + 109, + 62, + 173, + 62, + 333, + 334, + 136, + 9, + 38, + 39, + 40, + 44, + 43, + 30, + 89, + 105, + 279, + 280 + ] + }, + { + "value": 80000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 79, + 109, + 62, + 173, + 62, + 333, + 334, + 396 + ] + }, + { + "value": 20000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 79, + 109, + 62, + 173, + 62, + 333, + 334, + 58, + 9, + 38, + 39, + 40, + 44, + 43, + 47 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 79, + 109, + 62, + 173, + 62, + 333, + 334, + 58, + 9, + 207, + 216, + 234, + 30, + 235, + 181, + 729, + 297, + 368, + 517 + ] + }, + { + "value": 20000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 616, 27, 9, 86] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 730, 117, 210] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 313, 58, 9, 86] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 932, 136] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 219, 237, 58, 9, 86] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 79, + 219, + 237, + 58, + 9, + 38, + 39, + 40, + 44, + 43, + 30, + 89, + 105 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 79, + 219, + 237, + 397, + 27, + 9, + 86 + ] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 617, 731] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 296, 27, 9, 86] + }, + { "value": 10000000, "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 732] }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 618, + 518, + 619, + 620, + 733, + 621, + 933, + 934, + 621, + 734, + 735, + 276, + 277, + 519, + 314 + ] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 618, 518, 619, 620, 733, 621, 734, 735, 736] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 32, 36, 618, 518, 619, 620, 935] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 32, + 520, + 521, + 522, + 523, + 524, + 54, + 59, + 308, + 309, + 310, + 311, + 90, + 272, + 273, + 217, + 132, + 123, + 121, + 209, + 394, + 513 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 32, + 520, + 521, + 522, + 523, + 524, + 54, + 59, + 308, + 309, + 310, + 311, + 90, + 272, + 273, + 217, + 132, + 123, + 121, + 209, + 615, + 70 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 32, + 520, + 521, + 522, + 523, + 524, + 54, + 59, + 308, + 309, + 310, + 311, + 90, + 272, + 273, + 217, + 132, + 123, + 121, + 209, + 187, + 274 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 14, + 45, + 46, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 32, + 520, + 521, + 522, + 523, + 524, + 54, + 59, + 622, + 452, + 58, + 9, + 30, + 220, + 221, + 278 + ] + }, + { "value": 10000000, "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 33, 34, 35, 155, 88] }, + { "value": 10000000, "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 155, 88] }, + { "value": 10000000, "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 398, 936, 737, 738, 937] }, + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 398, 335, 369, 453, 525, 336, 623, 938, 939, 624, 625] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 398, 335, 369, 739, 740, 741, 742, 743, 744, 940, 941, 626] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 942, 335, 369, 739, 740, 741, 742, 743, 744, 943, 944, 945, 946] + }, + { "value": 10000000, "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 335] }, + { "value": 10000000, "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 947, 335, 369, 453, 525, 337, 208, 281] }, + { "value": 10000000, "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 31, 745, 746, 27, 9] }, + { "value": 10000000, "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 338, 399, 948, 949, 400] }, + { "value": 10000000, "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 338, 399, 526, 627, 628, 950] }, + { + "value": 30000000, + "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 338, 399, 526, 627, 628, 747, 748, 315, 108, 339, 340, 91] + }, + { "value": 10000000, "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 338, 629, 630, 527, 27] }, + { "value": 10000000, "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 338, 629, 630, 155, 88] }, + { "value": 10000000, "layers": [0, 24, 26, 14, 45, 46, 28, 14, 29, 338, 629, 630, 951, 401, 402] }, + { "value": 10000000, "layers": [0, 24, 26, 14, 45, 46, 631, 952, 528, 529, 749, 750, 953, 27, 9] }, + { "value": 10000000, "layers": [0, 24, 26, 14, 45, 46, 631, 341, 954] }, + { "value": 10000000, "layers": [0, 24, 26, 14, 45, 46, 631, 341, 155, 88] }, + { "value": 10000000, "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 617, 731] }, + { "value": 10000000, "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 617, 751] }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 313, 62, 454, 396] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 313, 62, 752, 62, 955] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 313, 62, 752, 62, 454, 396] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 313, 62, 753] + }, + { + "value": 40000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 313, 58, 9, 86] + }, + { + "value": 20000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 313, 136, 210] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 79, + 109, + 62, + 313, + 27, + 9, + 38, + 39, + 40, + 44, + 43, + 47 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 79, + 109, + 62, + 313, + 27, + 9, + 207, + 216, + 234, + 30, + 235, + 181, + 370, + 297, + 368, + 517 + ] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 173, 62, 454, 396] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 79, + 109, + 62, + 173, + 62, + 333, + 334, + 58, + 9, + 38, + 39, + 40, + 44, + 43, + 47 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 79, + 109, + 62, + 173, + 62, + 333, + 334, + 58, + 9, + 38, + 39, + 40, + 44, + 43, + 30, + 89, + 105, + 279, + 280 + ] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 173, 62, 333, 334, 58, 282] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 79, + 109, + 62, + 173, + 62, + 333, + 334, + 136, + 129 + ] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 173, 62, 455, 62, 753] + }, + { + "value": 20000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 173, 62, 455, 62, 454, 396] + }, + { + "value": 20000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 173, 62, 455, 62, 956] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 173, 62, 455, 136, 70] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 79, + 109, + 62, + 173, + 62, + 455, + 136, + 9, + 38, + 39, + 40, + 44, + 43, + 47 + ] + }, + { + "value": 50000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 173, 62, 754, 755, 396] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 79, + 109, + 62, + 173, + 62, + 754, + 755, + 136, + 9, + 38, + 39, + 957 + ] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 173, 136, 70] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 173, 27, 9] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 616, 27, 210] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 616, 62] + }, + { "value": 20000000, "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 454] }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 109, 62, 730, 117, 70] + }, + { "value": 10000000, "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 219, 237, 69] }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 79, + 219, + 237, + 397, + 27, + 9, + 38, + 39, + 40, + 44, + 43, + 47 + ] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 219, 237, 397, 27, 9, 238] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 219, 237, 397, 27, 9, 86] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 219, 237, 397, 27, 210] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 219, 237, 397, 148, 147, 9, 530] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 219, 237, 27, 129] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 219, 237, 58, 9, 38, 39, 40, 371] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 79, + 219, + 237, + 58, + 9, + 38, + 39, + 40, + 44, + 43, + 47 + ] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 219, 237, 58, 9, 238] + }, + { "value": 10000000, "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 79, 219, 958] }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 79, + 296, + 366, + 367, + 395, + 58, + 9, + 38, + 39, + 40, + 44, + 43, + 47 + ] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 93, 77, 78, 123, 121, 187, 274, 632] + }, + { + "value": 20000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 93, 77, 78, 123, 121, 187, 274, 959] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 93, 77, 78, 123, 121, 187, 633, 70] + }, + { + "value": 40000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 93, 77, 78, 123, 121, 209, 187, 274] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 93, 77, 78, 123, 121, 209, 960] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 93, 77, 78, 123, 121, 209, 394] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 93, 77, 78, 123, 121, 961] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 93, 77, 78, 123, 121, 615, 70] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 93, 77, 78, 123, 756, 757, 758] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 93, + 77, + 78, + 182, + 239, + 58, + 9, + 38, + 39, + 312, + 278 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 93, + 77, + 78, + 182, + 239, + 58, + 9, + 38, + 39, + 40, + 44, + 43, + 47 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 53, + 57, + 93, + 77, + 78, + 182, + 239, + 58, + 9, + 30, + 220, + 221, + 43, + 47 + ] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 57, 93, 77, 78, 182, 70] + }, + { "value": 10000000, "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 732] }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 962, 152, 218, 171, 30, 172, 282] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 53, 93, 77, 78, 963, 759, 760, 964, 965] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 50, + 966, + 211, + 212, + 634, + 759, + 760, + 403, + 132, + 967, + 446, + 447, + 607, + 721, + 722, + 217, + 132, + 449, + 968, + 969, + 152, + 761, + 171, + 30, + 172, + 188, + 124, + 118, + 104, + 101, + 87 + ] + }, + { "value": 10000000, "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 50, 518, 970, 971, 762, 70] }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 80, 272, 273, 217, 132, 531, 121, 187, 274] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 272, + 273, + 217, + 132, + 531, + 121, + 187, + 633, + 70 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 236, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 364, + 49, + 362, + 49, + 362, + 49, + 179, + 360, + 180 + ] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 80, 236, 90, 103, 107, 111, 49, 98, 99, 283] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 80, 236, 763, 764, 765, 972] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 80, 236, 763, 764, 765, 737, 738, 331] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 80, 27, 9, 38, 39, 40, 44, 43, 47] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 80, 27, 9, 342, 30, 343, 372, 373, 163, 404] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 140, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 180, + 117, + 9, + 38, + 39, + 40, + 44, + 43, + 30, + 89, + 105, + 279, + 280 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 140, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 179, + 360, + 180, + 117, + 9, + 38, + 39, + 40, + 44, + 43, + 47 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 140, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 179, + 766, + 767, + 768, + 769, + 973, + 136, + 9, + 404 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 140, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 179, + 766, + 767, + 768, + 769, + 974, + 975, + 976, + 977 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 80, + 140, + 90, + 103, + 107, + 111, + 49, + 98, + 99, + 49, + 253, + 254, + 98, + 99, + 180, + 117, + 129 + ] + }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 80, 140, 90, 103, 611, 510, 208, 281] + }, + { "value": 10000000, "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 80, 140, 602, 532] }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 80, 140, 978, 979, 980] + }, + { "value": 10000000, "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 80, 512, 635, 456] }, + { "value": 10000000, "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 80, 512, 117, 70] }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 622, 452, 58, 9, 30, 220, 221, 43, 47] + }, + { "value": 10000000, "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 59, 622, 981, 982] }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 308, + 309, + 310, + 311, + 90, + 272, + 273, + 217, + 132, + 531, + 121, + 209, + 187, + 274 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 59, + 308, + 309, + 310, + 311, + 90, + 272, + 273, + 217, + 132, + 531, + 983, + 984 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 24, + 26, + 28, + 14, + 29, + 31, + 33, + 34, + 35, + 32, + 36, + 37, + 54, + 516, + 985, + 986, + 987, + 395, + 58, + 9, + 38, + 39, + 40, + 44, + 43, + 47 + ] + }, + { "value": 10000000, "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 636, 208] }, + { "value": 10000000, "layers": [0, 24, 26, 28, 14, 29, 31, 33, 34, 35, 32, 36, 37, 54, 405, 533, 637, 255, 344] }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 398, 335, 369, 453, 525, 336, 623, 181, 624, 625, 370, 770] + }, + { "value": 10000000, "layers": [0, 24, 26, 28, 14, 29, 31, 398, 335, 369, 453, 525, 337, 208, 281] }, + { + "value": 10000000, + "layers": [0, 24, 26, 28, 14, 29, 31, 398, 335, 369, 453, 58, 9, 38, 39, 312, 278, 638, 336, 623, 181, 624, 625] + }, + { "value": 10000000, "layers": [0, 24, 26, 28, 14, 29, 338, 399, 526, 988] }, + { "value": 10000000, "layers": [0, 24, 26, 28, 14, 29, 338, 399, 526, 627, 628, 747, 748, 315, 108, 339, 340, 91] }, + { "value": 10000000, "layers": [0, 24, 26, 28, 989, 990] }, + { "value": 70000000, "layers": [0, 24, 374, 256, 991, 345, 346, 315, 108, 339, 340, 91] }, + { "value": 10000000, "layers": [0, 24, 374, 256, 771, 457, 772, 992, 70] }, + { "value": 10000000, "layers": [0, 24, 374, 256, 771, 457, 772, 773] }, + { "value": 10000000, "layers": [0, 24, 374, 639, 457, 993, 994] }, + { "value": 10000000, "layers": [0, 24, 374, 639, 457, 211, 212, 634, 255, 774, 58, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 24, 374, 639, 457, 775, 776, 995] }, + { "value": 10000000, "layers": [0, 24, 374, 996, 997, 998, 999] }, + { "value": 40000000, "layers": [0, 24, 777, 1000, 1001, 1002, 458, 459, 460, 461, 91] }, + { "value": 10000000, "layers": [0, 24, 777, 640, 1003, 778] }, + { "value": 10000000, "layers": [0, 24, 189, 779, 1004, 1005] }, + { "value": 10000000, "layers": [0, 24, 189, 779, 1006, 780, 781, 88] }, + { "value": 20000000, "layers": [0, 24, 189, 155, 406] }, + { "value": 10000000, "layers": [0, 24, 189, 155, 88] }, + { "value": 10000000, "layers": [0, 24, 189, 375, 407, 782, 331, 141] }, + { "value": 10000000, "layers": [0, 24, 189, 375, 407, 69, 141] }, + { "value": 10000000, "layers": [0, 24, 189, 375, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 24, 189, 375, 27, 9, 47] }, + { "value": 10000000, "layers": [0, 24, 189, 375, 1007, 1008] }, + { "value": 10000000, "layers": [0, 24, 189, 375, 1009] }, + { "value": 10000000, "layers": [0, 24, 189, 375, 1010] }, + { "value": 10000000, "layers": [0, 24, 189, 403] }, + { "value": 10000000, "layers": [0, 24, 189, 783, 641, 642, 635, 456] }, + { "value": 10000000, "layers": [0, 24, 189, 783, 641, 642, 217, 132, 449, 332, 365, 149, 108, 150, 151, 91] }, + { "value": 10000000, "layers": [0, 24, 189, 1011] }, + { "value": 10000000, "layers": [0, 24, 189, 784, 1012] }, + { "value": 10000000, "layers": [0, 24, 784, 27, 9, 47] }, + { "value": 10000000, "layers": [0, 24, 155, 88] }, + { + "value": 10000000, + "layers": [0, 24, 403, 132, 449, 332, 365, 149, 108, 150, 151, 91, 462, 463, 30, 534, 104, 101, 87] + }, + { "value": 10000000, "layers": [0, 24, 403, 132, 449, 1013] }, + { "value": 10000000, "layers": [0, 24, 640, 155, 88] }, + { "value": 10000000, "layers": [0, 24, 1014, 780, 643, 451, 88] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 190, 191, 164, 165, 137, 408, 535] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 190, 191, 164, 165, 137, 222, 409, 536] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 190, 191, 164, 165, 137, 222, 409, 1015] }, + { "value": 20000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 190, 191, 164, 165, 137, 222, 785] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 190, 191, 164, 165, 137, 222, 464] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 190, 191, 164, 165, 137, 786] }, + { + "value": 10000000, + "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 190, 191, 537, 538, 1016, 58, 9, 30, 220, 221, 43, 47] + }, + { + "value": 10000000, + "layers": [ + 0, + 60, + 63, + 64, + 65, + 66, + 67, + 71, + 119, + 190, + 191, + 537, + 538, + 1017, + 27, + 9, + 30, + 220, + 221, + 43, + 89, + 105, + 1018, + 1019, + 1020 + ] + }, + { + "value": 10000000, + "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 190, 191, 537, 538, 27, 9, 30, 220, 221, 43, 47] + }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 190, 191, 537, 538, 1021, 58, 9, 38, 39, 40] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 190, 69, 275, 410] }, + { + "value": 10000000, + "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 190, 69, 162, 148, 147, 9, 30, 220, 221, 43, 47] + }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 190, 751, 1022, 1023] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 190, 331, 141] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 190, 787, 788] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 190, 1024] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 773, 539] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 465, 401, 370, 297, 368, 517] }, + { "value": 20000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 465, 401, 402] }, + { "value": 20000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 465, 401, 644, 297, 368, 517] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 465, 401, 644, 770] }, + { "value": 20000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 465, 401, 540, 541, 456] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 645, 69, 141] }, + { "value": 20000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 645, 316] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 1025, 762, 70] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 527, 27, 9, 38, 39, 40, 371] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 527, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 527, 27, 282] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 119, 283] }, + { "value": 20000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 789, 1026, 297, 368] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 789, 402] }, + { "value": 20000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 1027] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 298, 164, 165, 137, 408, 1028] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 298, 164, 165, 137, 408, 535] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 298, 164, 165, 137, 222, 409, 536] }, + { + "value": 10000000, + "layers": [0, 60, 63, 64, 65, 66, 67, 71, 298, 164, 165, 137, 222, 464, 646, 647, 376, 376, 376, 790] + }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 298, 164, 165, 137, 1029] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 298, 466, 467, 137, 222, 409, 536] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 298, 466, 467, 137, 222, 646, 647, 376, 376] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 298, 466, 467, 137, 408] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 298, 791, 70] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 298, 791, 136, 9, 38, 39, 648] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 299, 191, 164, 165, 137, 222, 785] }, + { + "value": 10000000, + "layers": [0, 60, 63, 64, 65, 66, 67, 71, 299, 191, 164, 165, 137, 222, 464, 646, 647, 376, 376, 790] + }, + { "value": 20000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 299, 191, 164, 165, 137, 222, 409] }, + { "value": 20000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 299, 191, 164, 165, 137, 408, 535] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 299, 191, 164, 165, 137, 786] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 299, 191, 164, 165, 137, 1030] }, + { "value": 20000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 299, 1031, 645, 316] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 299, 1032] }, + { "value": 20000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 299, 1033] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 299, 787, 788] }, + { "value": 30000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 532] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 792, 540, 541, 456] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 792, 540, 541, 793] }, + { "value": 60000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 1034, 644, 297, 368] }, + { "value": 30000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 1035, 532] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 71, 1036, 1037, 1038, 117, 9] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 1039, 283] }, + { "value": 10000000, "layers": [0, 60, 63, 64, 65, 66, 67, 1040] }, + { "value": 10000000, "layers": [0, 60, 1041, 1042, 1043, 1044, 1045, 1046, 1047, 91] }, + { "value": 10000000, "layers": [0, 60, 794, 1048, 1049, 1050, 1051, 1052, 1053] }, + { "value": 10000000, "layers": [0, 60, 794, 223, 224, 225, 226, 347, 348, 542, 543, 277] }, + { "value": 30000000, "layers": [0, 2, 3, 4, 12, 25, 76, 92, 83, 27, 9, 284] }, + { "value": 50000000, "layers": [0, 2, 3, 4, 12, 25, 76, 92, 83, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 92, 83, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 92, 83, 27, 9, 207, 216, 234, 30, 235, 181, 370, 297] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 92, 27, 9, 38, 39, 40, 44, 43, 30, 89, 105, 468] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 76, 92, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 92, 27, 9, 38, 39, 40, 1054] }, + { "value": 40000000, "layers": [0, 2, 3, 4, 12, 25, 76, 92, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 92, 27, 9, 404] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 92, 27, 9, 530] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 92, 27, 129] }, + { "value": 70000000, "layers": [0, 2, 3, 4, 12, 25, 76, 92, 69, 141] }, + { "value": 30000000, "layers": [0, 2, 3, 4, 12, 25, 76, 240, 116, 9, 238] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 240, 116, 9, 47] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 76, 240, 116, 9, 38, 39, 40, 44, 43, 30, 89, 105] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 76, 240, 116, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 240, 116, 9, 38, 361, 411] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 240, 116, 9, 284] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 240, 116, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 795, 183, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 795, 183, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 412, 27, 9, 207, 216, 234, 30, 235, 181, 402] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 412, 27, 9, 86] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 76, 412, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 469, 116, 9, 238] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 469, 116, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 469, 83, 27, 9] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 469, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 469, 1055, 1056, 1057] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 1058] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 544, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 544, 27, 9, 38, 39, 40, 44, 43, 30, 89, 377] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 76, 1059] }, + { "value": 180000000, "layers": [0, 2, 3, 4, 12, 25, 116, 129] }, + { "value": 170000000, "layers": [0, 2, 3, 4, 12, 25, 116, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 116, 9, 284] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 116, 9, 38, 39, 40, 44, 43, 30, 89, 105, 279, 280] }, + { "value": 120000000, "layers": [0, 2, 3, 4, 12, 25, 116, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 116, 9, 404] }, + { "value": 60000000, "layers": [0, 2, 3, 4, 12, 25, 116, 9, 238] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 116, 9, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 116, 9, 207, 216, 234, 30, 235, 181, 729, 297] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 116, 9, 342, 30, 343, 372, 373, 163] }, + { "value": 30000000, "layers": [0, 2, 3, 4, 12, 25, 116, 210] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 116, 307, 378] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 116, 796] }, + { "value": 30000000, "layers": [0, 2, 3, 4, 12, 25, 116, 282] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 83, 27, 796] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 83, 27, 9, 511] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 83, 27, 9, 38, 39, 40, 44, 349, 47] }, + { "value": 70000000, "layers": [0, 2, 3, 4, 12, 25, 83, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 83, 27, 9, 38, 39, 40, 44, 43, 30, 89, 105, 279, 280] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 83, 27, 9, 38, 39, 40, 44, 43, 30, 89, 105, 797] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 83, 27, 9, 38, 39, 40, 44, 43, 30, 89, 105, 1060] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 83, 27, 9, 38, 39, 312, 278] }, + { "value": 50000000, "layers": [0, 2, 3, 4, 12, 25, 83, 27, 9, 238] }, + { "value": 200000000, "layers": [0, 2, 3, 4, 12, 25, 83, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 83, 27, 9, 284] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 83, 27, 798] }, + { "value": 90000000, "layers": [0, 2, 3, 4, 12, 25, 83, 27, 129] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 83, 27, 282] }, + { "value": 40000000, "layers": [0, 2, 3, 4, 12, 25, 83, 27, 210] }, + { "value": 120000000, "layers": [0, 2, 3, 4, 12, 25, 183, 9, 86] }, + { "value": 40000000, "layers": [0, 2, 3, 4, 12, 25, 183, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 183, 9, 38, 39, 40, 411] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 183, 9, 38, 39, 649] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 183, 9, 38, 361, 411] }, + { "value": 50000000, "layers": [0, 2, 3, 4, 12, 25, 183, 9, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 183, 9, 799] }, + { "value": 30000000, "layers": [0, 2, 3, 4, 12, 25, 183, 9, 284] }, + { "value": 50000000, "layers": [0, 2, 3, 4, 12, 25, 183, 9, 238] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 183, 9, 210] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 183, 282] }, + { "value": 70000000, "layers": [0, 2, 3, 4, 12, 25, 183, 129] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 183, 210] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 86, 284] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 207, 216, 234, 30, 235, 181, 370, 297] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 207, 216, 234, 30, 235, 181, 370, 413] }, + { "value": 700000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 38, 39, 40, 44, 43, 30, 89, 105, 511] }, + { "value": 40000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 38, 39, 40, 44, 43, 30, 89, 105, 279, 280] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 38, 39, 40, 44, 43, 30, 89, 105, 797] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 38, 39, 40, 44, 43, 30, 89, 105, 1061, 1062, 545] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 38, 39, 40, 44, 43, 30, 89, 105, 468] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 38, 39, 40, 44, 43, 30, 89, 377] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 38, 39, 40, 44, 349, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 38, 39, 40, 44, 349, 800] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 38, 39, 40, 278, 638] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 38, 39, 40, 371] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 38, 39, 40, 650] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 38, 39, 40, 227, 470] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 47] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 284] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 342, 30, 343, 372, 373, 163] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 27, 9, 238, 284] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 27, 1063] }, + { "value": 70000000, "layers": [0, 2, 3, 4, 12, 25, 27, 129] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 27, 210] }, + { "value": 160000000, "layers": [0, 2, 3, 4, 12, 25, 69, 141] }, + { "value": 30000000, "layers": [0, 2, 3, 4, 12, 25, 69, 307] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 69, 546] }, + { + "value": 10000000, + "layers": [0, 2, 3, 4, 12, 25, 130, 547, 548, 801, 802, 651, 307, 378, 30, 414, 415, 379, 803] + }, + { "value": 40000000, "layers": [0, 2, 3, 4, 12, 25, 130, 547, 548, 801, 802, 228] }, + { "value": 60000000, "layers": [0, 2, 3, 4, 12, 25, 130, 547, 548, 1064] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 130, 471, 255, 344, 652, 549] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 130, 471, 255, 344, 228] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 130, 471, 255, 344, 549] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 130, 471, 255, 1065] }, + { "value": 30000000, "layers": [0, 2, 3, 4, 12, 25, 130, 117, 9, 511] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 130, 117, 9, 129] }, + { "value": 40000000, "layers": [0, 2, 3, 4, 12, 25, 130, 117, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 130, 117, 9, 38, 39, 40, 44, 43, 30, 89, 377] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 130, 117, 9, 38, 39, 40, 44, 43, 30, 89, 105] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 130, 117, 9, 38, 361] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 130, 117, 9, 1066] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 130, 117, 9, 404] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 130, 117, 9, 799] }, + { "value": 30000000, "layers": [0, 2, 3, 4, 12, 25, 130, 117, 70] }, + { "value": 50000000, "layers": [0, 2, 3, 4, 12, 25, 130, 117, 129] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 130, 117, 282] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 130, 117, 210] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 130, 117, 1067] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 130, 550, 416, 344, 652] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 130, 550, 416, 344, 549] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 130, 550, 416, 651, 307, 378, 30, 414, 415, 379] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 130, 550, 651, 307, 378, 30, 414, 415, 551] }, + { "value": 250000000, "layers": [0, 2, 3, 4, 12, 25, 241, 1068] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 241, 1069] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 241, 136, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 241, 136, 9, 38, 39, 40, 44, 43, 30, 89, 105] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 241, 136, 9, 38, 39, 312, 278] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 241, 136, 9, 342, 30, 343, 372, 373, 163, 552] }, + { "value": 30000000, "layers": [0, 2, 3, 4, 12, 25, 241, 136, 210] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 241, 136, 282] }, + { "value": 50000000, "layers": [0, 2, 3, 4, 12, 25, 241, 136, 129] }, + { "value": 50000000, "layers": [0, 2, 3, 4, 12, 25, 241, 804] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 241, 1070] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 241, 1071] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 241, 1072] }, + { "value": 30000000, "layers": [0, 2, 3, 4, 12, 25, 300, 92, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 300, 92, 27, 9, 38, 39, 40, 44, 43, 30, 89, 105, 279, 280] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 300, 92, 27, 9, 38, 39, 40, 44, 43, 30, 89, 105, 553, 470] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 300, 92, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 300, 92, 83, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 300, 92, 83, 27, 129] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 300, 92, 69] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 300, 240, 116, 9, 342, 30, 343, 372, 373, 163, 379] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 300, 240, 116, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 58, 9, 86] }, + { "value": 30000000, "layers": [0, 2, 3, 4, 12, 25, 58, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 58, 9, 38, 39, 40, 44, 43, 30, 89, 105] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 58, 9, 530] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 58, 9, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 58, 129] }, + { "value": 190000000, "layers": [0, 2, 3, 4, 12, 25, 417, 805, 70] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 417, 805, 1073] }, + { "value": 90000000, "layers": [0, 2, 3, 4, 12, 25, 417, 804] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 417, 1074] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 417, 1075] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 417, 228] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 228] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 307, 378, 30, 414, 415, 551] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 92, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 92, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 92, 27, 282] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 92, 69, 141] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 92, 83, 27, 9, 38, 39, 40, 44, 43, 30, 89, 166, 174, 1076] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 92, 83, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 240, 116, 9] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 806] }, + { "value": 30000000, "layers": [0, 2, 3, 4, 12, 25, 413] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 807] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 1077] }, + { "value": 30000000, "layers": [0, 2, 3, 4, 12, 25, 472, 9] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 472, 210] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 25, 808] }, + { "value": 30000000, "layers": [0, 2, 3, 4, 12, 25, 410] }, + { "value": 30000000, "layers": [0, 2, 3, 4, 12, 25, 809] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 25, 1078] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 229, 167, 168, 95, 73, 653, 141] }, + { "value": 30000000, "layers": [0, 2, 3, 4, 12, 110, 229, 167, 168, 95, 73, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 229, 167, 168, 95, 73, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 229, 167, 168, 95, 73, 83, 27, 9] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 110, 229, 167, 168, 95, 73, 73, 69] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 110, 229, 167, 168, 95, 73, 69, 141] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 229, 167, 168, 95, 73, 192, 156] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 229, 167, 168, 95, 73, 116, 129] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 229, 167, 168, 95, 73, 654] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 229, 167, 168, 655, 83, 27, 9] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 229, 95, 73, 83, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 229, 95, 73, 73, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 229, 656, 657, 1079, 1080] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 229, 745, 1081, 746, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 76, 92, 27, 9, 38, 39, 40, 810] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 76, 92, 27, 9, 38, 39, 40, 44, 43, 30, 89, 105, 1082] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 76, 92, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 76, 92, 27, 9, 86] }, + { "value": 50000000, "layers": [0, 2, 3, 4, 12, 110, 76, 92, 69] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 76, 92, 83, 27, 9, 86] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 110, 76, 240, 116, 9] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 76, 1083] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 76, 412, 27, 9, 342, 30, 343, 372, 373, 163] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 95, 73, 73, 413] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 95, 73, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 110, 92, 69, 141] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 92, 83, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 658, 83, 27, 210] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 658, 472] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 554, 73, 83, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 554, 83, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 300, 240, 116, 9, 38, 361, 411] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 110, 69] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 95, 73, 69, 1084, 307, 378, 30, 414, 415] }, + { "value": 190000000, "layers": [0, 2, 3, 4, 12, 95, 73, 69, 141] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 95, 73, 69, 546] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 95, 73, 69, 162, 148, 147, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 95, 73, 83, 27, 9] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 95, 73, 653] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 95, 73, 73, 27, 9, 38, 39, 40, 44, 43, 30, 89, 105] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 95, 73, 73, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 95, 73, 73, 27, 210] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 95, 73, 73, 27, 129] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 95, 73, 73, 69, 141] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 95, 73, 809] }, + { "value": 50000000, "layers": [0, 2, 3, 4, 12, 95, 73, 192, 156] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 95, 73, 156] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 95, 73, 283] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 95, 73, 307, 378, 30, 414, 415, 379] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 301, 76, 544, 27, 9, 38, 39, 40, 371] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 301, 76, 544, 192, 156] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 301, 76, 92, 83, 27, 9] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 301, 76, 92, 27, 9, 38, 39, 40, 411] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 301, 76, 240] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 301, 92, 69] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 301, 92, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 301, 554, 73, 73] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 301, 1085, 472, 9] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 301, 1086, 1087, 83, 27, 9, 86] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 12, 555, 69, 162, 148, 147, 9, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 555, 69, 162, 148, 147, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 555, 69, 275, 295, 317] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 555, 83, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 257, 554, 73, 69] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 257, 76, 92, 69, 141] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 257, 76, 92, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 257, 76, 92, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 257, 76, 412, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 257, 76, 1088, 1089] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 257, 1090, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 257, 27, 9, 38, 39, 40] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 257, 83, 27, 129] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 257, 92, 83, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 257, 95, 73, 73, 73, 69, 162, 148, 147, 9, 207, 216, 1091] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 257, 658, 69] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 175, 258, 318, 167, 168, 95, 73, 653] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 175, 258, 318, 167, 168, 95, 73, 116, 9, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 175, 258, 318, 167, 168, 95, 73, 73, 27, 9] }, + { + "value": 10000000, + "layers": [0, 2, 3, 4, 12, 175, 258, 318, 167, 168, 95, 73, 27, 9, 207, 216, 234, 30, 235, 181, 402] + }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 175, 258, 318, 167, 168, 95, 73, 806] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 175, 258, 318, 167, 168, 95, 73, 83, 27, 9] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 175, 258, 318, 167, 168, 655, 83, 27, 129] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 175, 258, 318, 167, 168, 655, 183, 129] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 175, 258, 318, 167, 168, 58, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 175, 258, 1092, 1093, 1094, 136] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 175, 258, 95, 73, 69] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 175, 258, 95, 73, 27, 9, 38, 39, 40, 44, 43, 30, 89, 811] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 175, 92] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 175, 83] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 175, 76, 92, 69] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 175, 76, 412, 83, 27, 9, 284] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 175, 95, 73, 73, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 12, 175, 95, 73, 69, 141] }, + { "value": 60000000, "layers": [0, 2, 3, 4, 12, 418, 419, 88] }, + { + "value": 10000000, + "layers": [0, 2, 3, 4, 12, 418, 1095, 812, 319, 218, 171, 30, 172, 188, 124, 118, 104, 101, 87] + }, + { "value": 30000000, "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 813, 133, 316] }, + { "value": 40000000, "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 813, 133, 156] }, + { "value": 40000000, "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 69, 162, 148, 147, 9, 86] }, + { + "value": 20000000, + "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 69, 162, 148, 147, 9, 38, 39, 40, 44, 43, 30, 89, 377] + }, + { + "value": 10000000, + "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 69, 162, 148, 147, 9, 38, 39, 40, 44, 43, 30, 89, 105, 279, 280] + }, + { + "value": 10000000, + "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 69, 162, 148, 147, 9, 38, 39, 40, 44, 43, 30, 89, 105, 553, 470] + }, + { "value": 20000000, "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 69, 162, 148, 147, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 69, 162, 148, 147, 9, 38, 39, 312, 278] }, + { + "value": 20000000, + "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 69, 162, 148, 147, 9, 342, 30, 343, 372, 373, 163] + }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 69, 162, 148, 147, 9, 342, 30, 343, 88] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 69, 162, 148, 147, 9, 238] }, + { + "value": 10000000, + "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 69, 162, 148, 147, 9, 207, 216, 234, 30, 235, 181, 370, 297, 368] + }, + { "value": 30000000, "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 69, 275, 295, 141] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 69, 275, 295, 546] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 69, 275, 295, 605, 1096] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 69, 275, 295, 317] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 192, 156] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 156] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 169, 133, 283] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 120, 69, 141] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 120, 83, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 120, 83, 27, 9, 238] }, + { "value": 30000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 120, 27, 9, 38, 39, 40, 44, 43, 47] }, + { + "value": 10000000, + "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 120, 27, 9, 38, 39, 40, 44, 43, 30, 89, 105, 468] + }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 120, 27, 9, 86] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 120, 814] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 120, 192, 156] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 120, 410] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 814] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 27, 9, 38, 39, 40, 44, 349] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 27, 9, 38, 39, 649] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 27, 129] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 69] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 192, 156] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 192, 283] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 83, 27, 9, 238] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 83, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 157, 120, 83, 27, 129] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 556, 133, 69, 275, 295] }, + { "value": 80000000, "layers": [0, 2, 3, 4, 81, 75, 84, 556, 133, 331] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 556, 133, 192, 156] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 84, 556, 133, 283] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 557, 558, 152, 166, 174, 473] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 557, 558, 152, 218, 1097, 285, 286] }, + { + "value": 10000000, + "layers": [0, 2, 3, 4, 81, 75, 557, 558, 152, 218, 171, 30, 172, 188, 124, 118, 104, 101, 87] + }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 557, 532] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 27, 9, 38, 39, 40, 44, 349, 800] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 75, 1098] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 659, 1099, 1100, 1101, 1102, 171, 30, 172] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 815, 1103] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 81, 815, 1104] }, + { "value": 20000000, "layers": [0, 2, 3, 4, 1105, 419, 88] }, + { "value": 50000000, "layers": [0, 2, 3, 4, 474, 559, 88] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 474, 559, 1106] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 474, 559, 1107, 473] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 474, 559, 1108] }, + { "value": 30000000, "layers": [0, 2, 3, 4, 405, 533, 660, 419, 88] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 405, 533, 637, 27, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 405, 1109, 1110, 1111, 27, 129] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 405, 816, 817] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 418, 1112, 659] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 418, 416] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 418, 419, 88] }, + { "value": 10000000, "layers": [0, 2, 3, 4, 418, 1113, 1114, 317] }, + { "value": 20000000, "layers": [0, 2, 3, 818, 660, 419, 88] }, + { "value": 10000000, "layers": [0, 2, 3, 818, 660, 1115, 255, 1116, 1117] }, + { "value": 10000000, "layers": [0, 2, 3, 350, 341, 528, 529, 1118] }, + { "value": 10000000, "layers": [0, 2, 3, 350, 341, 528, 529, 1119, 1120, 1121] }, + { "value": 10000000, "layers": [0, 2, 3, 350, 341, 528, 529, 749, 750, 1122] }, + { "value": 10000000, "layers": [0, 2, 3, 350, 341, 155, 88] }, + { "value": 10000000, "layers": [0, 2, 3, 350, 341, 155, 406] }, + { "value": 10000000, "layers": [0, 2, 3, 350, 341, 255, 1123, 1124, 1125] }, + { "value": 10000000, "layers": [0, 2, 3, 350, 1126, 1127] }, + { "value": 10000000, "layers": [0, 2, 3, 350, 1128, 130, 547, 548, 1129, 661, 662, 1130] }, + { + "value": 480000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 192, 156] + }, + { + "value": 60000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 82, 51, 380] + }, + { + "value": 100000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 82, 51, 320] + }, + { + "value": 20000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 82, 51, 153, 48, 74, 70] + }, + { + "value": 360000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 82, 51, 48, 74, 70] + }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 10, + 11, + 15, + 16, + 18, + 1, + 8, + 17, + 21, + 23, + 13, + 1, + 22, + 1, + 8, + 55, + 61, + 13, + 82, + 51, + 48, + 74, + 182, + 239, + 58, + 9, + 30, + 220, + 221, + 349, + 47 + ] + }, + { + "value": 100000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 82, 51, 48, 74, 182, 70] + }, + { + "value": 20000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 82, 51, 48, 74, 228] + }, + { + "value": 40000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 82, 51, 48, 74, 125] + }, + { + "value": 60000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 82, 51, 48, 125] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 82, 51, 560, 320] + }, + { + "value": 90000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 82, 51, 287] + }, + { + "value": 160000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 82, 48, 74, 70] + }, + { + "value": 30000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 82, 48, 125] + }, + { + "value": 40000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 82, 153, 48] + }, + { + "value": 30000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 1, 126, 51, 48, 125] + }, + { + "value": 340000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 1, 126, 51, 48, 74, 70] + }, + { + "value": 30000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 1, 126, 51, 48, 74, 125] + }, + { + "value": 20000000, + "layers": [ + 0, + 5, + 6, + 7, + 10, + 11, + 15, + 16, + 18, + 1, + 8, + 17, + 21, + 23, + 13, + 1, + 22, + 1, + 8, + 55, + 61, + 13, + 1, + 126, + 51, + 153, + 48, + 74, + 70 + ] + }, + { + "value": 90000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 1, 126, 51, 380] + }, + { + "value": 30000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 1, 126, 51, 320] + }, + { + "value": 50000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 1, 126, 51, 560] + }, + { + "value": 30000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 1, 126, 51, 287] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 1, 126, 51, 228] + }, + { + "value": 130000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 1, 242, 475, 314, 539] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 1, 242, 663] + }, + { + "value": 140000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 1, 321, 288, 289, 664] + }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 10, + 11, + 15, + 16, + 18, + 1, + 8, + 17, + 21, + 23, + 13, + 1, + 22, + 1, + 8, + 55, + 61, + 13, + 1, + 321, + 288, + 289, + 48, + 125 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 10, + 11, + 15, + 16, + 18, + 1, + 8, + 17, + 21, + 23, + 13, + 1, + 22, + 1, + 8, + 55, + 61, + 13, + 1, + 321, + 288, + 289, + 48, + 74, + 70 + ] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 1, 321, 288, 289, 287] + }, + { + "value": 40000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 1, 819] + }, + { + "value": 140000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 1, 420] + }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 10, + 11, + 15, + 16, + 18, + 1, + 8, + 17, + 21, + 23, + 13, + 1, + 22, + 1, + 8, + 55, + 61, + 13, + 1, + 8, + 55, + 61, + 13, + 1, + 1131, + 1132 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 10, + 11, + 15, + 16, + 18, + 1, + 8, + 17, + 21, + 23, + 13, + 1, + 22, + 1, + 8, + 55, + 61, + 13, + 1, + 8, + 55, + 61, + 13, + 1, + 126, + 51, + 153 + ] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 1, 22, 82, 51, 48, 125] + }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 10, + 11, + 15, + 16, + 18, + 1, + 8, + 17, + 21, + 23, + 13, + 1, + 22, + 1, + 8, + 55, + 61, + 13, + 1, + 100, + 193, + 213, + 194, + 302, + 303 + ] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 1, 100, 193, 1133] + }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 10, + 11, + 15, + 16, + 18, + 1, + 8, + 17, + 21, + 23, + 13, + 1, + 22, + 1, + 8, + 55, + 61, + 13, + 1, + 100, + 230, + 276, + 277, + 519, + 314 + ] + }, + { + "value": 310000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 156] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 243, 153, 48, 74, 70] + }, + { + "value": 110000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 243, 48, 74, 70] + }, + { + "value": 20000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 243, 48, 74, 125] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 243, 48, 125] + }, + { + "value": 30000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 1134] + }, + { + "value": 60000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 820] + }, + { + "value": 70000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 1135] + }, + { + "value": 30000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 413] + }, + { + "value": 20000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 821] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 822] + }, + { + "value": 50000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 283] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 823] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 13, 228] + }, + { "value": 30000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 665] }, + { + "value": 50000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 304, 48, 74, 70] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 304, 48, 74, 125] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 304, 48, 125] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 304, 153, 48, 74, 70] + }, + { + "value": 80000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 654, 1136] + }, + { "value": 60000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 824] }, + { + "value": 60000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 421, 48, 74, 70] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 61, 421, 153, 48, 74, 70] + }, + { "value": 30000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 1137] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 1138] }, + { "value": 40000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 1139] }, + { "value": 30000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 654] }, + { "value": 30000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 1140] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 400] }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 55, 825, 48, 74] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 8, 826, 242, 141] }, + { + "value": 230000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 351, 352, 51, 48, 74, 70] + }, + { + "value": 90000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 351, 352, 51, 48, 125] + }, + { + "value": 100000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 351, 352, 51, 153] + }, + { + "value": 50000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 351, 352, 51, 380] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 351, 352, 51, 320] + }, + { + "value": 30000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 351, 352, 51, 287] + }, + { + "value": 20000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 351, 352, 51, 560] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 351, 352, 825, 48, 125] + }, + { "value": 50000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 156] }, + { + "value": 20000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 421, 48, 74, 70] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 1, 126, 51, 48, 74, 70] + }, + { + "value": 20000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 1, 126, 51, 48, 125] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 1, 126, 51, 153] + }, + { + "value": 20000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 1, 126, 51, 380] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 1, 126, 51, 287] + }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 10, + 11, + 15, + 16, + 18, + 1, + 8, + 17, + 21, + 23, + 13, + 1, + 22, + 1, + 22, + 1, + 22, + 1, + 1141, + 1142, + 661, + 662, + 1143 + ] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 1, 321, 288, 289, 48, 74] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 1, 321, 288, 289, 664] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 1, 242, 663] }, + { + "value": 20000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 1, 242, 475, 314] + }, + { "value": 30000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 1, 420] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 1, 666] }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 304, 48, 74, 70] + }, + { "value": 80000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 192, 156] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 192, 821] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 824] }, + { + "value": 50000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 82, 51, 48, 74] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 82, 51, 153] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 82, 51, 320] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 82, 51, 380] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 82, 51, 287] }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 82, 48, 74, 70] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 82, 48, 125] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 243, 48, 125] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 283] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 665] }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 10, + 11, + 15, + 16, + 18, + 1, + 8, + 17, + 21, + 23, + 13, + 1, + 22, + 1, + 126, + 51, + 48, + 74, + 182, + 239, + 58, + 9, + 30, + 220, + 221, + 43, + 89, + 105, + 1144, + 322, + 476, + 561, + 827, + 828 + ] + }, + { + "value": 130000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 126, 51, 48, 74, 70] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 126, 51, 48, 74, 228] + }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 126, 51, 48, 125] }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 126, 51, 380, 228] + }, + { "value": 30000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 126, 51, 320] }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 126, 51, 153, 48, 125] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 126, 51, 153, 48, 74] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 126, 51, 228] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 126, 51, 287] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 126, 228] }, + { + "value": 80000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 242, 475, 314, 539] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 242, 663] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 337, 208, 283] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 337, 208, 281] }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 321, 288, 289, 287] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 321, 288, 289, 48, 74, 70] + }, + { + "value": 30000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 100, 230, 276, 277, 667, 668, 445] + }, + { + "value": 20000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 100, 230, 276, 277, 519, 314, 141] + }, + { + "value": 20000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 100, 230, 510, 208, 281] + }, + { + "value": 40000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 100, 193, 213, 194, 302, 303, 669] + }, + { + "value": 20000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 100, 193, 213, 194, 302, 303, 670, 671] + }, + { + "value": 30000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 100, 193, 213, 194, 302, 303, 672] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 100, 193, 213, 194, 302, 400] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 100, 193, 213, 194, 673] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 100, 193, 213, 194, 674] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 100, 303, 669] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 100, 303, 672] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 100, 674] }, + { + "value": 20000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 666, 288, 289, 664] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 666, 288, 289, 48, 74] + }, + { "value": 70000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 420] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 829, 830] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 819] }, + { "value": 30000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 82, 48, 74, 70] }, + { "value": 40000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 82, 48, 125] }, + { "value": 30000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 82, 51, 48, 125] }, + { "value": 160000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 82, 51, 48, 74, 70] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 82, 51, 48, 74, 125] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 82, 51, 153, 48, 74] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 82, 51, 287] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 82, 51, 560] }, + { "value": 40000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 82, 51, 380] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 82, 51, 320] }, + { "value": 30000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 82, 153] }, + { "value": 160000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 156] }, + { "value": 50000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 243, 48, 74] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 243, 48, 125] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 243, 153, 48] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 421, 48, 74, 70] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 421, 153] }, + { "value": 150000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 192, 156] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 304, 48, 74, 70] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 304, 287] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 283] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 410] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 242, 475, 314, 539] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 193, 213, 194, 400] }, + { + "value": 20000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 193, 213, 194, 302, 303, 670, 671, 831] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 193, 213, 194, 302, 303, 669] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 193, 213, 194, 302, 303, 672] + }, + { + "value": 20000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 193, 213, 194, 302, 604] + }, + { + "value": 20000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 193, 213, 194, 302, 194] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 193, 213, 194, 674] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 193, 213, 194, 673] }, + { + "value": 20000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 230, 276, 277, 667, 668, 445] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 230, 276, 277, 667, 668, 1145] + }, + { + "value": 20000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 230, 276, 277, 519, 314, 141] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 230, 276, 1146] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 230, 276, 808] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 230, 276, 807] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 230, 1147] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 230, 736] }, + { "value": 40000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 230, 510, 208, 281] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 673] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 303, 670, 671, 831] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 100, 400] }, + { "value": 70000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 337, 208, 281] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 829, 830, 1148] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 420] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 832] }, + { "value": 80000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 156] }, + { "value": 30000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 192, 156] }, + { "value": 60000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 82, 51, 48, 74, 70] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 82, 51, 48, 74, 125] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 82, 51, 48, 125] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 82, 51, 153] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 82, 51, 320] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 82, 48, 74, 70] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 243, 48, 125] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 243, 48, 74, 70] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 243, 153] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 410] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 820] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1149] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 23, 833] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 17, 21, 477, 82, 51, 320] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 17, 21, 477, 82, 51, 153] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 17, 21, 477, 1150] }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 17, 21, 477, 675, 51, 48, 74, 70] + }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 17, 21, 477, 833] }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 17, 21, 122, 82, 51, 48, 74, 70] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 17, 21, 122, 82, 51, 48, 125] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 17, 21, 122, 82, 51, 153] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 17, 21, 122, 675, 1151] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 17, 21, 122, 675, 51, 48, 74] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 17, 304, 287] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 381, 636, 208, 281] }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 381, 422, 562, 563, 834, 835, 626, 70] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 381, 422, 562, 563, 834, 835, 626, 1152, 70] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 381, 422, 562, 563, 1153] }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 381, 422, 562, 563, 775, 776, 1154] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 381, 422, 1155, 51, 48] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 381, 422, 1156, 255, 344, 652] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 400] }, + { "value": 30000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 82, 51, 48, 74] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 82, 51, 48, 125] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 82, 51, 153] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 21, 122, 82, 48, 74, 70] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 421, 48, 74] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 665] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 17, 304, 48, 74] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 8, 826, 242, 141] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 337, 208, 281] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 242, 475, 314] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 420] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 1, 100, 420] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 822] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 18, 832] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 836, 564, 317, 70] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 15, 16, 836, 564, 9, 86] }, + { "value": 40000000, "layers": [0, 5, 6, 7, 10, 11, 231, 290, 423, 424, 425, 426, 565, 427, 428, 316, 141] }, + { "value": 40000000, "layers": [0, 5, 6, 7, 10, 11, 231, 290, 423, 424, 425, 426, 565, 427, 428, 316, 445] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 231, 290, 423, 424, 425, 426, 565, 427, 428, 316, 546] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 231, 290, 423, 424, 425, 426, 565, 228] }, + { "value": 30000000, "layers": [0, 5, 6, 7, 10, 11, 231, 290, 423, 424, 425, 426, 192, 156] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 231, 290, 423, 424, 425, 426, 1157, 604] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 231, 290, 564, 129] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 231, 290, 564, 317] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 231, 290, 837, 427] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 231, 290, 1158] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 231, 290, 1159, 427, 428, 316] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 231, 837, 427, 428] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 231, 136, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 231, 136, 9, 30, 220, 221, 43, 47] }, + { + "value": 20000000, + "layers": [ + 0, + 5, + 6, + 7, + 10, + 11, + 106, + 93, + 77, + 78, + 195, + 838, + 839, + 840, + 841, + 171, + 30, + 172, + 188, + 124, + 118, + 104, + 101, + 87 + ] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 106, 93, 77, 78, 195, 232, 296, 27] }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 10, + 11, + 106, + 93, + 77, + 78, + 195, + 232, + 296, + 366, + 367, + 395, + 58, + 9, + 38, + 39, + 40, + 44, + 43, + 30, + 89, + 105, + 279, + 280 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 10, + 11, + 106, + 93, + 77, + 78, + 195, + 232, + 123, + 121, + 187, + 612, + 363, + 132, + 1160, + 446, + 447, + 429, + 726, + 676 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 10, + 11, + 106, + 93, + 77, + 78, + 195, + 232, + 123, + 121, + 187, + 612, + 363, + 132, + 842, + 843, + 382, + 319, + 218, + 171, + 30, + 172, + 188, + 124, + 118, + 104, + 101, + 87 + ] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 106, 93, 77, 78, 195, 232, 123, 121, 187, 633, 70] }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 10, + 11, + 106, + 93, + 77, + 78, + 195, + 232, + 123, + 121, + 187, + 274, + 363, + 132, + 842, + 843, + 382, + 319, + 218, + 171, + 30, + 172, + 188, + 124, + 118, + 104, + 101, + 87 + ] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 106, 93, 77, 78, 195, 232, 123, 121, 209, 187] }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 10, + 11, + 106, + 93, + 77, + 78, + 195, + 232, + 123, + 121, + 209, + 394, + 513, + 58, + 9, + 38, + 39, + 40, + 44, + 43, + 30, + 89, + 105, + 279, + 280 + ] + }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 106, 93, 77, 78, 195, 232, 123, 756, 757, 758] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 106, 93, 77, 78, 182, 70] }, + { "value": 30000000, "layers": [0, 5, 6, 7, 10, 11, 106, 93, 77, 78, 182, 239, 58, 9, 30, 220, 221, 43, 47] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 106, 93, 77, 78, 182, 239, 58, 9, 30, 220, 221, 349, 47] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 106, 93, 77, 78, 182, 239, 58, 9, 38, 39, 40, 44, 43, 47] }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 106, 93, 77, 78, 182, 239, 58, 9, 207, 216, 234, 30, 235, 181, 402] + }, + { + "value": 30000000, + "layers": [0, 5, 6, 7, 10, 11, 106, 244, 223, 224, 225, 226, 478, 514, 30, 515, 124, 118, 104, 101, 87] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 106, 244, 223, 224, 225, 226, 478, 844, 643, 1161, 1162, 1163] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 106, 244, 223, 224, 225, 226, 430, 347, 348, 542, 543, 277] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 106, 244, 223, 224, 225, 226, 430, 347, 348, 566, 152, 1164] }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 10, 11, 106, 244, 223, 224, 225, 226, 430, 347, 348, 566, 382, 319, 218, 171, 30, 172, 188] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 106, 244, 1165] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 323, 567, 245, 383, 353, 354, 384] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 323, 567, 245, 479, 480, 1166] }, + { "value": 30000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 323, 567, 354, 384] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 323, 567, 845, 353, 354, 384] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 323, 245, 479, 480, 568, 677] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 323, 245, 479, 480, 1167, 1168] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 323, 245, 479, 480, 678] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 323, 354, 384] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 323, 677] }, + { "value": 20000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 324, 245, 383, 353, 481, 846] }, + { "value": 40000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 324, 245, 383, 353, 481, 568, 1169] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 324, 245, 383, 353, 481, 568, 635, 456] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 324, 245, 383, 353, 481, 568, 677] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 324, 245, 383, 353, 1170] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 324, 245, 383, 847] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 324, 245, 847] }, + { "value": 30000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 324, 354, 384] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 324, 845, 353, 481, 846] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 678] }, + { "value": 30000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 354, 384] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 679, 245, 479, 480, 678] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 679, 1171, 1172] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 142, 679, 354] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 11, 138, 139, 354, 384] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 405, 533, 637, 27, 9, 38, 39, 649] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 10, 1173, 399, 1174] }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 114, + 115, + 15, + 16, + 18, + 1, + 8, + 17, + 21, + 23, + 13, + 1, + 22, + 1, + 22, + 1, + 22, + 1, + 22, + 1, + 22, + 82, + 51, + 48, + 74, + 70 + ] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 114, 115, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 100] + }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 114, + 115, + 15, + 16, + 18, + 1, + 8, + 17, + 21, + 23, + 13, + 1, + 22, + 1, + 22, + 1, + 22, + 1, + 22, + 1, + 22, + 1, + 22, + 82, + 48, + 74 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 114, + 115, + 15, + 16, + 18, + 1, + 8, + 17, + 21, + 23, + 13, + 1, + 22, + 1, + 22, + 1, + 22, + 1, + 22, + 1, + 22, + 1, + 22, + 1, + 242 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 114, + 115, + 15, + 16, + 18, + 1, + 8, + 17, + 21, + 23, + 13, + 1, + 22, + 1, + 22, + 1, + 22, + 1, + 22, + 1, + 22, + 1, + 22, + 243, + 48 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 114, + 115, + 15, + 16, + 18, + 1, + 8, + 17, + 21, + 23, + 13, + 1, + 22, + 1, + 22, + 1, + 22, + 1, + 22, + 1, + 22, + 1, + 321, + 288, + 289, + 48, + 74, + 70 + ] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 114, 115, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 242] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 114, 115, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 243, 48] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 114, 115, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 1, 22, 82, 51, 48, 74, 70] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 114, 115, 15, 16, 18, 1, 8, 17, 21, 23, 13, 1, 22, 1, 22, 1, 100, 230] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 114, 115, 15, 16, 18, 1, 22, 1, 22, 1, 126] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 114, 115, 15, 16, 18, 1, 22, 304] }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 114, 115, 106, 244, 223, 224, 225, 226, 430, 347, 348, 542, 543, 155, 88] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 114, 115, 106, 244, 223, 224, 225, 226, 430, 347, 348, 542, 543, 277] }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 114, 115, 106, 244, 223, 224, 225, 226, 430, 347, 348, 566, 848, 27, 9] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 114, 115, 106, 244, 223, 224, 225, 226, 478, 514, 30, 515, 124, 118, 104, 101, 87] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 114, 115, 106, 244, 223, 224, 225, 226, 478, 781, 88] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 114, 115, 106, 244, 223, 224, 225, 226, 478, 844, 643, 451, 88] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 114, 115, 106, 244, 223, 224, 225, 226, 816, 817] }, + { + "value": 40000000, + "layers": [ + 0, + 5, + 6, + 7, + 114, + 115, + 106, + 93, + 77, + 78, + 195, + 838, + 839, + 840, + 841, + 171, + 30, + 172, + 188, + 124, + 118, + 104, + 101, + 87 + ] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 114, 115, 106, 93, 77, 78, 195, 232, 123, 121, 209, 394, 513] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 114, 115, 106, 93, 77, 78, 195, 232, 123, 121, 209, 394, 274, 363] }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 114, 115, 106, 93, 77, 78, 195, 232, 296, 366, 367, 395, 58, 9, 207, 166, 174] + }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 114, + 115, + 106, + 93, + 77, + 78, + 195, + 232, + 296, + 366, + 367, + 395, + 58, + 9, + 38, + 39, + 40, + 44, + 43, + 30, + 89, + 105, + 553 + ] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 114, 115, 106, 93, 77, 78, 195, 232, 296, 366, 367, 636, 208, 281] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 114, 115, 106, 93, 77, 78, 182, 239, 58, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 114, 115, 1175, 155] }, + { + "value": 10000000, + "layers": [ + 0, + 5, + 6, + 7, + 114, + 115, + 569, + 680, + 849, + 325, + 385, + 325, + 850, + 325, + 385, + 325, + 385, + 325, + 385, + 1176, + 1177, + 1178, + 1179 + ] + }, + { + "value": 10000000, + "layers": [0, 5, 6, 7, 114, 115, 569, 680, 849, 325, 385, 325, 850, 325, 385, 325, 385, 183, 9, 38, 39, 312, 278] + }, + { "value": 10000000, "layers": [0, 5, 6, 7, 114, 115, 569, 680, 1180, 1181] }, + { "value": 10000000, "layers": [0, 5, 6, 7, 114, 115, 569, 316] }, + { + "value": 20000000, + "layers": [0, 176, 246, 259, 211, 212, 355, 256, 326, 431, 345, 346, 315, 108, 339, 340, 91, 386, 851] + }, + { + "value": 10000000, + "layers": [0, 176, 246, 259, 211, 212, 355, 256, 326, 431, 345, 346, 315, 108, 339, 340, 91, 462, 463, 482] + }, + { "value": 10000000, "layers": [0, 176, 246, 259, 211, 212, 355, 256, 326, 431, 345, 346, 315, 108, 1182] }, + { "value": 20000000, "layers": [0, 176, 246, 259, 211, 212, 355, 256, 326, 431, 345, 346, 1183, 1184] }, + { "value": 10000000, "layers": [0, 176, 246, 259, 211, 212, 355, 256, 326, 431, 1185] }, + { "value": 60000000, "layers": [0, 176, 246, 259, 211, 212, 355, 256, 326, 345, 346, 315, 108, 339, 340, 91] }, + { "value": 220000000, "layers": [0, 176, 246, 259, 211, 212, 355, 429, 852, 70] }, + { "value": 10000000, "layers": [0, 176, 246, 259, 211, 212, 355, 429, 632] }, + { "value": 10000000, "layers": [0, 176, 246, 259, 211, 212, 58, 9, 38, 39, 40, 44, 43, 30, 89, 377] }, + { + "value": 10000000, + "layers": [0, 176, 246, 259, 211, 212, 634, 255, 774, 58, 9, 38, 39, 40, 44, 43, 30, 89, 105, 470] + }, + { + "value": 10000000, + "layers": [0, 176, 246, 259, 211, 212, 1186, 152, 218, 171, 30, 172, 188, 124, 118, 104, 101, 87] + }, + { "value": 10000000, "layers": [0, 176, 246, 259, 211, 212, 1187, 1188, 382, 319] }, + { "value": 10000000, "layers": [0, 176, 246, 681, 471, 255] }, + { "value": 20000000, "layers": [0, 176, 382, 319, 218, 171, 30, 172, 188, 124, 118, 104, 101, 87] }, + { "value": 10000000, "layers": [0, 176, 256, 326, 431, 345, 346, 315, 108, 339, 340, 91] }, + { + "value": 10000000, + "layers": [0, 176, 256, 326, 1189, 1190, 1191, 1192, 761, 171, 30, 172, 188, 124, 118, 104, 101, 87] + }, + { "value": 10000000, "layers": [0, 176, 256, 326, 345, 346, 315, 108, 339, 340, 91, 386] }, + { "value": 10000000, "layers": [0, 176, 1193, 1194, 416, 344, 549] }, + { "value": 1940000000, "layers": [0, 52, 85, 56, 432, 124, 118, 104, 101, 87] }, + { "value": 40000000, "layers": [0, 52, 85, 56, 432, 124, 1195] }, + { "value": 10000000, "layers": [0, 52, 85, 56, 432, 124, 166, 174] }, + { "value": 2370000000, "layers": [0, 52, 85, 56, 94, 483, 570] }, + { "value": 140000000, "layers": [0, 52, 85, 56, 94, 483, 1196] }, + { "value": 2840000000, "layers": [0, 52, 85, 56, 94, 196, 247, 260, 433, 682, 683, 684, 118, 104, 101, 87] }, + { "value": 10000000, "layers": [0, 52, 85, 56, 94, 196, 247, 260, 433, 682, 166, 174] }, + { "value": 20000000, "layers": [0, 52, 85, 56, 94, 196, 247, 260, 433, 1197] }, + { "value": 20000000, "layers": [0, 52, 85, 56, 94, 196, 247, 260, 433, 285, 286, 101, 87] }, + { "value": 10000000, "layers": [0, 52, 85, 56, 94, 196, 247, 260, 433, 166, 174] }, + { "value": 30000000, "layers": [0, 52, 85, 56, 94, 196, 247, 260, 571, 155, 406] }, + { "value": 10000000, "layers": [0, 52, 85, 56, 94, 196, 247, 260, 571, 155, 88] }, + { "value": 10000000, "layers": [0, 52, 85, 56, 94, 196, 247, 260, 571, 812, 319, 317] }, + { "value": 60000000, "layers": [0, 52, 85, 56, 94, 196, 247, 260, 1198, 1199] }, + { "value": 10000000, "layers": [0, 52, 85, 56, 94, 196, 247, 260, 1200, 171, 172, 188, 124, 118, 104, 101, 87] }, + { "value": 30000000, "layers": [0, 52, 85, 56, 94, 196, 247, 166] }, + { "value": 40000000, "layers": [0, 52, 85, 56, 94, 88, 484] }, + { "value": 1950000000, "layers": [0, 52, 85, 56, 94, 387, 485, 327, 87] }, + { "value": 10000000, "layers": [0, 52, 85, 56, 94, 387, 572, 486, 1201] }, + { "value": 10000000, "layers": [0, 52, 85, 56, 94, 387, 572, 1202] }, + { "value": 80000000, "layers": [0, 52, 85, 56, 94, 853, 854, 685] }, + { "value": 30000000, "layers": [0, 52, 85, 56, 94, 1203] }, + { "value": 100000000, "layers": [0, 52, 85, 56, 94, 573, 574] }, + { "value": 10000000, "layers": [0, 52, 85, 56, 94, 1204] }, + { "value": 60000000, "layers": [0, 52, 85, 56, 94, 572, 486] }, + { "value": 20000000, "layers": [0, 52, 85, 56, 94, 285, 286] }, + { "value": 20000000, "layers": [0, 52, 85, 56, 94, 686] }, + { "value": 30000000, "layers": [0, 52, 85, 56, 94, 1205, 1206] }, + { "value": 10000000, "layers": [0, 52, 85, 56, 94, 1207] }, + { "value": 10000000, "layers": [0, 52, 85, 56, 94, 855] }, + { "value": 10000000, "layers": [0, 52, 85, 56, 94, 166, 174] }, + { "value": 70000000, "layers": [0, 52, 85, 56, 1208] }, + { "value": 40000000, "layers": [0, 52, 85, 56, 88, 484] }, + { "value": 50000000, "layers": [0, 52, 85, 56, 856] }, + { "value": 130000000, "layers": [0, 52, 85, 56, 1209] }, + { "value": 10000000, "layers": [0, 52, 85, 56, 166, 174, 473] }, + { "value": 20000000, "layers": [0, 52, 85, 56, 857, 482] }, + { "value": 20000000, "layers": [0, 52, 85, 56, 413] }, + { "value": 10000000, "layers": [0, 52, 85, 56, 686] }, + { "value": 10000000, "layers": [0, 52, 85, 56, 1210] }, + { "value": 30000000, "layers": [0, 52, 85, 56, 1211, 485, 327, 87] }, + { "value": 10000000, "layers": [0, 52, 85, 56, 285, 286] }, + { "value": 10000000, "layers": [0, 52, 85, 56, 196, 1212] }, + { "value": 10000000, "layers": [0, 52, 85, 56, 1213] }, + { "value": 10000000, "layers": [0, 52, 85, 1214] }, + { "value": 20000000, "layers": [0, 52, 85, 1215] }, + { "value": 10000000, "layers": [0, 52, 85, 1216] }, + { "value": 20000000, "layers": [0, 52, 233, 197, 56, 94, 88, 484] }, + { "value": 10000000, "layers": [0, 52, 233, 197, 56, 94, 483, 570] }, + { "value": 30000000, "layers": [0, 52, 233, 197, 56, 94, 387, 485, 327, 87] }, + { "value": 10000000, "layers": [0, 52, 233, 197, 56, 94, 387, 572, 486] }, + { "value": 20000000, "layers": [0, 52, 233, 197, 56, 94, 196] }, + { "value": 2020000000, "layers": [0, 52, 233, 197, 56, 88] }, + { "value": 70000000, "layers": [0, 52, 233, 197, 56, 432, 124, 118, 104, 101, 87] }, + { "value": 20000000, "layers": [0, 52, 233, 197, 56, 857, 482] }, + { "value": 20000000, "layers": [0, 52, 233, 197, 56, 285, 286] }, + { "value": 10000000, "layers": [0, 52, 233, 197, 56, 856] }, + { "value": 30000000, "layers": [0, 52, 233, 197, 56, 196] }, + { "value": 40000000, "layers": [0, 52, 233, 197, 482] }, + { "value": 30000000, "layers": [0, 52, 233, 166, 174] }, + { "value": 10000000, "layers": [0, 52, 233, 285, 286] }, + { "value": 20000000, "layers": [0, 52, 291, 56, 94, 387, 485, 327, 87] }, + { "value": 110000000, "layers": [0, 52, 291, 56, 94, 483, 570] }, + { "value": 10000000, "layers": [0, 52, 291, 56, 94, 196, 247, 260, 571, 155, 406] }, + { "value": 10000000, "layers": [0, 52, 291, 56, 94, 196, 247, 260, 433, 682, 683, 684, 118, 104, 101, 87] }, + { "value": 20000000, "layers": [0, 52, 291, 56, 94, 573, 574] }, + { "value": 10000000, "layers": [0, 52, 291, 56, 94, 853, 854, 685] }, + { "value": 60000000, "layers": [0, 52, 291, 56, 94, 88] }, + { "value": 20000000, "layers": [0, 52, 291, 56, 88] }, + { "value": 40000000, "layers": [0, 52, 291, 56, 432, 124, 118, 104, 101, 87] }, + { "value": 10000000, "layers": [0, 52, 291, 56, 686] }, + { "value": 10000000, "layers": [0, 52, 291, 1217, 540, 541, 793] }, + { "value": 30000000, "layers": [0, 52, 1218, 387, 485, 327, 87] }, + { "value": 10000000, "layers": [0, 292, 1219, 152] }, + { "value": 10000000, "layers": [0, 292, 305, 152, 166, 174, 473] }, + { "value": 10000000, "layers": [0, 292, 305, 152, 317] }, + { "value": 10000000, "layers": [0, 292, 305, 152, 285, 286] }, + { "value": 10000000, "layers": [0, 292, 305, 413] }, + { "value": 30000000, "layers": [0, 292, 305, 487, 58, 9, 30, 220, 221, 43, 47] }, + { "value": 10000000, "layers": [0, 292, 305, 487, 58, 9, 30, 220, 221, 349, 47] }, + { "value": 70000000, "layers": [0, 292, 305, 487, 58, 9, 86] }, + { "value": 10000000, "layers": [0, 292, 305, 487, 58, 9, 38, 39, 40, 44, 43, 30, 89, 377] }, + { "value": 10000000, "layers": [0, 292, 305, 487, 58, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 60000000, "layers": [0, 292, 305, 1220] }, + { "value": 80000000, "layers": [0, 248, 152, 218, 171, 30, 172, 188, 124, 118, 104, 101, 87] }, + { + "value": 10000000, + "layers": [0, 248, 403, 132, 676, 1221, 332, 365, 149, 108, 150, 151, 91, 462, 463, 30, 534, 104, 101, 87] + }, + { + "value": 30000000, + "layers": [0, 248, 403, 132, 676, 332, 365, 149, 108, 150, 151, 91, 462, 463, 30, 534, 104, 101, 87] + }, + { "value": 10000000, "layers": [0, 248, 1222, 778] }, + { "value": 20000000, "layers": [0, 248, 687, 688, 155, 406] }, + { "value": 20000000, "layers": [0, 248, 687, 688, 1223, 451, 88] }, + { "value": 10000000, "layers": [0, 248, 687, 688, 242] }, + { "value": 10000000, "layers": [0, 248, 488, 489, 407, 1224, 641, 1225] }, + { "value": 10000000, "layers": [0, 248, 488, 489, 407, 58, 9, 38, 39, 40, 371] }, + { "value": 10000000, "layers": [0, 248, 488, 489, 407, 1226] }, + { "value": 20000000, "layers": [0, 248, 488, 489, 407, 782] }, + { "value": 10000000, "layers": [0, 248, 488, 489, 1227, 1228] }, + { "value": 10000000, "layers": [0, 248, 382, 319, 218, 171, 30, 172, 188, 124, 118, 104, 101, 87] }, + { "value": 890000000, "layers": [0, 170, 434, 689, 858, 573, 574] }, + { "value": 20000000, "layers": [0, 170, 434, 689, 1229, 1230] }, + { "value": 10000000, "layers": [0, 170, 434, 282] }, + { "value": 10000000, "layers": [0, 170, 434, 166, 174] }, + { "value": 10000000, "layers": [0, 170, 434, 285, 286] }, + { "value": 30000000, "layers": [0, 170, 30, 356, 322, 859, 1231] }, + { "value": 10000000, "layers": [0, 170, 30, 356, 322, 859, 1232, 1233] }, + { "value": 3730000000, "layers": [0, 170, 30, 356, 322, 476, 561, 827, 828] }, + { "value": 30000000, "layers": [0, 170, 30, 356, 322, 476, 561, 468] }, + { "value": 10000000, "layers": [0, 170, 30, 356, 322, 476, 561, 1234] }, + { "value": 10000000, "layers": [0, 170, 30, 356, 322, 476, 860] }, + { "value": 50000000, "layers": [0, 170, 30, 356, 322, 1235] }, + { "value": 10000000, "layers": [0, 170, 30, 356, 322, 1236, 1237, 860] }, + { "value": 660000000, "layers": [0, 170, 30, 88] }, + { "value": 10000000, "layers": [0, 170, 30, 285, 286] }, + { "value": 20000000, "layers": [0, 170, 1238, 434, 689, 858, 573, 574] }, + { "value": 260000000, "layers": [0, 170, 88] }, + { "value": 30000000, "layers": [0, 170, 1239, 861] }, + { "value": 10000000, "layers": [0, 170, 166, 174, 327, 87] }, + { "value": 110000000, "layers": [0, 102, 30, 127, 1240] }, + { "value": 10000000, "layers": [0, 102, 30, 127, 131, 163, 379, 650] }, + { "value": 150000000, "layers": [0, 102, 30, 127, 131, 163, 1241] }, + { "value": 10000000, "layers": [0, 102, 30, 127, 131, 163, 552, 1242] }, + { "value": 10000000, "layers": [0, 102, 30, 127, 131, 163, 552, 1243] }, + { "value": 10000000, "layers": [0, 102, 30, 127, 131, 163, 552, 1244] }, + { "value": 370000000, "layers": [0, 102, 30, 127, 131, 163, 404] }, + { "value": 20000000, "layers": [0, 102, 30, 127, 131, 163, 650] }, + { "value": 120000000, "layers": [0, 102, 30, 127, 131, 163, 551] }, + { "value": 60000000, "layers": [0, 102, 30, 127, 131, 163, 1245] }, + { "value": 50000000, "layers": [0, 102, 30, 127, 131, 163, 284] }, + { "value": 10000000, "layers": [0, 102, 30, 127, 131, 163, 1246] }, + { "value": 10000000, "layers": [0, 102, 30, 127, 131, 163, 1247] }, + { "value": 60000000, "layers": [0, 102, 30, 127, 131, 1248] }, + { "value": 40000000, "layers": [0, 102, 30, 127, 131, 530] }, + { "value": 110000000, "layers": [0, 102, 30, 127, 131, 1249] }, + { "value": 50000000, "layers": [0, 102, 30, 127, 131, 1250] }, + { "value": 30000000, "layers": [0, 102, 30, 127, 131, 238] }, + { "value": 10000000, "layers": [0, 102, 30, 127, 131, 1251, 483, 570] }, + { "value": 40000000, "layers": [0, 102, 30, 127, 131, 284] }, + { "value": 10000000, "layers": [0, 102, 30, 127, 131, 575, 862, 863, 181, 864, 865, 866, 379] }, + { "value": 10000000, "layers": [0, 102, 30, 127, 131, 575, 862, 863, 181, 864, 865, 1252] }, + { "value": 10000000, "layers": [0, 102, 30, 127, 131, 575, 1253] }, + { "value": 10000000, "layers": [0, 102, 30, 127, 131, 575, 1254, 866] }, + { "value": 10000000, "layers": [0, 102, 30, 127, 131, 1255, 1256, 1257] }, + { "value": 20000000, "layers": [0, 102, 30, 127, 131, 1258] }, + { "value": 10000000, "layers": [0, 102, 30, 127, 131, 1259, 1260, 576, 577, 578] }, + { "value": 10000000, "layers": [0, 102, 328, 435, 30, 690, 579, 1261, 486, 691, 312, 227] }, + { "value": 10000000, "layers": [0, 102, 328, 435, 30, 690, 579, 104, 101, 87] }, + { "value": 10000000, "layers": [0, 102, 328, 435, 30, 690, 88] }, + { "value": 20000000, "layers": [0, 102, 328, 435, 30, 867, 580, 868, 576, 577, 578] }, + { "value": 10000000, "layers": [0, 102, 328, 435, 30, 867, 580, 1262, 486, 691] }, + { "value": 10000000, "layers": [0, 102, 328, 435, 30, 1263] }, + { "value": 10000000, "layers": [0, 102, 328, 30, 1264, 581, 88] }, + { "value": 10000000, "layers": [0, 102, 328, 30, 869, 580, 581, 692, 327, 87] }, + { "value": 10000000, "layers": [0, 102, 328, 30, 869, 580, 868, 576, 577, 578] }, + { "value": 1850000000, "layers": [0, 261, 262, 329, 118, 104, 101, 87] }, + { "value": 1800000000, "layers": [0, 261, 262, 329, 685] }, + { "value": 1300000000, "layers": [0, 261, 262, 329, 581, 692, 327, 87] }, + { "value": 20000000, "layers": [0, 261, 262, 329, 581, 88, 484] }, + { "value": 290000000, "layers": [0, 261, 262, 329, 1265] }, + { "value": 60000000, "layers": [0, 261, 262, 329, 166, 174] }, + { "value": 10000000, "layers": [0, 261, 262, 329, 683, 684, 118, 104, 101, 87] }, + { "value": 60000000, "layers": [0, 261, 262, 329, 870, 576, 577, 578] }, + { "value": 10000000, "layers": [0, 261, 262, 329, 870, 855] }, + { "value": 40000000, "layers": [0, 261, 262, 88, 484] }, + { "value": 10000000, "layers": [0, 261, 262, 166, 174, 473] }, + { "value": 10000000, "layers": [0, 261, 262, 285, 286] }, + { "value": 10000000, "layers": [0, 198, 490, 1266, 416, 1267, 58, 9] }, + { "value": 10000000, "layers": [0, 198, 490, 1268, 1269, 1270] }, + { "value": 10000000, "layers": [0, 198, 490, 1271] }, + { "value": 10000000, "layers": [0, 198, 490, 661, 662, 1272] }, + { "value": 10000000, "layers": [0, 198, 490, 1273] }, + { "value": 10000000, "layers": [0, 198, 152, 166, 174] }, + { "value": 10000000, "layers": [0, 198, 152, 1274] }, + { "value": 10000000, "layers": [0, 198, 1275] }, + { "value": 10000000, "layers": [0, 198, 871, 164, 165, 137, 408, 535] }, + { "value": 10000000, "layers": [0, 198, 871, 164, 165, 137, 222, 464, 872, 873, 70] }, + { "value": 10000000, "layers": [0, 198, 1276, 1277, 1278, 1279, 1280, 1281, 1282, 1283, 1284] }, + { "value": 10000000, "layers": [0, 198, 1285, 27] }, + { "value": 10000000, "layers": [0, 198, 466, 467, 137, 222, 464, 872, 873] }, + { "value": 10000000, "layers": [0, 198, 466, 467, 137, 222, 409, 536, 228] }, + { "value": 10000000, "layers": [0, 198, 27] }, + { "value": 10000000, "layers": [0, 198, 1286, 192] }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 143, 144, 68, 72, 177, 199, 200, 201, 158, 112, 113, 159, 160, 491, 379] + }, + { + "value": 10000000, + "layers": [ + 0, + 41, + 19, + 20, + 19, + 20, + 42, + 143, + 144, + 68, + 72, + 177, + 199, + 200, + 201, + 158, + 112, + 113, + 159, + 160, + 263, + 264, + 265, + 266, + 267 + ] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 143, 144, 68, 72, 177, 199, 200, 201, 158, 656, 657, 58, 9] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 143, 144, 68, 72, 177, 199, 200, 201, 158, 492, 69, 275, 295, 317] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 143, 144, 68, 72, 177, 199, 200, 201, 158, 492, 69, 162, 148, 147, 9] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 143, 144, 68, 72, 177, 1287, 428] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 143, 144, 68, 72, 177, 27, 210] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 143, 144, 68, 72, 97, 128, 155, 406, 1288] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 143, 144, 68, 72, 97, 128, 155, 88] }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 143, 144, 68, 72, 97, 128, 249, 250, 112, 113, 268, 269, 145] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 143, 144, 68, 72, 97, 128, 249, 250, 158, 1289, 874] }, + { + "value": 10000000, + "layers": [ + 0, + 41, + 19, + 20, + 19, + 20, + 42, + 143, + 144, + 68, + 72, + 97, + 128, + 249, + 250, + 158, + 178, + 77, + 78, + 202, + 149, + 108, + 150, + 151, + 91 + ] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 143, 144, 68, 72, 97, 128, 249, 250, 1290, 1291, 875, 875, 1292, 1293, 1294] + }, + { + "value": 20000000, + "layers": [ + 0, + 41, + 19, + 20, + 19, + 20, + 42, + 143, + 144, + 68, + 72, + 97, + 128, + 493, + 270, + 178, + 77, + 78, + 202, + 149, + 108, + 150, + 151, + 91 + ] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 143, 144, 68, 72, 97, 128, 493, 270, 178, 693, 27, 129] + }, + { + "value": 10000000, + "layers": [ + 0, + 41, + 19, + 20, + 19, + 20, + 42, + 143, + 144, + 68, + 72, + 97, + 128, + 494, + 270, + 178, + 77, + 78, + 202, + 149, + 108, + 150, + 151, + 91 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 41, + 19, + 20, + 19, + 20, + 42, + 143, + 144, + 68, + 72, + 97, + 128, + 876, + 270, + 178, + 77, + 78, + 202, + 149, + 108, + 150, + 151, + 91 + ] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 143, 144, 68, 72, 97, 271, 1295, 495, 496, 145, 386] }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 143, 144, 68, 72, 97, 271, 293, 694, 158, 642, 217, 132, 852] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 143, 144, 68, 72, 97, 271, 293, 436, 582, 497, 877, 1296, 1297, 1298] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 143, 144, 68, 72, 97, 271, 293, 1299] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 143, 144, 68, 695, 495, 496, 145] }, + { "value": 80000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 330, 583, 437, 202, 149, 108, 150, 151, 91] }, + { "value": 20000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 330, 583, 437, 58, 9, 38, 39, 40, 44, 43, 47] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 330, 583, 83, 27, 9] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 330, 583, 1300, 498] }, + { "value": 20000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 330, 112, 113, 268, 269, 145] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 330, 112, 113, 159, 160, 263, 264, 265, 266, 267] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 330, 112, 113, 159, 160, 491, 551] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 330, 1301, 69, 162, 148, 147, 9] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 330, 584, 585, 1302, 1303, 1304] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 388, 586, 294, 178, 693, 878, 879, 91] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 388, 586, 294, 178, 1305, 182] }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 388, 586, 294, 112, 113, 159, 160, 263, 264, 265, 266, 267] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 388, 586, 584, 585, 880, 58, 9] }, + { + "value": 20000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 388, 881, 294, 112, 113, 159, 160, 263, 264, 265, 266, 267] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 388, 881, 294, 112, 113, 268, 269, 145] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 388, 1306, 437, 202, 1307] }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 306, 438, 294, 112, 113, 159, 160, 882, 883, 91, 386, 482] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 306, 438, 294, 112, 113, 159, 160, 27, 9, 86] }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 306, 438, 294, 112, 113, 159, 160, 263, 264, 265, 266, 267] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 306, 438, 294, 112, 113, 268, 269, 145] }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 306, 438, 294, 178, 77, 78, 202, 149, 108, 150, 151, 91] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 306, 438, 294, 178, 587, 588, 458, 459, 460, 461, 91] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 306, 884, 885, 886, 117, 9] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 306, 884, 885, 498] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 306, 112, 113, 268, 269, 145] }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 306, 112, 113, 159, 160, 882, 883, 91, 386, 851] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 1308, 1309, 886, 874] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 584, 585] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 161, 1310, 437, 202, 149, 108, 150, 151, 91] }, + { + "value": 30000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 96, 389, 390, 112, 113, 159, 160, 263, 264, 265, 266, 267] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 389, 390, 112, 113, 268, 269, 145] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 389, 390, 499, 587, 588, 458, 459, 460, 461, 91] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 389, 390, 499, 1311] }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 96, 389, 390, 499, 437, 58, 9, 38, 39, 40, 44, 43, 30, 89, 105, 468] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 96, 389, 390, 499, 437, 58, 9, 38, 39, 40, 44, 43, 30, 89, 811] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 389, 390, 499, 136, 129] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 1312, 1313, 316, 445] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 584, 585, 880, 58] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 696, 1314, 696, 1315, 696, 1316, 887, 439, 69] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 96, 695, 495, 496, 145] }, + { + "value": 30000000, + "layers": [ + 0, + 41, + 19, + 20, + 19, + 20, + 42, + 134, + 154, + 68, + 72, + 177, + 199, + 200, + 201, + 158, + 112, + 113, + 159, + 160, + 263, + 264, + 265, + 266, + 267 + ] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 134, 154, 68, 72, 177, 199, 200, 201, 158, 112, 113, 159, 160, 491, 1317] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 134, 154, 68, 72, 177, 199, 200, 201, 158, 112, 113, 268, 269, 145] + }, + { + "value": 10000000, + "layers": [ + 0, + 41, + 19, + 20, + 19, + 20, + 42, + 134, + 154, + 68, + 72, + 177, + 199, + 200, + 201, + 158, + 178, + 77, + 78, + 182, + 239, + 58, + 9, + 38, + 39, + 40, + 44, + 43, + 47 + ] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 134, 154, 68, 72, 177, 199, 200, 201, 158, 492, 69, 275, 295, 141] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 134, 154, 68, 72, 177, 199, 200, 201, 158, 492, 498] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 134, 154, 68, 72, 177, 199, 200, 201, 83, 27, 129] }, + { + "value": 10000000, + "layers": [ + 0, + 41, + 19, + 20, + 19, + 20, + 42, + 134, + 154, + 68, + 72, + 97, + 271, + 293, + 694, + 452, + 58, + 9, + 207, + 216, + 234, + 30, + 235, + 181, + 1318 + ] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 134, 154, 68, 72, 97, 271, 293, 436, 582, 497, 877, 1319] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 134, 154, 68, 72, 97, 271, 293, 436, 888, 1320, 208, 281] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 134, 154, 68, 72, 97, 271, 293, 436, 888, 255] }, + { + "value": 10000000, + "layers": [ + 0, + 41, + 19, + 20, + 19, + 20, + 42, + 134, + 154, + 68, + 72, + 97, + 271, + 293, + 270, + 178, + 77, + 78, + 202, + 149, + 108, + 150, + 151, + 91 + ] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 134, 154, 68, 72, 97, 128, 249, 250, 440, 697, 698, 1321, 1322] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 134, 154, 68, 72, 97, 128, 249, 250, 440, 697, 698, 889, 890, 145] + }, + { + "value": 10000000, + "layers": [ + 0, + 41, + 19, + 20, + 19, + 20, + 42, + 134, + 154, + 68, + 72, + 97, + 128, + 249, + 250, + 440, + 500, + 501, + 58, + 9, + 38, + 39, + 40, + 44, + 43, + 30, + 89, + 105, + 553, + 891 + ] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 134, 154, 68, 72, 97, 128, 249, 250, 440, 500, 501, 589, 108, 590, 591, 91] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 134, 154, 68, 72, 97, 128, 249, 250, 158, 112, 113, 268, 269, 145, 386] + }, + { + "value": 10000000, + "layers": [ + 0, + 41, + 19, + 20, + 19, + 20, + 42, + 134, + 154, + 68, + 72, + 97, + 128, + 494, + 270, + 112, + 113, + 159, + 160, + 263, + 264, + 265, + 266, + 267 + ] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 134, 154, 68, 72, 97, 128, 494, 270, 112, 113, 268, 269, 145] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 134, 154, 68, 72, 97, 128, 493, 136, 70] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 134, 154, 68, 72, 97, 128, 493, 117, 9, 38, 361] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 134, 887, 1323, 472, 9] }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 134, 592, 30, 1324, 1325, 1326, 1327, 1328, 691, 312, 278] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 134, 592, 699, 30, 700, 88] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 134, 592, 699, 30, 700, 579, 104, 101, 87] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 184, 185, 68, 695, 495, 496, 145] }, + { + "value": 20000000, + "layers": [ + 0, + 41, + 19, + 20, + 19, + 20, + 42, + 184, + 185, + 68, + 72, + 177, + 199, + 200, + 201, + 158, + 112, + 113, + 159, + 160, + 263, + 264, + 265, + 266, + 267 + ] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 184, 185, 68, 72, 177, 199, 200, 201, 158, 492, 69, 275, 295, 317] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 184, 185, 68, 72, 177, 199, 200, 201, 158, 1329] }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 184, 185, 68, 72, 177, 199, 200, 201, 158, 656, 657, 58, 9, 86] + }, + { + "value": 20000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 184, 185, 68, 72, 97, 128, 249, 250, 440, 500, 501, 589, 108, 590, 591, 91] + }, + { + "value": 30000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 184, 185, 68, 72, 97, 128, 249, 250, 440, 697, 698, 889, 890, 145] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 184, 185, 68, 72, 97, 128, 249, 250, 112, 113, 268, 269, 145] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 184, 185, 68, 72, 97, 128, 249, 250, 452, 58, 9] }, + { + "value": 10000000, + "layers": [ + 0, + 41, + 19, + 20, + 19, + 20, + 42, + 184, + 185, + 68, + 72, + 97, + 128, + 876, + 270, + 178, + 77, + 78, + 202, + 149, + 108, + 150, + 151, + 91 + ] + }, + { + "value": 10000000, + "layers": [ + 0, + 41, + 19, + 20, + 19, + 20, + 42, + 184, + 185, + 68, + 72, + 97, + 128, + 493, + 270, + 178, + 77, + 78, + 182, + 239, + 58, + 9, + 38, + 39, + 40, + 44, + 43, + 30, + 89, + 377 + ] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 184, 185, 68, 72, 97, 128, 494, 270, 112, 113, 159, 160, 27] + }, + { + "value": 10000000, + "layers": [ + 0, + 41, + 19, + 20, + 19, + 20, + 42, + 184, + 185, + 68, + 72, + 97, + 128, + 494, + 270, + 178, + 77, + 78, + 202, + 149, + 108, + 150, + 151, + 91 + ] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 184, 185, 68, 72, 97, 271, 293, 436, 582, 497, 892, 1330, 1331, 1332, 632] + }, + { + "value": 10000000, + "layers": [ + 0, + 41, + 19, + 20, + 19, + 20, + 42, + 184, + 185, + 68, + 72, + 97, + 271, + 293, + 270, + 112, + 113, + 159, + 160, + 491, + 30, + 893, + 894, + 1333 + ] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 184, 185, 68, 72, 97, 271, 293, 694, 158, 112, 113, 268, 269, 145] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 184, 185, 68, 72, 1334, 1335, 1336, 1337] }, + { + "value": 30000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 441, 502, 593, 178, 77, 78, 202, 149, 108, 150, 151, 91] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 441, 502, 593, 178, 587, 588, 458, 459, 460, 461, 91] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 441, 502, 593, 498, 228] }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 42, 441, 502, 593, 112, 113, 159, 160, 263, 264, 265, 266, 267] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 441, 502, 27, 9, 38, 39, 40, 371] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 441, 1338, 439, 69, 141] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 42, 1339, 419, 88] }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 19, 20, 19, 20, 19, 20, 19, 20, 594, 439, 27, 9, 38, 39, 40, 44, 43, 30, 89, 105, 1340] + }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 19, 20, 19, 20, 19, 20, 594, 439, 69, 162, 148, 147, 129] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 19, 20, 19, 20, 19, 20, 19, 20, 594, 439, 27, 9, 238] }, + { "value": 10000000, "layers": [0, 41, 19, 20, 19, 20, 19, 20, 19, 20, 594, 439, 27, 9, 86] }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 42, 595, 251, 252, 391, 701, 702, 1341, 1342, 703, 1343, 1344, 1345] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 42, 595, 251, 252, 391, 704, 705, 706, 707, 500, 501, 589, 108, 590, 591, 91] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 42, 595, 251, 252, 357, 392, 93, 77, 78, 429, 202, 149, 108, 150, 151, 91] + }, + { + "value": 10000000, + "layers": [0, 41, 19, 20, 42, 595, 251, 252, 357, 392, 1346, 587, 588, 458, 459, 460, 461, 91] + }, + { "value": 10000000, "layers": [0, 358, 442, 443, 640, 155, 88] }, + { + "value": 10000000, + "layers": [0, 358, 442, 443, 596, 597, 708, 709, 710, 711, 895, 145, 462, 463, 30, 534, 104, 101, 87] + }, + { "value": 10000000, "layers": [0, 358, 442, 443, 596, 597, 708, 709, 710, 711, 895, 145, 386] }, + { "value": 10000000, "layers": [0, 358, 442, 443, 596, 597, 708, 709, 710, 711, 896, 27, 9] }, + { "value": 10000000, "layers": [0, 358, 442, 443, 596, 597, 1347, 491, 30, 893, 894, 1348] }, + { "value": 10000000, "layers": [0, 358, 442, 443, 514, 30, 515, 124, 118, 104, 101, 87] }, + { "value": 20000000, "layers": [0, 358, 897, 898, 450, 727, 728, 451, 88] }, + { "value": 10000000, "layers": [0, 358, 897, 898, 450, 613, 152, 899] }, + { + "value": 20000000, + "layers": [0, 203, 204, 205, 214, 215, 251, 252, 391, 704, 705, 706, 707, 500, 501, 589, 108, 590, 591, 91] + }, + { + "value": 10000000, + "layers": [ + 0, + 203, + 204, + 205, + 214, + 215, + 251, + 252, + 391, + 704, + 705, + 706, + 707, + 112, + 113, + 159, + 160, + 263, + 264, + 265, + 266, + 267 + ] + }, + { "value": 10000000, "layers": [0, 203, 204, 205, 214, 215, 251, 252, 391, 701, 702, 900, 901, 703, 1349, 896] }, + { + "value": 20000000, + "layers": [0, 203, 204, 205, 214, 215, 251, 252, 391, 701, 702, 900, 901, 703, 1350, 1351, 145] + }, + { "value": 10000000, "layers": [0, 203, 204, 205, 214, 215, 251, 252, 391, 1352, 1353, 178, 693, 878, 879, 91] }, + { + "value": 30000000, + "layers": [0, 203, 204, 205, 214, 215, 251, 252, 357, 392, 93, 77, 78, 429, 202, 149, 108, 150, 151, 91] + }, + { "value": 10000000, "layers": [0, 203, 204, 205, 214, 215, 251, 252, 357, 392, 112, 113, 268, 269, 145] }, + { "value": 10000000, "layers": [0, 203, 204, 205, 214, 215, 251, 252, 357, 392, 1354, 1355, 1356, 497, 902] }, + { "value": 10000000, "layers": [0, 203, 204, 205, 214, 215, 251, 252, 357, 392, 498] }, + { "value": 20000000, "layers": [0, 203, 204, 205, 214, 215, 1357, 1358, 1359, 495, 496, 145] }, + { + "value": 20000000, + "layers": [0, 203, 204, 205, 214, 215, 357, 392, 93, 77, 78, 429, 202, 149, 108, 150, 151, 91] + }, + { "value": 10000000, "layers": [0, 203, 204, 205, 214, 215, 357, 823] }, + { "value": 10000000, "layers": [0, 203, 204, 205, 214, 215, 903, 112, 113, 159, 160, 263, 264, 265, 266, 267] }, + { "value": 10000000, "layers": [0, 203, 204, 205, 214, 215, 903, 178, 77, 78, 202, 149, 108, 150, 151, 91] }, + { "value": 10000000, "layers": [0, 203, 204, 205, 214, 215, 1360, 1361, 436, 582, 497, 892, 902, 1362] }, + { "value": 10000000, "layers": [0, 203, 204, 205, 1363, 592, 699, 30, 700, 579, 124, 118, 104, 101, 87] }, + { "value": 40000000, "layers": [0, 712, 30] }, + { "value": 20000000, "layers": [0, 712, 52] }, + { "value": 10000000, "layers": [0, 712, 1364] }, + { "value": 10000000, "layers": [0, 503, 1365, 1366, 58, 9, 207, 216, 234, 30, 235, 181] }, + { "value": 20000000, "layers": [0, 503, 904, 905, 906, 692, 327, 87] }, + { "value": 10000000, "layers": [0, 503, 904, 905, 906, 1367, 30, 1368, 1369] }, + { "value": 10000000, "layers": [0, 503, 88] }, + { "value": 10000000, "layers": [0, 503, 1370, 861] }, + { "value": 30000000, "layers": [0, 88] }, + { "value": 30000000, "layers": [0, 186, 206, 227, 470] }, + { "value": 10000000, "layers": [0, 186, 206, 227, 798] }, + { "value": 280000000, "layers": [0, 186, 206, 227, 444, 30, 598, 713, 714, 545, 1371] }, + { "value": 10000000, "layers": [0, 186, 206, 227, 444, 30, 598, 713, 714, 545, 1372] }, + { "value": 20000000, "layers": [0, 186, 206, 227, 444, 30, 598, 713, 714, 545, 1373] }, + { "value": 10000000, "layers": [0, 186, 206, 227, 444, 30, 598, 1374] }, + { "value": 20000000, "layers": [0, 186, 206, 227, 444, 30, 166, 174] }, + { "value": 20000000, "layers": [0, 186, 206, 227, 444, 30, 285, 286] }, + { "value": 10000000, "layers": [0, 186, 206, 227, 411] }, + { "value": 20000000, "layers": [0, 186, 206, 227, 1375] }, + { "value": 10000000, "layers": [0, 186, 206, 227, 891] }, + { "value": 10000000, "layers": [0, 186, 206, 227, 278, 638] }, + { "value": 10000000, "layers": [0, 186, 206, 715, 371] }, + { "value": 10000000, "layers": [0, 186, 206, 715, 810] }, + { "value": 10000000, "layers": [0, 186, 206, 715, 1376] }, + { "value": 20000000, "layers": [0, 186, 206, 648] }, + { "value": 10000000, "layers": [0, 186, 803] }, + { "value": 10000000, "layers": [0, 359, 1377] }, + { + "value": 10000000, + "layers": [0, 359, 393, 75, 84, 504, 505, 506, 135, 146, 135, 146, 135, 146, 135, 146, 135, 146, 135, 410] + }, + { + "value": 10000000, + "layers": [ + 0, + 359, + 393, + 75, + 84, + 504, + 505, + 506, + 135, + 146, + 135, + 146, + 135, + 146, + 135, + 146, + 135, + 146, + 135, + 146, + 135, + 146, + 135, + 192 + ] + }, + { + "value": 10000000, + "layers": [0, 359, 393, 75, 84, 504, 505, 506, 135, 146, 135, 146, 135, 146, 135, 146, 135, 146, 337] + }, + { + "value": 10000000, + "layers": [0, 359, 393, 75, 84, 504, 505, 506, 135, 146, 135, 146, 135, 146, 135, 146, 337, 208] + }, + { "value": 10000000, "layers": [0, 359, 393, 75, 84, 504, 505, 506, 135, 146, 135, 156] }, + { "value": 10000000, "layers": [0, 359, 393, 75, 1378, 558, 152, 899] }, + { "value": 10000000, "layers": [0, 359, 393, 659] }, + { "value": 20000000, "layers": [0, 716, 332, 365, 149, 108, 150, 151, 91] }, + { "value": 10000000, "layers": [0, 716, 332, 365, 1379, 1380] }, + { "value": 20000000, "layers": [0, 716, 332, 27, 9] }, + { "value": 10000000, "layers": [0, 507, 336, 599, 197, 56, 166, 174] }, + { "value": 70000000, "layers": [0, 507, 336, 599, 197, 56, 88] }, + { "value": 10000000, "layers": [0, 507, 336, 599, 197, 56, 432, 124, 118, 104, 101, 87] }, + { "value": 10000000, "layers": [0, 507, 336, 599, 197, 56, 124, 118, 104, 101, 87] }, + { "value": 10000000, "layers": [0, 507, 336, 1381, 56, 94, 88] }, + { "value": 10000000, "layers": [0, 600, 601, 907, 1382, 155, 88] }, + { "value": 10000000, "layers": [0, 600, 601, 907, 1383] }, + { "value": 10000000, "layers": [0, 600, 601, 1384] }, + { "value": 10000000, "layers": [0, 600, 601, 152, 166, 174] }, + { "value": 30000000, "layers": [0, 1385, 382, 319, 218, 171, 30, 172, 188, 124, 118, 104, 101, 87] }, + { "value": 10000000, "layers": [0, 717, 211, 212, 1386, 1387, 908, 1388, 152] }, + { "value": 10000000, "layers": [0, 717, 909, 1389, 681, 908, 474] }, + { "value": 10000000, "layers": [0, 717, 909, 1390, 681, 74, 182, 239, 58, 9, 38, 39, 648] }, + { "value": 10000000, "layers": [0, 1391, 331] }, + { "value": 10000000, "layers": [0, 1392, 1393, 27, 9, 38, 39, 40, 44, 43, 30, 89, 105, 279, 280] }, + { "value": 10000000, "layers": [0, 1394, 152, 282] }, + { "value": 10000000, "layers": [0, 1395, 223, 224, 225, 226, 347, 348, 566, 848, 27] } + ] +} diff --git a/packages/osd-charts/src/mocks/hierarchical/dimension_codes.ts b/packages/osd-charts/src/mocks/hierarchical/dimension_codes.ts new file mode 100644 index 000000000000..d128e3a95cf1 --- /dev/null +++ b/packages/osd-charts/src/mocks/hierarchical/dimension_codes.ts @@ -0,0 +1,309 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const productDimension = [ + { sitc1: '0', name: 'Food and live animals' }, + { sitc1: '1', name: 'Beverages and tobacco' }, + { sitc1: '2', name: 'Crude materials, inedible, except fuels' }, + { sitc1: '3', name: 'Mineral fuels, lubricants and related materials' }, + { sitc1: '4', name: 'Animal and vegetable oils, fats and waxes' }, + { sitc1: '5', name: 'Chemicals and related products' }, + { sitc1: '6', name: 'Manufactured goods classified chiefly by material' }, + { sitc1: '7', name: 'Machinery and transport equipment' }, + { sitc1: '8', name: 'Miscellaneous manufactured articles' }, + { sitc1: '9', name: 'Commodities and transactions not classified elsewhere' }, +]; + +/** @internal */ +export const regionDimension = [ + { region: 'af', regionName: 'Africa' }, + { region: 'as', regionName: 'Asia' }, + { region: 'eu', regionName: 'Europe' }, + { region: 'na', regionName: 'North America' }, + { region: 'sa', regionName: 'South America' }, + { region: 'oc', regionName: 'Oceania' }, +]; + +/** @internal */ +export const countryDimension = [ + { continentCountry: 'afago', country: 'ago', name: 'Angola' }, + { continentCountry: 'afbdi', country: 'bdi', name: 'Burundi' }, + { continentCountry: 'afben', country: 'ben', name: 'Benin' }, + { continentCountry: 'afbfa', country: 'bfa', name: 'Burkina Faso' }, + { continentCountry: 'afbwa', country: 'bwa', name: 'Botswana' }, + { continentCountry: 'afcaf', country: 'caf', name: 'Central African Republic' }, + { continentCountry: 'afciv', country: 'civ', name: "Cote d'Ivoire" }, + { continentCountry: 'afcmr', country: 'cmr', name: 'Cameroon' }, + { continentCountry: 'afcod', country: 'cod', name: 'Democratic Republic of the Congo' }, + { continentCountry: 'afcog', country: 'cog', name: 'Republic of the Congo' }, + { continentCountry: 'afcom', country: 'com', name: 'Comoros' }, + { continentCountry: 'afcpv', country: 'cpv', name: 'Cape Verde' }, + { continentCountry: 'afdji', country: 'dji', name: 'Djibouti' }, + { continentCountry: 'afdza', country: 'dza', name: 'Algeria' }, + { continentCountry: 'afegy', country: 'egy', name: 'Egypt' }, + { continentCountry: 'aferi', country: 'eri', name: 'Eritrea' }, + { continentCountry: 'afesh', country: 'esh', name: 'Western Sahara' }, + { continentCountry: 'afeth', country: 'eth', name: 'Ethiopia' }, + { continentCountry: 'afgab', country: 'gab', name: 'Gabon' }, + { continentCountry: 'afgha', country: 'gha', name: 'Ghana' }, + { continentCountry: 'afgin', country: 'gin', name: 'Guinea' }, + { continentCountry: 'afgmb', country: 'gmb', name: 'Gambia' }, + { continentCountry: 'afgnb', country: 'gnb', name: 'Guinea-Bissau' }, + { continentCountry: 'afgnq', country: 'gnq', name: 'Equatorial Guinea' }, + { continentCountry: 'afken', country: 'ken', name: 'Kenya' }, + { continentCountry: 'aflbr', country: 'lbr', name: 'Liberia' }, + { continentCountry: 'aflby', country: 'lby', name: 'Libya' }, + { continentCountry: 'aflso', country: 'lso', name: 'Lesotho' }, + { continentCountry: 'afmar', country: 'mar', name: 'Morocco' }, + { continentCountry: 'afmdg', country: 'mdg', name: 'Madagascar' }, + { continentCountry: 'afmli', country: 'mli', name: 'Mali' }, + { continentCountry: 'afmoz', country: 'moz', name: 'Mozambique' }, + { continentCountry: 'afmrt', country: 'mrt', name: 'Mauritania' }, + { continentCountry: 'afmus', country: 'mus', name: 'Mauritius' }, + { continentCountry: 'afmwi', country: 'mwi', name: 'Malawi' }, + { continentCountry: 'afmyt', country: 'myt', name: 'Mayotte' }, + { continentCountry: 'afnam', country: 'nam', name: 'Namibia' }, + { continentCountry: 'afner', country: 'ner', name: 'Niger' }, + { continentCountry: 'afnga', country: 'nga', name: 'Nigeria' }, + { continentCountry: 'afreu', country: 'reu', name: 'Reunion' }, + { continentCountry: 'afrwa', country: 'rwa', name: 'Rwanda' }, + { continentCountry: 'afsdn', country: 'sdn', name: 'Sudan' }, + { continentCountry: 'afsen', country: 'sen', name: 'Senegal' }, + { continentCountry: 'afshn', country: 'shn', name: 'Saint Helena' }, + { continentCountry: 'afsle', country: 'sle', name: 'Sierra Leone' }, + { continentCountry: 'afsom', country: 'som', name: 'Somalia' }, + { continentCountry: 'afssd', country: 'ssd', name: 'South Sudan' }, + { continentCountry: 'afstp', country: 'stp', name: 'Sao Tome and Principe' }, + { continentCountry: 'afswz', country: 'swz', name: 'Swaziland' }, + { continentCountry: 'afsyc', country: 'syc', name: 'Seychelles' }, + { continentCountry: 'aftcd', country: 'tcd', name: 'Chad' }, + { continentCountry: 'aftgo', country: 'tgo', name: 'Togo' }, + { continentCountry: 'aftun', country: 'tun', name: 'Tunisia' }, + { continentCountry: 'aftza', country: 'tza', name: 'Tanzania' }, + { continentCountry: 'afuga', country: 'uga', name: 'Uganda' }, + { continentCountry: 'afzaf', country: 'zaf', name: 'South Africa' }, + { continentCountry: 'afzmb', country: 'zmb', name: 'Zambia' }, + { continentCountry: 'afzwe', country: 'zwe', name: 'Zimbabwe' }, + { continentCountry: 'anata', country: 'ata', name: 'Antarctica' }, + { continentCountry: 'anatf', country: 'atf', name: 'French South Antarctic Territory' }, + { continentCountry: 'anbvt', country: 'bvt', name: 'Bouvet Island' }, + { continentCountry: 'anhmd', country: 'hmd', name: 'Heard Island and McDonald Islands' }, + { continentCountry: 'ansgs', country: 'sgs', name: 'South Georgia South Sandwich Islands' }, + { continentCountry: 'asafg', country: 'afg', name: 'Afghanistan' }, + { continentCountry: 'asare', country: 'are', name: 'United Arab Emirates' }, + { continentCountry: 'asarm', country: 'arm', name: 'Armenia' }, + { continentCountry: 'asaze', country: 'aze', name: 'Azerbaijan' }, + { continentCountry: 'asbgd', country: 'bgd', name: 'Bangladesh' }, + { continentCountry: 'asbhr', country: 'bhr', name: 'Bahrain' }, + { continentCountry: 'asbrn', country: 'brn', name: 'Brunei' }, + { continentCountry: 'asbtn', country: 'btn', name: 'Bhutan' }, + { continentCountry: 'ascck', country: 'cck', name: 'Cocos (Keeling) Islands' }, + { continentCountry: 'aschn', country: 'chn', name: 'China' }, + { continentCountry: 'ascxr', country: 'cxr', name: 'Christmas Island' }, + { continentCountry: 'ascyp', country: 'cyp', name: 'Cyprus' }, + { continentCountry: 'asgeo', country: 'geo', name: 'Georgia' }, + { continentCountry: 'ashkg', country: 'hkg', name: 'Hong Kong' }, + { continentCountry: 'asidn', country: 'idn', name: 'Indonesia' }, + { continentCountry: 'asind', country: 'ind', name: 'India' }, + { continentCountry: 'asiot', country: 'iot', name: 'British Indian Ocean Territory' }, + { continentCountry: 'asirn', country: 'irn', name: 'Iran' }, + { continentCountry: 'asirq', country: 'irq', name: 'Iraq' }, + { continentCountry: 'asisr', country: 'isr', name: 'Israel' }, + { continentCountry: 'asjor', country: 'jor', name: 'Jordan' }, + { continentCountry: 'asjpn', country: 'jpn', name: 'Japan' }, + { continentCountry: 'askaz', country: 'kaz', name: 'Kazakhstan' }, + { continentCountry: 'askgz', country: 'kgz', name: 'Kyrgyzstan' }, + { continentCountry: 'askhm', country: 'khm', name: 'Cambodia' }, + { continentCountry: 'askor', country: 'kor', name: 'South Korea' }, + { continentCountry: 'askwt', country: 'kwt', name: 'Kuwait' }, + { continentCountry: 'aslao', country: 'lao', name: 'Laos' }, + { continentCountry: 'aslbn', country: 'lbn', name: 'Lebanon' }, + { continentCountry: 'aslka', country: 'lka', name: 'Sri Lanka' }, + { continentCountry: 'asmac', country: 'mac', name: 'Macau' }, + { continentCountry: 'asmdv', country: 'mdv', name: 'Maldives' }, + { continentCountry: 'asmid', country: 'mid', name: 'Midway' }, + { continentCountry: 'asmmr', country: 'mmr', name: 'Burma' }, + { continentCountry: 'asmng', country: 'mng', name: 'Mongolia' }, + { continentCountry: 'asmys', country: 'mys', name: 'Malaysia' }, + { continentCountry: 'asnpl', country: 'npl', name: 'Nepal' }, + { continentCountry: 'asomn', country: 'omn', name: 'Oman' }, + { continentCountry: 'aspak', country: 'pak', name: 'Pakistan' }, + { continentCountry: 'asphl', country: 'phl', name: 'Philippines' }, + { continentCountry: 'asprk', country: 'prk', name: 'North Korea' }, + { continentCountry: 'aspse', country: 'pse', name: 'Palestine' }, + { continentCountry: 'asqat', country: 'qat', name: 'Qatar' }, + { continentCountry: 'assau', country: 'sau', name: 'Saudi Arabia' }, + { continentCountry: 'assgp', country: 'sgp', name: 'Singapore' }, + { continentCountry: 'assyr', country: 'syr', name: 'Syria' }, + { continentCountry: 'astha', country: 'tha', name: 'Thailand' }, + { continentCountry: 'astjk', country: 'tjk', name: 'Tajikistan' }, + { continentCountry: 'astkm', country: 'tkm', name: 'Turkmenistan' }, + { continentCountry: 'astls', country: 'tls', name: 'Timor-Leste' }, + { continentCountry: 'astur', country: 'tur', name: 'Turkey' }, + { continentCountry: 'astwn', country: 'twn', name: 'Taiwan' }, + { continentCountry: 'asuzb', country: 'uzb', name: 'Uzbekistan' }, + { continentCountry: 'asvnm', country: 'vnm', name: 'Vietnam' }, + { continentCountry: 'asyar', country: 'yar', name: 'Yemen Arab Republic' }, + { continentCountry: 'asyem', country: 'yem', name: 'Yemen' }, + { continentCountry: 'asymd', country: 'ymd', name: 'Democratic Yemen' }, + { continentCountry: 'eualb', country: 'alb', name: 'Albania' }, + { continentCountry: 'euand', country: 'and', name: 'Andorra' }, + { continentCountry: 'euaut', country: 'aut', name: 'Austria' }, + { continentCountry: 'eubel', country: 'bel', name: 'Belgium' }, + { continentCountry: 'eubgr', country: 'bgr', name: 'Bulgaria' }, + { continentCountry: 'eubih', country: 'bih', name: 'Bosnia and Herzegovina' }, + { continentCountry: 'eublr', country: 'blr', name: 'Belarus' }, + { continentCountry: 'eublx', country: 'blx', name: 'Belgium-Luxembourg' }, + { continentCountry: 'euche', country: 'che', name: 'Switzerland' }, + { continentCountry: 'euchi', country: 'chi', name: 'Channel Islands' }, + { continentCountry: 'eucsk', country: 'csk', name: 'Czechoslovakia' }, + { continentCountry: 'eucze', country: 'cze', name: 'Czech Republic' }, + { continentCountry: 'euddr', country: 'ddr', name: 'Democratic Republic of Germany' }, + { continentCountry: 'eudeu', country: 'deu', name: 'Germany' }, + { continentCountry: 'eudnk', country: 'dnk', name: 'Denmark' }, + { continentCountry: 'euesp', country: 'esp', name: 'Spain' }, + { continentCountry: 'euest', country: 'est', name: 'Estonia' }, + { continentCountry: 'eufdr', country: 'fdr', name: 'Federal Republic of Germany' }, + { continentCountry: 'eufin', country: 'fin', name: 'Finland' }, + { continentCountry: 'eufra', country: 'fra', name: 'France' }, + { continentCountry: 'eufro', country: 'fro', name: 'Faroe Islands' }, + { continentCountry: 'eugbr', country: 'gbr', name: 'United Kingdom' }, + { continentCountry: 'eugib', country: 'gib', name: 'Gibraltar' }, + { continentCountry: 'eugrc', country: 'grc', name: 'Greece' }, + { continentCountry: 'euhrv', country: 'hrv', name: 'Croatia' }, + { continentCountry: 'euhun', country: 'hun', name: 'Hungary' }, + { continentCountry: 'euimn', country: 'imn', name: 'Isle of Man' }, + { continentCountry: 'euirl', country: 'irl', name: 'Ireland' }, + { continentCountry: 'euisl', country: 'isl', name: 'Iceland' }, + { continentCountry: 'euita', country: 'ita', name: 'Italy' }, + { continentCountry: 'euksv', country: 'ksv', name: 'Kosovo' }, + { continentCountry: 'eulie', country: 'lie', name: 'Liechtenstein' }, + { continentCountry: 'eultu', country: 'ltu', name: 'Lithuania' }, + { continentCountry: 'eulux', country: 'lux', name: 'Luxembourg' }, + { continentCountry: 'eulva', country: 'lva', name: 'Latvia' }, + { continentCountry: 'eumco', country: 'mco', name: 'Monaco' }, + { continentCountry: 'eumda', country: 'mda', name: 'Moldova' }, + { continentCountry: 'eumkd', country: 'mkd', name: 'Macedonia' }, + { continentCountry: 'eumlt', country: 'mlt', name: 'Malta' }, + { continentCountry: 'eumne', country: 'mne', name: 'Montenegro' }, + { continentCountry: 'eunld', country: 'nld', name: 'Netherlands' }, + { continentCountry: 'eunor', country: 'nor', name: 'Norway' }, + { continentCountry: 'eupol', country: 'pol', name: 'Poland' }, + { continentCountry: 'euprt', country: 'prt', name: 'Portugal' }, + { continentCountry: 'eurou', country: 'rou', name: 'Romania' }, + { continentCountry: 'eurus', country: 'rus', name: 'Russia' }, + { continentCountry: 'euscg', country: 'scg', name: 'Serbia and Montenegro' }, + { continentCountry: 'eusjm', country: 'sjm', name: 'Svalbard' }, + { continentCountry: 'eusmr', country: 'smr', name: 'San Marino' }, + { continentCountry: 'eusrb', country: 'srb', name: 'Serbia' }, + { continentCountry: 'eusun', country: 'sun', name: 'USSR' }, + { continentCountry: 'eusvk', country: 'svk', name: 'Slovakia' }, + { continentCountry: 'eusvn', country: 'svn', name: 'Slovenia' }, + { continentCountry: 'euswe', country: 'swe', name: 'Sweden' }, + { continentCountry: 'euukr', country: 'ukr', name: 'Ukraine' }, + { continentCountry: 'euvat', country: 'vat', name: 'Holy See (Vatican City)' }, + { continentCountry: 'euyug', country: 'yug', name: 'Yugoslavia' }, + { continentCountry: 'naabw', country: 'abw', name: 'Aruba' }, + { continentCountry: 'naaia', country: 'aia', name: 'Anguilla' }, + { continentCountry: 'naant', country: 'ant', name: 'Netherlands Antilles' }, + { continentCountry: 'naatg', country: 'atg', name: 'Antigua and Barbuda' }, + { continentCountry: 'nabes', country: 'bes', name: 'Bonaire' }, + { continentCountry: 'nabhs', country: 'bhs', name: 'Bahamas' }, + { continentCountry: 'nablm', country: 'blm', name: 'Saint Barthélemy' }, + { continentCountry: 'nablz', country: 'blz', name: 'Belize' }, + { continentCountry: 'nabmu', country: 'bmu', name: 'Bermuda' }, + { continentCountry: 'nabrb', country: 'brb', name: 'Barbados' }, + { continentCountry: 'nacan', country: 'can', name: 'Canada' }, + { continentCountry: 'nacri', country: 'cri', name: 'Costa Rica' }, + { continentCountry: 'nacub', country: 'cub', name: 'Cuba' }, + { continentCountry: 'nacuw', country: 'cuw', name: 'Curaçao' }, + { continentCountry: 'nacym', country: 'cym', name: 'Cayman Islands' }, + { continentCountry: 'nadma', country: 'dma', name: 'Dominica' }, + { continentCountry: 'nadom', country: 'dom', name: 'Dominican Republic' }, + { continentCountry: 'nagrd', country: 'grd', name: 'Grenada' }, + { continentCountry: 'nagrl', country: 'grl', name: 'Greenland' }, + { continentCountry: 'nagtm', country: 'gtm', name: 'Guatemala' }, + { continentCountry: 'nahnd', country: 'hnd', name: 'Honduras' }, + { continentCountry: 'nahti', country: 'hti', name: 'Haiti' }, + { continentCountry: 'najam', country: 'jam', name: 'Jamaica' }, + { continentCountry: 'nakna', country: 'kna', name: 'Saint Kitts and Nevis' }, + { continentCountry: 'nalca', country: 'lca', name: 'Saint Lucia' }, + { continentCountry: 'namaf', country: 'maf', name: 'Saint Maarten' }, + { continentCountry: 'namex', country: 'mex', name: 'Mexico' }, + { continentCountry: 'namsr', country: 'msr', name: 'Montserrat' }, + { continentCountry: 'namtq', country: 'mtq', name: 'Martinique' }, + { continentCountry: 'nanaa', country: 'naa', name: 'Netherland Antilles and Aruba' }, + { continentCountry: 'nanic', country: 'nic', name: 'Nicaragua' }, + { continentCountry: 'napan', country: 'pan', name: 'Panama' }, + { continentCountry: 'napci', country: 'pci', name: 'Pacific Island (US)' }, + { continentCountry: 'napcz', country: 'pcz', name: 'Panama Canal Zone' }, + { continentCountry: 'napri', country: 'pri', name: 'Puerto Rico' }, + { continentCountry: 'naslv', country: 'slv', name: 'El Salvador' }, + { continentCountry: 'naspm', country: 'spm', name: 'Saint Pierre and Miquelon' }, + { continentCountry: 'natca', country: 'tca', name: 'Turks and Caicos Islands' }, + { continentCountry: 'natto', country: 'tto', name: 'Trinidad and Tobago' }, + { continentCountry: 'naumi', country: 'umi', name: 'United States Minor Outlying Islands' }, + { continentCountry: 'nausa', country: 'usa', name: 'United States' }, + { continentCountry: 'navct', country: 'vct', name: 'Saint Vincent and the Grenadines' }, + { continentCountry: 'navgb', country: 'vgb', name: 'British Virgin Islands' }, + { continentCountry: 'navir', country: 'vir', name: 'Virgin Islands' }, + { continentCountry: 'ocasm', country: 'asm', name: 'American Samoa' }, + { continentCountry: 'ocaus', country: 'aus', name: 'Australia' }, + { continentCountry: 'occok', country: 'cok', name: 'Cook Islands' }, + { continentCountry: 'ocfji', country: 'fji', name: 'Fiji' }, + { continentCountry: 'ocfsm', country: 'fsm', name: 'Micronesia' }, + { continentCountry: 'ocglp', country: 'glp', name: 'Guadeloupe' }, + { continentCountry: 'ocgum', country: 'gum', name: 'Guam' }, + { continentCountry: 'ockir', country: 'kir', name: 'Kiribati' }, + { continentCountry: 'ocmhl', country: 'mhl', name: 'Marshall Islands' }, + { continentCountry: 'ocmnp', country: 'mnp', name: 'Northern Mariana Islands' }, + { continentCountry: 'ocncl', country: 'ncl', name: 'New Caledonia' }, + { continentCountry: 'ocnfk', country: 'nfk', name: 'Norfolk Island' }, + { continentCountry: 'ocniu', country: 'niu', name: 'Niue' }, + { continentCountry: 'ocnru', country: 'nru', name: 'Nauru' }, + { continentCountry: 'ocnzl', country: 'nzl', name: 'New Zealand' }, + { continentCountry: 'ocpcn', country: 'pcn', name: 'Pitcairn Islands' }, + { continentCountry: 'ocplw', country: 'plw', name: 'Palau' }, + { continentCountry: 'ocpng', country: 'png', name: 'Papua New Guinea' }, + { continentCountry: 'ocpyf', country: 'pyf', name: 'French Polynesia' }, + { continentCountry: 'ocslb', country: 'slb', name: 'Solomon Islands' }, + { continentCountry: 'octkl', country: 'tkl', name: 'Tokelau' }, + { continentCountry: 'octon', country: 'ton', name: 'Tonga' }, + { continentCountry: 'octuv', country: 'tuv', name: 'Tuvalu' }, + { continentCountry: 'ocvut', country: 'vut', name: 'Vanuatu' }, + { continentCountry: 'ocwlf', country: 'wlf', name: 'Wallis and Futuna' }, + { continentCountry: 'ocwsm', country: 'wsm', name: 'Samoa' }, + { continentCountry: 'saarg', country: 'arg', name: 'Argentina' }, + { continentCountry: 'sabol', country: 'bol', name: 'Bolivia' }, + { continentCountry: 'sabra', country: 'bra', name: 'Brazil' }, + { continentCountry: 'sachl', country: 'chl', name: 'Chile' }, + { continentCountry: 'sacol', country: 'col', name: 'Colombia' }, + { continentCountry: 'saecu', country: 'ecu', name: 'Ecuador' }, + { continentCountry: 'saflk', country: 'flk', name: 'Falkland Islands' }, + { continentCountry: 'saguf', country: 'guf', name: 'French Guiana' }, + { continentCountry: 'saguy', country: 'guy', name: 'Guyana' }, + { continentCountry: 'saper', country: 'per', name: 'Peru' }, + { continentCountry: 'sapry', country: 'pry', name: 'Paraguay' }, + { continentCountry: 'sasur', country: 'sur', name: 'Suriname' }, + { continentCountry: 'saury', country: 'ury', name: 'Uruguay' }, + { continentCountry: 'saven', country: 'ven', name: 'Venezuela' }, + { continentCountry: 'xxwld', country: 'wld', name: 'World' }, + { continentCountry: 'xxxxh', country: 'xxa', name: 'Areas' }, +]; diff --git a/packages/osd-charts/src/mocks/hierarchical/index.ts b/packages/osd-charts/src/mocks/hierarchical/index.ts new file mode 100644 index 000000000000..ddfcab01b943 --- /dev/null +++ b/packages/osd-charts/src/mocks/hierarchical/index.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { manyPieMock } from './many_pie'; +import { miniSunburstMock } from './mini_sunburst'; +import { observabilityTreeMock } from './observability_tree'; +import { pieMock } from './pie'; +import { sunburstMock } from './sunburst'; + +/** @internal */ +export const mocks = { + pie: pieMock, + sunburst: sunburstMock, + miniSunburst: miniSunburstMock, + manyPie: manyPieMock, + observabilityTree: observabilityTreeMock, +}; diff --git a/packages/osd-charts/src/mocks/hierarchical/many_pie.ts b/packages/osd-charts/src/mocks/hierarchical/many_pie.ts new file mode 100644 index 000000000000..8fd3db3cb226 --- /dev/null +++ b/packages/osd-charts/src/mocks/hierarchical/many_pie.ts @@ -0,0 +1,252 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const manyPieMock = [ + { origin: 'chn', exportVal: 1680027842644 }, + { origin: 'usa', exportVal: 1102566931395 }, + { origin: 'deu', exportVal: 1062556067025 }, + { origin: 'jpn', exportVal: 682060607712 }, + { origin: 'fra', exportVal: 486717519772 }, + { origin: 'kor', exportVal: 429341500570 }, + { origin: 'ita', exportVal: 388082452213 }, + { origin: 'nld', exportVal: 374451602037 }, + { origin: 'can', exportVal: 362046595060 }, + { origin: 'rus', exportVal: 360435750136 }, + { origin: 'gbr', exportVal: 347628222830 }, + { origin: 'mex', exportVal: 279440490401 }, + { origin: 'bel', exportVal: 276331475549 }, + { origin: 'mys', exportVal: 221211941430 }, + { origin: 'esp', exportVal: 220725696243 }, + { origin: 'che', exportVal: 217916829452 }, + { origin: 'ind', exportVal: 211138300089 }, + { origin: 'sau', exportVal: 207321414541 }, + { origin: 'sgp', exportVal: 206513400598 }, + { origin: 'bra', exportVal: 201273933253 }, + { origin: 'aus', exportVal: 193277122627 }, + { origin: 'tha', exportVal: 186564165591 }, + { origin: 'idn', exportVal: 158159867052 }, + { origin: 'are', exportVal: 155039720778 }, + { origin: 'irl', exportVal: 135561950237 }, + { origin: 'swe', exportVal: 132241085429 }, + { origin: 'pol', exportVal: 130310102801 }, + { origin: 'aut', exportVal: 126493546692 }, + { origin: 'nor', exportVal: 123085657181 }, + { origin: 'cze', exportVal: 109138761423 }, + { origin: 'tur', exportVal: 107919189508 }, + { origin: 'zaf', exportVal: 86865327178 }, + { origin: 'dnk', exportVal: 80952198584 }, + { origin: 'hun', exportVal: 79687476997 }, + { origin: 'irn', exportVal: 77200364074 }, + { origin: 'hkg', exportVal: 73164278476 }, + { origin: 'nga', exportVal: 71428776813 }, + { origin: 'vnm', exportVal: 69896077996 }, + { origin: 'arg', exportVal: 65108843884 }, + { origin: 'phl', exportVal: 64380113683 }, + { origin: 'chl', exportVal: 62968068992 }, + { origin: 'fin', exportVal: 62202122673 }, + { origin: 'qat', exportVal: 59326138499 }, + { origin: 'ven', exportVal: 55500881984 }, + { origin: 'kwt', exportVal: 55400630374 }, + { origin: 'ukr', exportVal: 54278860343 }, + { origin: 'svk', exportVal: 53482619716 }, + { origin: 'dza', exportVal: 53192494262 }, + { origin: 'isr', exportVal: 50265596760 }, + { origin: 'lby', exportVal: 45235556598 }, + { origin: 'ago', exportVal: 45010895723 }, + { origin: 'irq', exportVal: 44615332292 }, + { origin: 'rou', exportVal: 44508446100 }, + { origin: 'prt', exportVal: 43367388493 }, + { origin: 'kaz', exportVal: 42068475259 }, + { origin: 'col', exportVal: 38322220284 }, + { origin: 'per', exportVal: 33873822574 }, + { origin: 'egy', exportVal: 28814207699 }, + { origin: 'nzl', exportVal: 28397440169 }, + { origin: 'omn', exportVal: 27058636294 }, + { origin: 'cri', exportVal: 23448846570 }, + { origin: 'grc', exportVal: 22066497483 }, + { origin: 'pak', exportVal: 21668594115 }, + { origin: 'svn', exportVal: 21426115576 }, + { origin: 'aze', exportVal: 20047358465 }, + { origin: 'bgr', exportVal: 18841613257 }, + { origin: 'bgd', exportVal: 18130589863 }, + { origin: 'ecu', exportVal: 16980863677 }, + { origin: 'lux', exportVal: 16478009245 }, + { origin: 'mar', exportVal: 16237430066 }, + { origin: 'tun', exportVal: 16031014518 }, + { origin: 'ltu', exportVal: 15737237575 }, + { origin: 'tto', exportVal: 12810076639 }, + { origin: 'hrv', exportVal: 10916770107 }, + { origin: 'est', exportVal: 10354731182 }, + { origin: 'sdn', exportVal: 9800479528 }, + { origin: 'srb', exportVal: 9029787907 }, + { origin: 'cog', exportVal: 8949393562 }, + { origin: 'blr', exportVal: 8929749700 }, + { origin: 'lva', exportVal: 8555460871 }, + { origin: 'civ', exportVal: 8073460547 }, + { origin: 'brn', exportVal: 7925150288 }, + { origin: 'gnq', exportVal: 7854023406 }, + { origin: 'syr', exportVal: 7754782660 }, + { origin: 'bhr', exportVal: 7529173128 }, + { origin: 'lka', exportVal: 7339588373 }, + { origin: 'ury', exportVal: 7165504406 }, + { origin: 'yem', exportVal: 7158061799 }, + { origin: 'gtm', exportVal: 7003757867 }, + { origin: 'hnd', exportVal: 6030839002 }, + { origin: 'pan', exportVal: 5930135538 }, + { origin: 'mmr', exportVal: 5831563317 }, + { origin: 'png', exportVal: 5769279559 }, + { origin: 'mlt', exportVal: 5510882139 }, + { origin: 'gab', exportVal: 5465186525 }, + { origin: 'dom', exportVal: 5436778260 }, + { origin: 'bol', exportVal: 5419211637 }, + { origin: 'zmb', exportVal: 5131623093 }, + { origin: 'khm', exportVal: 5112204572 }, + { origin: 'cod', exportVal: 5076397761 }, + { origin: 'uzb', exportVal: 4895581648 }, + { origin: 'slv', exportVal: 4452895199 }, + { origin: 'jor', exportVal: 4375537052 }, + { origin: 'pry', exportVal: 4164535906 }, + { origin: 'bih', exportVal: 4154602570 }, + { origin: 'gha', exportVal: 4052850512 }, + { origin: 'isl', exportVal: 3998498539 }, + { origin: 'ken', exportVal: 3745974291 }, + { origin: 'cmr', exportVal: 3744461941 }, + { origin: 'moz', exportVal: 3235381310 }, + { origin: 'cyp', exportVal: 3105446728 }, + { origin: 'mkd', exportVal: 3003234599 }, + { origin: 'nic', exportVal: 2968741221 }, + { origin: 'tcd', exportVal: 2803301047 }, + { origin: 'mng', exportVal: 2785683128 }, + { origin: 'bhs', exportVal: 2702012957 }, + { origin: 'nam', exportVal: 2582063735 }, + { origin: 'bwa', exportVal: 2484287290 }, + { origin: 'tza', exportVal: 2467509196 }, + { origin: 'cub', exportVal: 2404595120 }, + { origin: 'ant', exportVal: 2402558108 }, + { origin: 'atg', exportVal: 2348417348 }, + { origin: 'lbn', exportVal: 2341813110 }, + { origin: 'tkm', exportVal: 2267612447 }, + { origin: 'geo', exportVal: 1926999336 }, + { origin: 'gin', exportVal: 1891629964 }, + { origin: 'mrt', exportVal: 1886427106 }, + { origin: 'mus', exportVal: 1853024506 }, + { origin: 'prk', exportVal: 1840466387 }, + { origin: 'lao', exportVal: 1813066982 }, + { origin: 'sur', exportVal: 1670933603 }, + { origin: 'mda', exportVal: 1535895371 }, + { origin: 'eth', exportVal: 1485000925 }, + { origin: 'alb', exportVal: 1422153482 }, + { origin: 'sen', exportVal: 1354504280 }, + { origin: 'zwe', exportVal: 1267893552 }, + { origin: 'ncl', exportVal: 1116638208 }, + { origin: 'kgz', exportVal: 1098720987 }, + { origin: 'mdg', exportVal: 1076708874 }, + { origin: 'brb', exportVal: 980638268 }, + { origin: 'cym', exportVal: 974251949 }, + { origin: 'tjk', exportVal: 958314400 }, + { origin: 'guy', exportVal: 954432679 }, + { origin: 'jam', exportVal: 943069430 }, + { origin: 'uga', exportVal: 935275666 }, + { origin: 'mwi', exportVal: 932077166 }, + { origin: 'mac', exportVal: 887740944 }, + { origin: 'tgo', exportVal: 838487021 }, + { origin: 'lbr', exportVal: 801935957 }, + { origin: 'swz', exportVal: 785406576 }, + { origin: 'ben', exportVal: 725226637 }, + { origin: 'arm', exportVal: 718132350 }, + { origin: 'npl', exportVal: 714792917 }, + { origin: 'fro', exportVal: 711844362 }, + { origin: 'bmu', exportVal: 705643821 }, + { origin: 'hti', exportVal: 617209639 }, + { origin: 'mli', exportVal: 604706423 }, + { origin: 'fji', exportVal: 602978562 }, + { origin: 'grl', exportVal: 582480590 }, + { origin: 'afg', exportVal: 567904408 }, + { origin: 'bfa', exportVal: 525591271 }, + { origin: 'lso', exportVal: 484570865 }, + { origin: 'vut', exportVal: 435685569 }, + { origin: 'mne', exportVal: 420770610 }, + { origin: 'vgb', exportVal: 401903288 }, + { origin: 'mhl', exportVal: 398834621 }, + { origin: 'slb', exportVal: 367668498 }, + { origin: 'blz', exportVal: 360301876 }, + { origin: 'syc', exportVal: 346263287 }, + { origin: 'sle', exportVal: 324356717 }, + { origin: 'ner', exportVal: 296079291 }, + { origin: 'som', exportVal: 285486366 }, + { origin: 'gib', exportVal: 280334935 }, + { origin: 'niu', exportVal: 261210980 }, + { origin: 'and', exportVal: 234285648 }, + { origin: 'abw', exportVal: 196774818 }, + { origin: 'btn', exportVal: 189669110 }, + { origin: 'pyf', exportVal: 178629453 }, + { origin: 'rwa', exportVal: 174428177 }, + { origin: 'smr', exportVal: 159388050 }, + { origin: 'flk', exportVal: 159227983 }, + { origin: 'gnb', exportVal: 139648187 }, + { origin: 'vct', exportVal: 128049639 }, + { origin: 'mdv', exportVal: 125878962 }, + { origin: 'caf', exportVal: 118997237 }, + { origin: 'bvt', exportVal: 113300883 }, + { origin: 'pse', exportVal: 106543441 }, + { origin: 'dji', exportVal: 106353077 }, + { origin: 'bdi', exportVal: 71571028 }, + { origin: 'kna', exportVal: 69543245 }, + { origin: 'wsm', exportVal: 69125482 }, + { origin: 'tls', exportVal: 62816488 }, + { origin: 'lca', exportVal: 62118974 }, + { origin: 'gmb', exportVal: 61496143 }, + { origin: 'ata', exportVal: 54524545 }, + { origin: 'fsm', exportVal: 51668670 }, + { origin: 'nru', exportVal: 50706656 }, + { origin: 'cpv', exportVal: 50405460 }, + { origin: 'dma', exportVal: 49759430 }, + { origin: 'umi', exportVal: 36534854 }, + { origin: 'com', exportVal: 32497602 }, + { origin: 'asm', exportVal: 30381132 }, + { origin: 'gum', exportVal: 27562111 }, + { origin: 'cok', exportVal: 22419977 }, + { origin: 'kir', exportVal: 20578810 }, + { origin: 'shn', exportVal: 19041382 }, + { origin: 'grd', exportVal: 17998604 }, + { origin: 'tca', exportVal: 17650505 }, + { origin: 'eri', exportVal: 16165269 }, + { origin: 'tkl', exportVal: 15171571 }, + { origin: 'cxr', exportVal: 14415365 }, + { origin: 'plw', exportVal: 14061574 }, + { origin: 'tuv', exportVal: 13677929 }, + { origin: 'stp', exportVal: 12435538 }, + { origin: 'ton', exportVal: 10576037 }, + { origin: 'aia', exportVal: 10374477 }, + { origin: 'cck', exportVal: 8621289 }, + { origin: 'myt', exportVal: 7502485 }, + { origin: 'sgs', exportVal: 6000762 }, + { origin: 'spm', exportVal: 5045811 }, + { origin: 'vat', exportVal: 4840493 }, + { origin: 'msr', exportVal: 4343283 }, + { origin: 'iot', exportVal: 3375434 }, + { origin: 'atf', exportVal: 3247132 }, + { origin: 'nfk', exportVal: 2788736 }, + { origin: 'mnp', exportVal: 2435135 }, + { origin: 'pcn', exportVal: 1748735 }, + { origin: 'wlf', exportVal: 1337566 }, + { origin: 'esh', exportVal: 960601 }, + { origin: 'hmd', exportVal: 154369 }, +]; diff --git a/packages/osd-charts/src/mocks/hierarchical/mini_sunburst.ts b/packages/osd-charts/src/mocks/hierarchical/mini_sunburst.ts new file mode 100644 index 000000000000..85ea8bdcc5fc --- /dev/null +++ b/packages/osd-charts/src/mocks/hierarchical/mini_sunburst.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const miniSunburstMock = [ + { sitc1: '7', dest: 'usa', exportVal: 553359100104 }, + { sitc1: '7', dest: 'chn', exportVal: 392617281424 }, + { sitc1: '3', dest: 'usa', exportVal: 324856796136 }, + { sitc1: '7', dest: 'deu', exportVal: 253250650864 }, + { sitc1: '8', dest: 'usa', exportVal: 226628559432 }, + { sitc1: '7', dest: 'hkg', exportVal: 177490158520 }, + { sitc1: '3', dest: 'jpn', exportVal: 177421375512 }, + { sitc1: '2', dest: 'chn', exportVal: 173840557624 }, + { sitc1: '3', dest: 'chn', exportVal: 167989572088 }, + { sitc1: '7', dest: 'fra', exportVal: 135443006088 }, + { sitc1: '6', dest: 'usa', exportVal: 129879187528 }, + { sitc1: '5', dest: 'usa', exportVal: 127516647672 }, + { sitc1: '7', dest: 'can', exportVal: 118114362152 }, + { sitc1: '7', dest: 'gbr', exportVal: 117139481536 }, + { sitc1: '3', dest: 'kor', exportVal: 108348223232 }, + { sitc1: '7', dest: 'jpn', exportVal: 106979336520 }, +]; diff --git a/packages/osd-charts/src/mocks/hierarchical/observability_tree.ts b/packages/osd-charts/src/mocks/hierarchical/observability_tree.ts new file mode 100644 index 000000000000..e9649e31eda0 --- /dev/null +++ b/packages/osd-charts/src/mocks/hierarchical/observability_tree.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// source of data: Martin Spier's https://github.com/spiermar/d3-flame-graph + +// prettier-ignore + +/** @internal */ +export const observabilityTreeMock = {c:[{n:'genunix`syscall_mstate',v:89},{c:[{c:[{c:[{c:[{c:[{c:[{c:[{c:[{n:'unix`page_lookup_create',v:1}],n:'unix`page_lookup',v:1}],n:'ufs`ufs_getpage',v:1}],n:'genunix`fop_getpage',v:1},{c:[{c:[{c:[{c:[{c:[{n:'genunix`pvn_plist_init',v:1},{n:'unix`lgrp_mem_choose',v:1},{c:[{c:[{c:[{n:'unix`mutex_enter',v:1}],n:'unix`page_get_mnode_freelist',v:1}],n:'unix`page_get_freelist',v:1}],n:'unix`page_create_va',v:1},{c:[{n:'unix`page_lookup_create',v:1}],n:'unix`page_lookup',v:1}],n:'genunix`swap_getapage',v:4}],n:'genunix`swap_getpage',v:4}],n:'genunix`fop_getpage',v:4},{c:[{c:[{n:'unix`hwblkclr',v:3}],n:'unix`pfnzero',v:3}],n:'unix`pagezero',v:3}],n:'genunix`anon_zero',v:7}],n:'genunix`segvn_faultpage',v:7},{n:'ufs`ufs_getpage',v:1},{c:[{c:[{c:[{c:[{c:[{c:[{c:[{c:[{n:'unix`hment_compare',v:1}],n:'genunix`avl_find',v:1}],n:'genunix`avl_add',v:1}],n:'unix`hment_insert',v:2}],n:'unix`hment_assign',v:2}],n:'unix`hati_pte_map',v:2}],n:'unix`hati_load_common',v:2}],n:'unix`hat_memload',v:2}],n:'unix`hat_memload_region',v:2}],n:'genunix`segvn_fault',v:11}],n:'genunix`as_fault',v:12},{n:'genunix`segvn_fault',v:1}],n:'unix`pagefault',v:13}],n:'unix`trap',v:13}],n:'unix`0xfffffffffb8001d6',v:13},{n:'unix`0xfffffffffb800c7c',v:42},{n:'unix`0xfffffffffb800c81',v:2},{c:[{n:'genunix`gethrtime_unscaled',v:4},{c:[{c:[{n:'unix`tsc_gethrtimeunscaled',v:11},{n:'unix`tsc_read',v:186}],n:'genunix`gethrtime_unscaled',v:203},{n:'unix`tsc_gethrtimeunscaled',v:13}],n:'genunix`syscall_mstate',v:355},{n:'unix`atomic_add_64',v:110}],n:'unix`0xfffffffffb800c86',v:472},{c:[{n:'genunix`audit_getstate',v:27},{n:'genunix`clear_stale_fd',v:10},{n:'genunix`disp_lock_exit',v:27},{c:[{n:'FSS`fss_preempt',v:1},{n:'genunix`audit_getstate',v:15},{n:'genunix`clear_stale_fd',v:44},{c:[{n:'unix`clear_int_flag',v:39},{n:'unix`do_splx',v:1993},{c:[{c:[{c:[{n:'unix`do_splx',v:1}],n:'genunix`disp_lock_exit_nopreempt',v:1}],n:'unix`preempt',v:1}],n:'unix`kpreempt',v:1}],n:'genunix`disp_lock_exit',v:2096},{n:'genunix`sigcheck',v:1},{c:[{n:'unix`clear_int_flag',v:180},{n:'unix`splr',v:400}],n:'genunix`thread_lock',v:670},{n:'unix`do_splx',v:31},{n:'unix`i_ddi_splhigh',v:23},{n:'unix`lock_clear_splx',v:28},{n:'unix`lock_try',v:778},{n:'unix`lwp_getdatamodel',v:6},{c:[{c:[{c:[{c:[{c:[{n:'unix`tsc_gethrtimeunscaled',v:1}],n:'genunix`mstate_thread_onproc_time',v:1}],n:'unix`caps_charge_adjust',v:1}],n:'unix`cpucaps_charge',v:3},{c:[{n:'unix`cmt_balance',v:1},{c:[{n:'unix`bitset_in_set',v:1}],n:'unix`cpu_wakeup_mwait',v:1}],n:'unix`setbackdq',v:5}],n:'FSS`fss_preempt',v:8},{n:'unix`do_splx',v:1},{c:[{n:'genunix`disp_lock_exit_high',v:1},{c:[{n:'unix`membar_enter',v:1}],n:'unix`disp',v:1},{n:'unix`do_splx',v:1},{c:[{c:[{n:'genunix`schedctl_save',v:1}],n:'genunix`savectx',v:2}],n:'unix`resume',v:2}],n:'unix`swtch',v:5}],n:'unix`preempt',v:14},{n:'unix`prunstop',v:36},{n:'unix`splr',v:92},{n:'unix`splx',v:6}],n:'genunix`post_syscall',v:4245},{n:'genunix`thread_lock',v:33},{n:'unix`lwp_getdatamodel',v:3},{n:'unix`prunstop',v:2}],n:'unix`0xfffffffffb800c91',v:4361},{c:[{n:'genunix`gethrtime_unscaled',v:7},{c:[{c:[{n:'unix`tsc_gethrtimeunscaled',v:17},{n:'unix`tsc_read',v:160}],n:'genunix`gethrtime_unscaled',v:182},{n:'unix`tsc_gethrtimeunscaled',v:12}],n:'genunix`syscall_mstate',v:412},{n:'unix`atomic_add_64',v:95}],n:'unix`0xfffffffffb800ca0',v:517},{n:'unix`_sys_rtt',v:6},{c:[{c:[{c:[{c:[{c:[{c:[{n:'genunix`cpu_decay',v:1}],n:'genunix`cpu_grow',v:1}],n:'genunix`cpu_update_pct',v:1}],n:'genunix`new_mstate',v:1}],n:'unix`trap',v:1}],n:'unix`sys_rtt_common',v:1}],n:'unix`_sys_rtt_ints_disabled',v:1},{c:[{c:[{c:[{c:[{c:[{c:[{c:[{n:'doorfs`door_close',v:1}],n:'namefs`nm_close',v:1}],n:'genunix`fop_close',v:1}],n:'genunix`closef',v:1}],n:'genunix`close_exec',v:1}],n:'genunix`exec_common',v:1}],n:'genunix`exece',v:1}],n:'unix`_sys_sysenter_post_swapgs',v:1},{c:[{n:'genunix`gethrtime_unscaled',v:11},{c:[{c:[{c:[{c:[{c:[{c:[{c:[{c:[{c:[{c:[{n:'unix`mtype_func',v:1},{n:'unix`mutex_enter',v:1}],n:'unix`page_get_mnode_freelist',v:2}],n:'unix`page_get_freelist',v:2}],n:'unix`page_create_va',v:3}],n:'genunix`pvn_read_kluster',v:3}],n:'ufs`ufs_getpage_ra',v:3}],n:'ufs`ufs_getpage',v:3}],n:'genunix`fop_getpage',v:3}],n:'genunix`segvn_faulta',v:3}],n:'genunix`as_faulta',v:3}],n:'genunix`memcntl',v:3},{c:[{c:[{c:[{c:[{c:[{c:[{c:[{c:[{n:'unix`htable_lookup',v:1}],n:'unix`htable_walk',v:1}],n:'unix`hat_unload_callback',v:1}],n:'genunix`segvn_unmap',v:1}],n:'genunix`as_unmap',v:1}],n:'unix`mmapobj_map_elf',v:1}],n:'unix`mmapobj_map_interpret',v:1}],n:'unix`mmapobj',v:1}],n:'genunix`mmapobjsys',v:1},{c:[{n:'genunix`copen',v:7},{c:[{n:'genunix`audit_getstate',v:62},{c:[{n:'genunix`audit_falloc',v:8},{c:[{c:[{c:[{c:[{c:[{n:'unix`swtch',v:1}],n:'unix`preempt',v:1}],n:'unix`kpreempt',v:1}],n:'unix`sys_rtt_common',v:1}],n:'unix`_sys_rtt_ints_disabled',v:1}],n:'genunix`audit_getstate',v:66},{n:'genunix`audit_unfalloc',v:32},{n:'genunix`crfree',v:9},{n:'genunix`crhold',v:5},{n:'genunix`cv_broadcast',v:16},{c:[{c:[{n:'genunix`kmem_cache_alloc',v:11},{c:[{n:'genunix`kmem_cache_alloc',v:66},{n:'unix`mutex_enter',v:122},{n:'unix`mutex_exit',v:46}],n:'genunix`kmem_zalloc',v:280},{n:'unix`bzero',v:8}],n:'genunix`audit_falloc',v:313},{n:'genunix`crhold',v:11},{n:'genunix`kmem_cache_alloc',v:49},{n:'genunix`kmem_zalloc',v:13},{c:[{n:'genunix`fd_find',v:13},{n:'genunix`fd_reserve',v:9},{c:[{n:'genunix`fd_find',v:161},{n:'genunix`fd_reserve',v:15}],n:'genunix`ufalloc_file',v:294},{n:'unix`mutex_enter',v:197},{n:'unix`mutex_exit',v:29}],n:'genunix`ufalloc',v:551},{n:'genunix`ufalloc_file',v:20},{n:'unix`atomic_add_32',v:134},{n:'unix`mutex_enter',v:99},{n:'unix`mutex_exit',v:58}],n:'genunix`falloc',v:1363},{n:'genunix`fd_reserve',v:8},{n:'genunix`kmem_cache_alloc',v:9},{n:'genunix`kmem_cache_free',v:5},{n:'genunix`lookupnameat',v:69},{n:'genunix`set_errno',v:24},{c:[{n:'genunix`audit_getstate',v:31},{n:'genunix`cv_broadcast',v:25},{n:'genunix`fd_reserve',v:35}],n:'genunix`setf',v:187},{n:'genunix`ufalloc',v:10},{c:[{c:[{n:'genunix`kmem_cache_free',v:5},{c:[{n:'genunix`kmem_cache_free',v:73},{n:'unix`mutex_enter',v:111},{n:'unix`mutex_exit',v:55}],n:'genunix`kmem_free',v:288}],n:'genunix`audit_unfalloc',v:340},{n:'genunix`crfree',v:13},{n:'genunix`kmem_cache_free',v:51},{n:'genunix`kmem_free',v:11},{n:'unix`atomic_add_32_nv',v:100},{n:'unix`mutex_enter',v:97},{n:'unix`mutex_exit',v:56}],n:'genunix`unfalloc',v:729},{c:[{c:[{c:[{c:[{n:'genunix`audit_getstate',v:16},{n:'genunix`fop_lookup',v:55},{c:[{n:'genunix`audit_getstate',v:21},{n:'genunix`crgetmapped',v:55},{n:'genunix`fop_inactive',v:39},{c:[{n:'genunix`crgetmapped',v:57},{n:'genunix`dnlc_lookup',v:26},{n:'genunix`fop_lookup',v:85},{n:'genunix`kmem_alloc',v:73},{n:'genunix`traverse',v:30},{n:'genunix`vfs_matchops',v:28},{c:[{c:[{n:'genunix`kmem_cache_alloc',v:241},{n:'unix`mutex_enter',v:366},{n:'unix`mutex_exit',v:149}],n:'genunix`kmem_alloc',v:934},{n:'genunix`kmem_cache_alloc',v:32}],n:'genunix`vn_setpath',v:1969},{c:[{n:'genunix`crgetmapped',v:36},{c:[{n:'genunix`crgetmapped',v:58},{n:'genunix`dnlc_lookup',v:70},{n:'genunix`vn_rele',v:14},{n:'ufs`ufs_iaccess',v:91},{c:[{n:'genunix`crgetuid',v:30},{c:[{n:'genunix`memcmp',v:38},{c:[{n:'genunix`memcmp',v:277}],n:'unix`bcmp',v:295}],n:'genunix`dnlc_lookup',v:1843},{n:'genunix`secpolicy_vnode_access2',v:72},{n:'genunix`vn_rele',v:39},{c:[{n:'genunix`crgetuid',v:22},{n:'genunix`secpolicy_vnode_access2',v:217}],n:'ufs`ufs_iaccess',v:648},{n:'unix`bcmp',v:42},{n:'unix`mutex_enter',v:980},{n:'unix`mutex_exit',v:350},{n:'unix`rw_enter',v:525},{n:'unix`rw_exit',v:439}],n:'ufs`ufs_lookup',v:5399}],n:'genunix`fop_lookup',v:6470},{n:'genunix`kmem_cache_alloc',v:39},{c:[{n:'genunix`rwst_exit',v:18},{n:'genunix`rwst_tryenter',v:32},{n:'genunix`vn_mountedvfs',v:11},{n:'genunix`vn_vfslocks_getlock',v:62},{n:'genunix`vn_vfslocks_rele',v:50},{c:[{n:'genunix`kmem_alloc',v:32},{n:'genunix`rwst_enter_common',v:32},{n:'genunix`rwst_init',v:28},{c:[{n:'genunix`rwst_enter_common',v:264},{n:'unix`mutex_enter',v:337},{n:'unix`mutex_exit',v:105}],n:'genunix`rwst_tryenter',v:734},{c:[{n:'genunix`cv_init',v:53},{c:[{c:[{n:'genunix`kmem_cpu_reload',v:2}],n:'genunix`kmem_cache_alloc',v:168},{n:'unix`mutex_enter',v:379},{n:'unix`mutex_exit',v:155}],n:'genunix`kmem_alloc',v:795},{n:'genunix`kmem_cache_alloc',v:29},{c:[{n:'genunix`cv_init',v:65},{n:'unix`mutex_init',v:53}],n:'genunix`rwst_init',v:236},{n:'unix`mutex_init',v:46}],n:'genunix`vn_vfslocks_getlock',v:1357},{n:'unix`mutex_enter',v:727},{n:'unix`mutex_exit',v:371}],n:'genunix`vn_vfsrlock',v:3342},{c:[{n:'genunix`cv_broadcast',v:25},{n:'genunix`kmem_free',v:35},{n:'genunix`rwst_destroy',v:32},{c:[{n:'genunix`cv_broadcast',v:40}],n:'genunix`rwst_exit',v:167},{n:'genunix`vn_vfslocks_getlock',v:120},{c:[{n:'genunix`cv_destroy',v:77},{n:'genunix`kmem_cache_free',v:22},{c:[{n:'genunix`kmem_cache_free',v:154},{n:'unix`mutex_enter',v:316},{n:'unix`mutex_exit',v:148}],n:'genunix`kmem_free',v:693},{c:[{n:'genunix`cv_destroy',v:42},{n:'unix`mutex_destroy',v:176}],n:'genunix`rwst_destroy',v:296},{n:'unix`mutex_destroy',v:31}],n:'genunix`vn_vfslocks_rele',v:1420},{n:'unix`mutex_enter',v:1202},{n:'unix`mutex_exit',v:512}],n:'genunix`vn_vfsunlock',v:3578}],n:'genunix`traverse',v:7243},{n:'genunix`vfs_getops',v:21},{c:[{n:'genunix`vfs_getops',v:157},{n:'unix`membar_consumer',v:123}],n:'genunix`vfs_matchops',v:336},{n:'genunix`vn_alloc',v:20},{n:'genunix`vn_exists',v:17},{n:'genunix`vn_mountedvfs',v:30},{n:'genunix`vn_setops',v:41},{n:'genunix`vn_vfsrlock',v:13},{n:'genunix`vn_vfsunlock',v:40},{n:'lofs`lfind',v:26},{n:'lofs`lsave',v:27},{n:'lofs`makelfsnode',v:28},{c:[{n:'genunix`kmem_cache_alloc',v:234},{n:'genunix`kmem_cpu_reload',v:1},{c:[{n:'genunix`kmem_cache_alloc',v:179},{n:'genunix`vn_recycle',v:33},{c:[{c:[{n:'genunix`vsd_free',v:155}],n:'genunix`vn_recycle',v:319},{n:'genunix`vsd_free',v:14}],n:'genunix`vn_reinit',v:424},{n:'unix`mutex_enter',v:318},{n:'unix`mutex_exit',v:142}],n:'genunix`vn_alloc',v:1189},{n:'genunix`vn_exists',v:50},{n:'genunix`vn_reinit',v:48},{n:'genunix`vn_setops',v:160},{n:'lofs`lfind',v:278},{n:'lofs`lsave',v:162},{n:'lofs`makelfsnode',v:82},{n:'lofs`table_lock_enter',v:220},{n:'unix`atomic_cas_64',v:318},{n:'unix`membar_consumer',v:237},{n:'unix`mutex_enter',v:640},{n:'unix`mutex_exit',v:138}],n:'lofs`makelonode',v:4212},{n:'lofs`table_lock_enter',v:43},{n:'ufs`ufs_lookup',v:46},{n:'unix`atomic_add_32',v:325},{n:'unix`mutex_exit',v:26}],n:'lofs`lo_lookup',v:19887},{n:'lofs`makelonode',v:39},{n:'unix`bcopy',v:896},{n:'unix`mutex_enter',v:947},{n:'unix`mutex_exit',v:337},{c:[{c:[{c:[{n:'unix`dispatch_hilevel',v:1}],n:'unix`do_interrupt',v:1}],n:'unix`_interrupt',v:1}],n:'unix`strlen',v:2659},{n:'zfs`specvp_check',v:10},{n:'zfs`zfs_fastaccesschk_execute',v:4},{c:[{n:'genunix`crgetuid',v:6},{c:[{n:'genunix`memcmp',v:3},{c:[{n:'genunix`memcmp',v:38}],n:'unix`bcmp',v:45}],n:'genunix`dnlc_lookup',v:263},{n:'unix`bcmp',v:11},{n:'unix`mutex_enter',v:309},{n:'unix`mutex_exit',v:135},{n:'zfs`specvp_check',v:20},{c:[{n:'genunix`crgetuid',v:2}],n:'zfs`zfs_fastaccesschk_execute',v:50}],n:'zfs`zfs_lookup',v:946}],n:'genunix`fop_lookup',v:29216},{n:'genunix`fsop_root',v:62},{n:'genunix`pn_fixslash',v:44},{n:'genunix`pn_getcomponent',v:454},{c:[{c:[{n:'lofs`lo_root',v:80},{n:'unix`mutex_enter',v:95},{n:'unix`mutex_exit',v:59}],n:'genunix`fsop_root',v:297},{n:'genunix`rwst_exit',v:12},{n:'genunix`rwst_tryenter',v:37},{n:'genunix`vn_mountedvfs',v:20},{n:'genunix`vn_rele',v:19},{n:'genunix`vn_vfslocks_getlock',v:47},{n:'genunix`vn_vfslocks_rele',v:34},{c:[{n:'genunix`kmem_alloc',v:11},{n:'genunix`rwst_enter_common',v:28},{n:'genunix`rwst_init',v:13},{c:[{n:'genunix`rwst_enter_common',v:314},{n:'unix`mutex_enter',v:238},{n:'unix`mutex_exit',v:49}],n:'genunix`rwst_tryenter',v:628},{c:[{n:'genunix`cv_init',v:56},{c:[{n:'genunix`kmem_cache_alloc',v:126},{n:'unix`mutex_enter',v:252},{n:'unix`mutex_exit',v:95}],n:'genunix`kmem_alloc',v:533},{n:'genunix`kmem_cache_alloc',v:17},{c:[{n:'genunix`cv_init',v:49},{n:'unix`mutex_init',v:38}],n:'genunix`rwst_init',v:173},{n:'unix`mutex_init',v:31}],n:'genunix`vn_vfslocks_getlock',v:973},{n:'unix`mutex_enter',v:455},{n:'unix`mutex_exit',v:250}],n:'genunix`vn_vfsrlock',v:2414},{c:[{n:'genunix`cv_broadcast',v:14},{n:'genunix`kmem_free',v:17},{n:'genunix`rwst_destroy',v:20},{c:[{n:'genunix`cv_broadcast',v:19}],n:'genunix`rwst_exit',v:110},{n:'genunix`vn_vfslocks_getlock',v:79},{c:[{n:'genunix`cv_destroy',v:81},{n:'genunix`kmem_cache_free',v:18},{c:[{n:'genunix`kmem_cache_free',v:116},{n:'unix`mutex_enter',v:195},{n:'unix`mutex_exit',v:90}],n:'genunix`kmem_free',v:457},{c:[{n:'genunix`cv_destroy',v:31},{n:'unix`mutex_destroy',v:53}],n:'genunix`rwst_destroy',v:146},{n:'unix`mutex_destroy',v:17}],n:'genunix`vn_vfslocks_rele',v:903},{n:'unix`mutex_enter',v:823},{n:'unix`mutex_exit',v:356}],n:'genunix`vn_vfsunlock',v:2372},{n:'lofs`lo_root',v:31},{n:'unix`mutex_enter',v:95},{n:'unix`mutex_exit',v:56}],n:'genunix`traverse',v:5557},{n:'genunix`vn_mountedvfs',v:43},{c:[{n:'genunix`crgetmapped',v:31},{c:[{n:'genunix`crgetmapped',v:41},{n:'lofs`freelonode',v:35},{c:[{n:'genunix`kmem_cache_free',v:29},{n:'genunix`vn_free',v:26},{n:'genunix`vn_invalid',v:20},{n:'genunix`vn_rele',v:25},{c:[{c:[{n:'genunix`kmem_cpu_reload',v:1}],n:'genunix`kmem_cache_free',v:184},{n:'genunix`kmem_free',v:115},{c:[{c:[{n:'genunix`kmem_cpu_reload',v:4}],n:'genunix`kmem_cache_free',v:215},{n:'genunix`kmem_cpu_reload',v:5},{c:[{n:'genunix`kmem_cache_free',v:209},{n:'unix`mutex_enter',v:299},{n:'unix`mutex_exit',v:160}],n:'genunix`kmem_free',v:785},{n:'genunix`vsd_free',v:48},{n:'unix`mutex_enter',v:314},{n:'unix`mutex_exit',v:171}],n:'genunix`vn_free',v:1663},{n:'genunix`vn_invalid',v:47},{n:'genunix`vn_rele',v:64},{n:'genunix`vsd_free',v:17},{n:'lofs`table_lock_enter',v:189},{n:'unix`membar_consumer',v:106},{n:'unix`mutex_enter',v:905},{n:'unix`mutex_exit',v:358},{n:'unix`strlen',v:1238}],n:'lofs`freelonode',v:5313},{n:'lofs`table_lock_enter',v:44},{n:'unix`atomic_add_32',v:292},{n:'unix`mutex_enter',v:279},{n:'unix`mutex_exit',v:212}],n:'lofs`lo_inactive',v:6307}],n:'genunix`fop_inactive',v:6689},{n:'lofs`lo_inactive',v:21}],n:'genunix`vn_rele',v:6943},{n:'genunix`vn_setpath',v:58},{n:'genunix`vn_vfsrlock',v:12},{n:'genunix`vn_vfsunlock',v:20},{n:'lofs`lo_lookup',v:65},{n:'unix`mutex_enter',v:575},{n:'unix`mutex_exit',v:379},{n:'unix`strlen',v:107},{n:'zfs`zfs_lookup',v:22}],n:'genunix`lookuppnvp',v:44242},{n:'genunix`pn_fixslash',v:14},{n:'genunix`pn_getcomponent',v:41},{n:'genunix`traverse',v:17},{n:'genunix`vn_mountedvfs',v:56},{n:'genunix`vn_rele',v:73},{c:[{n:'unix`mutex_delay_default',v:1},{n:'unix`tsc_read',v:1}],n:'unix`mutex_vector_enter',v:2}],n:'genunix`lookuppnatcred',v:44681},{n:'genunix`lookuppnvp',v:10},{c:[{n:'unix`copyinstr',v:25},{n:'unix`copystr',v:598}],n:'genunix`pn_get_buf',v:687},{n:'unix`copyinstr',v:18},{n:'unix`mutex_enter',v:320},{n:'unix`mutex_exit',v:163}],n:'genunix`lookupnameatcred',v:45978},{n:'genunix`lookuppnatcred',v:12},{n:'genunix`pn_get_buf',v:13}],n:'genunix`lookupnameat',v:46075},{n:'genunix`lookupnameatcred',v:22}],n:'genunix`vn_openat',v:46342},{n:'unix`mutex_enter',v:303},{n:'unix`mutex_exit',v:38}],n:'genunix`copen',v:49444},{n:'genunix`falloc',v:36},{n:'genunix`set_errno',v:9},{n:'genunix`setf',v:16},{n:'genunix`unfalloc',v:39},{n:'genunix`vn_openat',v:14}],n:'genunix`openat',v:49647}],n:'genunix`open',v:49669},{n:'genunix`openat',v:17},{c:[{c:[{c:[{n:'genunix`dotoprocs',v:1}],n:'genunix`doprio',v:1}],n:'genunix`priocntl_common',v:1}],n:'genunix`priocntlsys',v:1},{c:[{c:[{c:[{c:[{c:[{c:[{c:[{c:[{n:'genunix`dnlc_lookup',v:1}],n:'ufs`ufs_lookup',v:1}],n:'genunix`fop_lookup',v:1}],n:'lofs`lo_lookup',v:1}],n:'genunix`fop_lookup',v:1}],n:'genunix`lookuppnvp',v:1}],n:'genunix`lookuppnatcred',v:1}],n:'genunix`lookuppn',v:1}],n:'genunix`resolvepath',v:1},{c:[{c:[{c:[{c:[{c:[{c:[{c:[{n:'genunix`kmem_cache_free',v:1}],n:'genunix`kmem_free',v:1}],n:'genunix`removectx',v:1}],n:'genunix`schedctl_lwp_cleanup',v:1}],n:'genunix`exitlwps',v:1},{c:[{c:[{c:[{c:[{c:[{c:[{c:[{n:'unix`hment_compare',v:2}],n:'genunix`avl_find',v:2}],n:'unix`hment_remove',v:2},{n:'unix`page_numtopp_nolock',v:1}],n:'unix`hat_pte_unmap',v:3}],n:'unix`hat_unload_callback',v:3}],n:'genunix`segvn_unmap',v:3}],n:'genunix`as_free',v:3}],n:'genunix`relvm',v:3},{c:[{c:[{c:[{c:[{n:'genunix`vmem_free',v:1}],n:'genunix`segkp_release_internal',v:1}],n:'genunix`segkp_release',v:1}],n:'genunix`schedctl_freepage',v:1}],n:'genunix`schedctl_proc_cleanup',v:1}],n:'genunix`proc_exit',v:5}],n:'genunix`exit',v:5}],n:'genunix`rexit',v:5},{c:[{c:[{n:'unix`tsc_gethrtimeunscaled',v:43},{n:'unix`tsc_read',v:367}],n:'genunix`gethrtime_unscaled',v:420},{n:'unix`tsc_gethrtimeunscaled',v:59}],n:'genunix`syscall_mstate',v:1336},{n:'unix`atomic_add_64',v:205}],n:'unix`sys_syscall',v:51908}],n:'root',v:57412}; diff --git a/packages/osd-charts/src/mocks/hierarchical/palettes.ts b/packages/osd-charts/src/mocks/hierarchical/palettes.ts new file mode 100644 index 000000000000..6cb5da4b16af --- /dev/null +++ b/packages/osd-charts/src/mocks/hierarchical/palettes.ts @@ -0,0 +1,544 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RgbTuple } from '../../common/color_library_wrappers'; + +const CET2s: RgbTuple[] = [ + [46, 34, 235], + [49, 32, 237], + [52, 30, 238], + [56, 29, 239], + [59, 28, 240], + [63, 27, 241], + [66, 27, 242], + [70, 27, 242], + [73, 27, 243], + [77, 28, 244], + [80, 29, 244], + [84, 30, 245], + [87, 31, 245], + [91, 32, 246], + [94, 33, 246], + [97, 35, 246], + [100, 36, 247], + [103, 38, 247], + [106, 39, 248], + [109, 41, 248], + [112, 42, 248], + [115, 44, 249], + [118, 45, 249], + [121, 47, 249], + [123, 48, 250], + [126, 49, 250], + [129, 51, 250], + [132, 52, 251], + [135, 53, 251], + [137, 54, 251], + [140, 56, 251], + [143, 57, 251], + [146, 58, 252], + [149, 59, 252], + [152, 60, 252], + [155, 60, 252], + [158, 61, 252], + [162, 62, 252], + [165, 63, 252], + [168, 63, 252], + [171, 64, 252], + [175, 65, 252], + [178, 65, 252], + [181, 66, 252], + [185, 66, 252], + [188, 66, 252], + [191, 67, 252], + [195, 67, 252], + [198, 68, 252], + [201, 68, 251], + [204, 69, 251], + [207, 69, 251], + [211, 70, 251], + [214, 70, 251], + [217, 71, 250], + [219, 72, 250], + [222, 73, 250], + [225, 74, 249], + [227, 75, 249], + [230, 76, 248], + [232, 78, 247], + [234, 79, 246], + [236, 81, 245], + [238, 83, 244], + [240, 85, 243], + [242, 88, 241], + [243, 90, 240], + [244, 93, 238], + [245, 96, 236], + [246, 99, 234], + [247, 102, 232], + [248, 105, 230], + [249, 108, 227], + [249, 111, 225], + [250, 114, 223], + [250, 117, 220], + [251, 120, 217], + [251, 123, 215], + [252, 127, 212], + [252, 130, 210], + [252, 133, 207], + [252, 136, 204], + [252, 139, 201], + [253, 141, 199], + [253, 144, 196], + [253, 147, 193], + [253, 150, 190], + [253, 153, 188], + [253, 156, 185], + [253, 158, 182], + [253, 161, 179], + [253, 164, 177], + [253, 166, 174], + [253, 169, 171], + [253, 171, 168], + [253, 174, 165], + [252, 176, 162], + [252, 179, 160], + [252, 181, 157], + [252, 184, 154], + [252, 186, 151], + [253, 188, 148], + [253, 191, 145], + [253, 193, 142], + [253, 195, 139], + [253, 198, 136], + [253, 200, 133], + [253, 202, 130], + [253, 204, 127], + [253, 207, 124], + [253, 209, 120], + [253, 211, 117], + [253, 213, 114], + [253, 215, 110], + [253, 217, 107], + [253, 219, 104], + [253, 221, 100], + [252, 223, 96], + [252, 225, 93], + [252, 227, 89], + [251, 229, 85], + [250, 231, 81], + [250, 232, 77], + [249, 234, 73], + [248, 235, 69], + [246, 236, 65], + [245, 237, 61], + [243, 238, 57], + [242, 239, 54], + [240, 239, 50], + [238, 239, 46], + [235, 239, 43], + [233, 239, 40], + [231, 239, 37], + [228, 239, 35], + [225, 238, 33], + [223, 238, 31], + [220, 237, 29], + [217, 236, 27], + [214, 235, 26], + [211, 234, 25], + [209, 233, 24], + [206, 232, 24], + [203, 231, 23], + [200, 230, 22], + [197, 229, 22], + [194, 228, 21], + [191, 227, 21], + [188, 226, 21], + [185, 225, 20], + [182, 224, 20], + [179, 223, 20], + [176, 221, 19], + [173, 220, 19], + [170, 219, 19], + [167, 218, 18], + [164, 217, 18], + [161, 216, 17], + [158, 215, 17], + [154, 214, 17], + [151, 213, 16], + [148, 211, 16], + [145, 210, 16], + [142, 209, 15], + [139, 208, 15], + [136, 207, 15], + [132, 206, 14], + [129, 205, 14], + [126, 204, 14], + [122, 202, 13], + [119, 201, 13], + [116, 200, 13], + [112, 199, 13], + [109, 198, 12], + [105, 197, 12], + [102, 196, 12], + [98, 194, 12], + [94, 193, 12], + [91, 192, 12], + [87, 191, 12], + [83, 190, 13], + [79, 188, 14], + [76, 187, 15], + [72, 186, 16], + [68, 185, 18], + [65, 183, 20], + [62, 182, 22], + [59, 181, 25], + [56, 179, 27], + [54, 178, 30], + [52, 176, 34], + [51, 175, 37], + [50, 173, 40], + [50, 172, 44], + [50, 170, 48], + [51, 168, 51], + [52, 167, 55], + [53, 165, 59], + [54, 163, 63], + [56, 161, 67], + [57, 160, 71], + [59, 158, 74], + [60, 156, 78], + [62, 154, 82], + [63, 152, 86], + [64, 150, 90], + [66, 148, 93], + [67, 147, 97], + [67, 145, 101], + [68, 143, 104], + [69, 141, 108], + [69, 139, 111], + [69, 137, 115], + [70, 135, 118], + [70, 133, 122], + [69, 131, 125], + [69, 129, 129], + [69, 128, 132], + [68, 126, 135], + [67, 124, 139], + [67, 122, 142], + [66, 120, 145], + [64, 118, 149], + [63, 116, 152], + [62, 114, 155], + [60, 112, 158], + [59, 110, 162], + [57, 108, 165], + [56, 106, 168], + [54, 104, 171], + [53, 102, 174], + [51, 100, 177], + [50, 98, 180], + [48, 96, 183], + [47, 93, 185], + [46, 91, 188], + [45, 89, 191], + [44, 86, 193], + [43, 84, 196], + [42, 81, 199], + [41, 79, 201], + [40, 76, 204], + [40, 73, 206], + [39, 70, 209], + [38, 68, 211], + [38, 65, 213], + [37, 62, 216], + [37, 59, 218], + [37, 56, 220], + [37, 53, 222], + [37, 50, 224], + [37, 47, 227], + [38, 44, 228], + [40, 41, 230], + [42, 39, 232], + [44, 36, 234], +]; + +const turbo: RgbTuple[] = [ + [48, 18, 59], + [50, 21, 67], + [51, 24, 74], + [52, 27, 81], + [53, 30, 88], + [54, 33, 95], + [55, 36, 102], + [56, 39, 109], + [57, 42, 115], + [58, 45, 121], + [59, 47, 128], + [60, 50, 134], + [61, 53, 139], + [62, 56, 145], + [63, 59, 151], + [63, 62, 156], + [64, 64, 162], + [65, 67, 167], + [65, 70, 172], + [66, 73, 177], + [66, 75, 181], + [67, 78, 186], + [68, 81, 191], + [68, 84, 195], + [68, 86, 199], + [69, 89, 203], + [69, 92, 207], + [69, 94, 211], + [70, 97, 214], + [70, 100, 218], + [70, 102, 221], + [70, 105, 224], + [70, 107, 227], + [71, 110, 230], + [71, 113, 233], + [71, 115, 235], + [71, 118, 238], + [71, 120, 240], + [71, 123, 242], + [70, 125, 244], + [70, 128, 246], + [70, 130, 248], + [70, 133, 250], + [70, 135, 251], + [69, 138, 252], + [69, 140, 253], + [68, 143, 254], + [67, 145, 254], + [66, 148, 255], + [65, 150, 255], + [64, 153, 255], + [62, 155, 254], + [61, 158, 254], + [59, 160, 253], + [58, 163, 252], + [56, 165, 251], + [55, 168, 250], + [53, 171, 248], + [51, 173, 247], + [49, 175, 245], + [47, 178, 244], + [46, 180, 242], + [44, 183, 240], + [42, 185, 238], + [40, 188, 235], + [39, 190, 233], + [37, 192, 231], + [35, 195, 228], + [34, 197, 226], + [32, 199, 223], + [31, 201, 221], + [30, 203, 218], + [28, 205, 216], + [27, 208, 213], + [26, 210, 210], + [26, 212, 208], + [25, 213, 205], + [24, 215, 202], + [24, 217, 200], + [24, 219, 197], + [24, 221, 194], + [24, 222, 192], + [24, 224, 189], + [25, 226, 187], + [25, 227, 185], + [26, 228, 182], + [28, 230, 180], + [29, 231, 178], + [31, 233, 175], + [32, 234, 172], + [34, 235, 170], + [37, 236, 167], + [39, 238, 164], + [42, 239, 161], + [44, 240, 158], + [47, 241, 155], + [50, 242, 152], + [53, 243, 148], + [56, 244, 145], + [60, 245, 142], + [63, 246, 138], + [67, 247, 135], + [70, 248, 132], + [74, 248, 128], + [78, 249, 125], + [82, 250, 122], + [85, 250, 118], + [89, 251, 115], + [93, 252, 111], + [97, 252, 108], + [101, 253, 105], + [105, 253, 102], + [109, 254, 98], + [113, 254, 95], + [117, 254, 92], + [121, 254, 89], + [125, 255, 86], + [128, 255, 83], + [132, 255, 81], + [136, 255, 78], + [139, 255, 75], + [143, 255, 73], + [146, 255, 71], + [150, 254, 68], + [153, 254, 66], + [156, 254, 64], + [159, 253, 63], + [161, 253, 61], + [164, 252, 60], + [167, 252, 58], + [169, 251, 57], + [172, 251, 56], + [175, 250, 55], + [177, 249, 54], + [180, 248, 54], + [183, 247, 53], + [185, 246, 53], + [188, 245, 52], + [190, 244, 52], + [193, 243, 52], + [195, 241, 52], + [198, 240, 52], + [200, 239, 52], + [203, 237, 52], + [205, 236, 52], + [208, 234, 52], + [210, 233, 53], + [212, 231, 53], + [215, 229, 53], + [217, 228, 54], + [219, 226, 54], + [221, 224, 55], + [223, 223, 55], + [225, 221, 55], + [227, 219, 56], + [229, 217, 56], + [231, 215, 57], + [233, 213, 57], + [235, 211, 57], + [236, 209, 58], + [238, 207, 58], + [239, 205, 58], + [241, 203, 58], + [242, 201, 58], + [244, 199, 58], + [245, 197, 58], + [246, 195, 58], + [247, 193, 58], + [248, 190, 57], + [249, 188, 57], + [250, 186, 57], + [251, 184, 56], + [251, 182, 55], + [252, 179, 54], + [252, 177, 54], + [253, 174, 53], + [253, 172, 52], + [254, 169, 51], + [254, 167, 50], + [254, 164, 49], + [254, 161, 48], + [254, 158, 47], + [254, 155, 45], + [254, 153, 44], + [254, 150, 43], + [254, 147, 42], + [254, 144, 41], + [253, 141, 39], + [253, 138, 38], + [252, 135, 37], + [252, 132, 35], + [251, 129, 34], + [251, 126, 33], + [250, 123, 31], + [249, 120, 30], + [249, 117, 29], + [248, 114, 28], + [247, 111, 26], + [246, 108, 25], + [245, 105, 24], + [244, 102, 23], + [243, 99, 21], + [242, 96, 20], + [241, 93, 19], + [240, 91, 18], + [239, 88, 17], + [237, 85, 16], + [236, 83, 15], + [235, 80, 14], + [234, 78, 13], + [232, 75, 12], + [231, 73, 12], + [229, 71, 11], + [228, 69, 10], + [226, 67, 10], + [225, 65, 9], + [223, 63, 8], + [221, 61, 8], + [220, 59, 7], + [218, 57, 7], + [216, 55, 6], + [214, 53, 6], + [212, 51, 5], + [210, 49, 5], + [208, 47, 5], + [206, 45, 4], + [204, 43, 4], + [202, 42, 4], + [200, 40, 3], + [197, 38, 3], + [195, 37, 3], + [193, 35, 2], + [190, 33, 2], + [188, 32, 2], + [185, 30, 2], + [183, 29, 2], + [180, 27, 1], + [178, 26, 1], + [175, 24, 1], + [172, 23, 1], + [169, 22, 1], + [167, 20, 1], + [164, 19, 1], + [161, 18, 1], + [158, 16, 1], + [155, 15, 1], + [152, 14, 1], + [149, 13, 1], + [146, 11, 1], + [142, 10, 1], + [139, 9, 2], + [136, 8, 2], + [133, 7, 2], + [129, 6, 2], + [126, 5, 2], + [122, 4, 3], +]; + +/** @internal */ +export const palettes = { + CET2s, + turbo, +}; diff --git a/packages/osd-charts/src/mocks/hierarchical/pie.ts b/packages/osd-charts/src/mocks/hierarchical/pie.ts new file mode 100644 index 000000000000..bfdae3227629 --- /dev/null +++ b/packages/osd-charts/src/mocks/hierarchical/pie.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const pieMock = [ + { sitc1: '7', exportVal: 3110253391368 }, + { sitc1: '3', exportVal: 1929578418424 }, + { sitc1: '5', exportVal: 848173542536 }, + { sitc1: '8', exportVal: 816837797016 }, + { sitc1: '6', exportVal: 745168037744 }, + { sitc1: '9', exportVal: 450507812880 }, + { sitc1: '2', exportVal: 393895581328 }, + { sitc1: '0', exportVal: 353335453296 }, + { sitc1: '1', exportVal: 54461075800 }, + { sitc1: '4', exportVal: 36006897720 }, +]; diff --git a/packages/osd-charts/src/mocks/hierarchical/sunburst.ts b/packages/osd-charts/src/mocks/hierarchical/sunburst.ts new file mode 100644 index 000000000000..0eb5e0c536ac --- /dev/null +++ b/packages/osd-charts/src/mocks/hierarchical/sunburst.ts @@ -0,0 +1,168 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const sunburstMock = [ + { sitc1: '7', dest: 'usa', exportVal: 553359100104 }, + { sitc1: '7', dest: 'chn', exportVal: 392617281424 }, + { sitc1: '3', dest: 'usa', exportVal: 324856796136 }, + { sitc1: '7', dest: 'deu', exportVal: 253250650864 }, + { sitc1: '8', dest: 'usa', exportVal: 226628559432 }, + { sitc1: '7', dest: 'hkg', exportVal: 177490158520 }, + { sitc1: '3', dest: 'jpn', exportVal: 177421375512 }, + { sitc1: '2', dest: 'chn', exportVal: 173840557624 }, + { sitc1: '3', dest: 'chn', exportVal: 167989572088 }, + { sitc1: '7', dest: 'fra', exportVal: 135443006088 }, + { sitc1: '6', dest: 'usa', exportVal: 129879187528 }, + { sitc1: '5', dest: 'usa', exportVal: 127516647672 }, + { sitc1: '7', dest: 'can', exportVal: 118114362152 }, + { sitc1: '7', dest: 'gbr', exportVal: 117139481536 }, + { sitc1: '3', dest: 'kor', exportVal: 108348223232 }, + { sitc1: '7', dest: 'jpn', exportVal: 106979336520 }, + { sitc1: '7', dest: 'mex', exportVal: 99444329328 }, + { sitc1: '3', dest: 'ind', exportVal: 96948083624 }, + { sitc1: '7', dest: 'sgp', exportVal: 92225795024 }, + { sitc1: '5', dest: 'deu', exportVal: 90784113552 }, + { sitc1: '5', dest: 'chn', exportVal: 90181681968 }, + { sitc1: '3', dest: 'deu', exportVal: 88253653112 }, + { sitc1: '7', dest: 'ita', exportVal: 81276009824 }, + { sitc1: '6', dest: 'chn', exportVal: 79452389712 }, + { sitc1: '7', dest: 'kor', exportVal: 78794233936 }, + { sitc1: '3', dest: 'ita', exportVal: 78558945920 }, + { sitc1: '3', dest: 'fra', exportVal: 74925032472 }, + { sitc1: '9', dest: 'deu', exportVal: 73071171872 }, + { sitc1: '5', dest: 'bel', exportVal: 70624508592 }, + { sitc1: '8', dest: 'deu', exportVal: 70580844576 }, + { sitc1: '7', dest: 'nld', exportVal: 68849046720 }, + { sitc1: '6', dest: 'deu', exportVal: 67652211616 }, + { sitc1: '3', dest: 'sgp', exportVal: 67210539248 }, + { sitc1: '8', dest: 'chn', exportVal: 63708999648 }, + { sitc1: '3', dest: 'nld', exportVal: 63266504120 }, + { sitc1: '9', dest: 'usa', exportVal: 60516229752 }, + { sitc1: '8', dest: 'jpn', exportVal: 56814537520 }, + { sitc1: '3', dest: 'gbr', exportVal: 54869262576 }, + { sitc1: '8', dest: 'hkg', exportVal: 53605517320 }, + { sitc1: '5', dest: 'fra', exportVal: 52161793136 }, + { sitc1: '7', dest: 'esp', exportVal: 50643986720 }, + { sitc1: '7', dest: 'bel', exportVal: 49990475528 }, + { sitc1: '7', dest: 'mys', exportVal: 49686542376 }, + { sitc1: '9', dest: 'gbr', exportVal: 49592572344 }, + { sitc1: '3', dest: 'esp', exportVal: 49335325064 }, + { sitc1: '0', dest: 'usa', exportVal: 48357009888 }, + { sitc1: '7', dest: 'rus', exportVal: 48165543208 }, + { sitc1: '8', dest: 'gbr', exportVal: 47653225184 }, + { sitc1: '3', dest: 'bel', exportVal: 47114307208 }, + { sitc1: '8', dest: 'fra', exportVal: 45557265608 }, + { sitc1: '9', dest: 'ind', exportVal: 41487417688 }, + { sitc1: '5', dest: 'gbr', exportVal: 41404042128 }, + { sitc1: '2', dest: 'jpn', exportVal: 41093852032 }, + { sitc1: '7', dest: 'aus', exportVal: 40800311744 }, + { sitc1: '7', dest: 'bra', exportVal: 38963332352 }, + { sitc1: '5', dest: 'ita', exportVal: 38938981872 }, + { sitc1: '7', dest: 'tha', exportVal: 37165468320 }, + { sitc1: '5', dest: 'jpn', exportVal: 35950015760 }, + { sitc1: '0', dest: 'jpn', exportVal: 34492545824 }, + { sitc1: '3', dest: 'can', exportVal: 34474612000 }, + { sitc1: '0', dest: 'deu', exportVal: 33477297584 }, + { sitc1: '6', dest: 'kor', exportVal: 33305898944 }, + { sitc1: '6', dest: 'jpn', exportVal: 32438828976 }, + { sitc1: '6', dest: 'fra', exportVal: 31882703000 }, + { sitc1: '6', dest: 'ind', exportVal: 31268977728 }, + { sitc1: '6', dest: 'hkg', exportVal: 29394455800 }, + { sitc1: '7', dest: 'pol', exportVal: 29081774000 }, + { sitc1: '5', dest: 'can', exportVal: 28752002440 }, + { sitc1: '7', dest: 'cze', exportVal: 28475442896 }, + { sitc1: '8', dest: 'can', exportVal: 28272413784 }, + { sitc1: '7', dest: 'are', exportVal: 27885234800 }, + { sitc1: '7', dest: 'ind', exportVal: 27758142816 }, + { sitc1: '6', dest: 'gbr', exportVal: 27633859680 }, + { sitc1: '3', dest: 'tha', exportVal: 27233135240 }, + { sitc1: '6', dest: 'can', exportVal: 26907257624 }, + { sitc1: '7', dest: 'tur', exportVal: 25382715776 }, + { sitc1: '5', dest: 'nld', exportVal: 25069805448 }, + { sitc1: '7', dest: 'sau', exportVal: 24929023368 }, + { sitc1: '6', dest: 'ita', exportVal: 24467435912 }, + { sitc1: '6', dest: 'bel', exportVal: 24164232160 }, + { sitc1: '5', dest: 'che', exportVal: 23923665600 }, + { sitc1: '3', dest: 'bra', exportVal: 23917919768 }, + { sitc1: '3', dest: 'idn', exportVal: 23793941504 }, + { sitc1: '2', dest: 'deu', exportVal: 23489678712 }, + { sitc1: '8', dest: 'ita', exportVal: 23095374432 }, + { sitc1: '9', dest: 'nld', exportVal: 22850286888 }, + { sitc1: '7', dest: 'aut', exportVal: 22375876440 }, + { sitc1: '3', dest: 'tur', exportVal: 22364831560 }, + { sitc1: '7', dest: 'swe', exportVal: 22362348184 }, + { sitc1: '7', dest: 'idn', exportVal: 22360516904 }, + { sitc1: '7', dest: 'che', exportVal: 22029172056 }, + { sitc1: '5', dest: 'esp', exportVal: 21845165664 }, + { sitc1: '9', dest: 'chn', exportVal: 21310345576 }, + { sitc1: '5', dest: 'kor', exportVal: 21172341776 }, + { sitc1: '0', dest: 'gbr', exportVal: 21018713424 }, + { sitc1: '8', dest: 'nld', exportVal: 20929434208 }, + { sitc1: '6', dest: 'mex', exportVal: 20914470128 }, + { sitc1: '3', dest: 'aus', exportVal: 20317805888 }, + { sitc1: '2', dest: 'kor', exportVal: 20313758104 }, + { sitc1: '3', dest: 'mex', exportVal: 20083955848 }, + { sitc1: '0', dest: 'fra', exportVal: 19532719264 }, + { sitc1: '7', dest: 'hun', exportVal: 19324152296 }, + { sitc1: '8', dest: 'che', exportVal: 19322818728 }, + { sitc1: '5', dest: 'mex', exportVal: 19022488904 }, + { sitc1: '2', dest: 'usa', exportVal: 18018752920 }, + { sitc1: '3', dest: 'ukr', exportVal: 17117857400 }, + { sitc1: '3', dest: 'swe', exportVal: 17081304488 }, + { sitc1: '8', dest: 'esp', exportVal: 16965769448 }, + { sitc1: '9', dest: 'che', exportVal: 16908619328 }, + { sitc1: '6', dest: 'are', exportVal: 16785511464 }, + { sitc1: '8', dest: 'bel', exportVal: 16569534928 }, + { sitc1: '8', dest: 'kor', exportVal: 16210034464 }, + { sitc1: '8', dest: 'mex', exportVal: 16191125592 }, + { sitc1: '3', dest: 'pol', exportVal: 15907941008 }, + { sitc1: '0', dest: 'ita', exportVal: 15654747448 }, + { sitc1: '0', dest: 'nld', exportVal: 15586950392 }, + { sitc1: '9', dest: 'are', exportVal: 15404570344 }, + { sitc1: '7', dest: 'phl', exportVal: 15383624208 }, + { sitc1: '6', dest: 'tha', exportVal: 15148414408 }, + { sitc1: '5', dest: 'ind', exportVal: 14162411288 }, + { sitc1: '5', dest: 'bra', exportVal: 14050466400 }, + { sitc1: '1', dest: 'usa', exportVal: 13823693464 }, + { sitc1: '9', dest: 'can', exportVal: 13644993280 }, + { sitc1: '5', dest: 'rus', exportVal: 13476578168 }, + { sitc1: '3', dest: 'mys', exportVal: 13228401584 }, + { sitc1: '6', dest: 'nld', exportVal: 13094123616 }, + { sitc1: '0', dest: 'can', exportVal: 13013471312 }, + { sitc1: '3', dest: 'hkg', exportVal: 12879879616 }, + { sitc1: '3', dest: 'zaf', exportVal: 12803323896 }, + { sitc1: '0', dest: 'chn', exportVal: 12775895680 }, + { sitc1: '7', dest: 'arg', exportVal: 12686127312 }, + { sitc1: '9', dest: 'tur', exportVal: 12520989584 }, + { sitc1: '3', dest: 'che', exportVal: 12243582232 }, + { sitc1: '3', dest: 'grc', exportVal: 11976563768 }, + { sitc1: '7', dest: 'lbr', exportVal: 11587009744 }, + { sitc1: '0', dest: 'bel', exportVal: 11092939192 }, + { sitc1: '7', dest: 'svk', exportVal: 11063197832 }, + { sitc1: '3', dest: 'blr', exportVal: 10893808304 }, + { sitc1: '7', dest: 'nor', exportVal: 10739879720 }, + { sitc1: '0', dest: 'rus', exportVal: 10581033656 }, + { sitc1: '3', dest: 'aut', exportVal: 10463844120 }, + { sitc1: '6', dest: 'che', exportVal: 10426999544 }, + { sitc1: '9', dest: 'sgp', exportVal: 10338784808 }, + { sitc1: '8', dest: 'aus', exportVal: 10297853920 }, + { sitc1: '3', dest: 'fin', exportVal: 10297129144 }, + { sitc1: '8', dest: 'rus', exportVal: 10219614352 }, +]; diff --git a/packages/osd-charts/src/mocks/index.ts b/packages/osd-charts/src/mocks/index.ts new file mode 100644 index 000000000000..df6d39a673ee --- /dev/null +++ b/packages/osd-charts/src/mocks/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './series'; +export * from './geometries'; +export * from './theme'; +export * from './canvas'; diff --git a/packages/osd-charts/src/mocks/scale/index.ts b/packages/osd-charts/src/mocks/scale/index.ts new file mode 100644 index 000000000000..bbabbe2892ba --- /dev/null +++ b/packages/osd-charts/src/mocks/scale/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './scale'; diff --git a/packages/osd-charts/src/mocks/scale/scale.ts b/packages/osd-charts/src/mocks/scale/scale.ts new file mode 100644 index 000000000000..1955cdeed49a --- /dev/null +++ b/packages/osd-charts/src/mocks/scale/scale.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Scale } from '../../scales'; +import { ScaleType } from '../../scales/constants'; +import { mergePartial } from '../../utils/common'; + +/** @internal */ +export class MockScale { + private static readonly base: Scale = { + scaleOrThrow: jest.fn().mockImplementation((x) => x), + scale: jest.fn().mockImplementation((x) => x), + type: ScaleType.Linear, + step: 0, + bandwidth: 0, + bandwidthPadding: 0, + minInterval: 0, + barsPadding: 0, + range: [0, 100], + domain: [0, 100], + ticks: jest.fn(), + pureScale: jest.fn(), + invert: jest.fn(), + invertWithStep: jest.fn(), + isSingleValue: jest.fn(), + isValueInDomain: jest.fn(), + isInverted: false, + }; + + static default(partial: Partial): Scale { + return mergePartial(MockScale.base, partial); + } +} diff --git a/packages/osd-charts/src/mocks/series/data.ts b/packages/osd-charts/src/mocks/series/data.ts new file mode 100644 index 000000000000..8dddfd4743c0 --- /dev/null +++ b/packages/osd-charts/src/mocks/series/data.ts @@ -0,0 +1,180 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DataSeriesDatum } from '../../chart_types/xy_chart/utils/series'; + +/** @internal */ +export const fitFunctionData: DataSeriesDatum[] = [ + { + x: 0, + y1: null, + y0: null, + initialY1: null, + initialY0: null, + mark: null, + datum: { + x: 0, + y: null, + }, + }, + { + x: 1, + y1: 3, + y0: 0, + initialY1: 3, + initialY0: 0, + mark: null, + datum: { + x: 1, + y: 3, + }, + }, + { + x: 2, + y1: 5, + y0: 0, + initialY1: 5, + initialY0: 0, + mark: null, + datum: { + x: 2, + y: 5, + }, + }, + { + x: 3, + y1: null, + y0: null, + initialY1: null, + initialY0: null, + mark: null, + datum: { + x: 3, + y: null, + }, + }, + { + x: 4, + y1: 4, + y0: 0, + initialY1: 4, + initialY0: 0, + mark: null, + datum: { + x: 4, + y: 4, + }, + }, + { + x: 5, + y1: null, + y0: null, + initialY1: null, + initialY0: null, + mark: null, + datum: { + x: 5, + y: null, + }, + }, + { + x: 6, + y1: 5, + y0: 0, + initialY1: 5, + initialY0: 0, + mark: null, + datum: { + x: 6, + y: 5, + }, + }, + { + x: 7, + y1: 6, + y0: 0, + initialY1: 6, + initialY0: 0, + mark: null, + datum: { + x: 7, + y: 6, + }, + }, + { + x: 8, + y1: null, + y0: null, + initialY1: null, + initialY0: null, + mark: null, + datum: { + x: 8, + y: null, + }, + }, + { + x: 9, + y1: null, + y0: null, + initialY1: null, + initialY0: null, + mark: null, + datum: { + x: 9, + y: null, + }, + }, + { + x: 10, + y1: null, + y0: null, + initialY1: null, + initialY0: null, + mark: null, + datum: { + x: 10, + y: null, + }, + }, + { + x: 11, + y1: 12, + y0: 0, + initialY1: 12, + initialY0: 0, + mark: null, + datum: { + x: 11, + y: 12, + }, + }, + { + x: 12, + y1: null, + y0: null, + initialY1: null, + initialY0: null, + mark: null, + datum: { + x: 12, + y: null, + }, + }, +]; diff --git a/packages/osd-charts/src/mocks/series/index.ts b/packages/osd-charts/src/mocks/series/index.ts new file mode 100644 index 000000000000..75a3d2709782 --- /dev/null +++ b/packages/osd-charts/src/mocks/series/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './series'; + +/** @internal */ +export * from './utils'; diff --git a/packages/osd-charts/src/mocks/series/series.ts b/packages/osd-charts/src/mocks/series/series.ts new file mode 100644 index 000000000000..fb56fbeac723 --- /dev/null +++ b/packages/osd-charts/src/mocks/series/series.ts @@ -0,0 +1,196 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { shuffle } from 'lodash'; + +import { FullDataSeriesDatum, WithIndex } from '../../chart_types/xy_chart/utils/fit_function'; +import { DataSeries, DataSeriesDatum, XYChartSeriesIdentifier } from '../../chart_types/xy_chart/utils/series'; +import { SeriesType } from '../../specs'; +import { mergePartial } from '../../utils/common'; +import { MockSeriesSpec } from '../specs'; +import { getRandomNumberGenerator } from '../utils'; +import { fitFunctionData } from './data'; + +const rng = getRandomNumberGenerator(); + +interface DomainRange { + min?: number; + max?: number; + fractionDigits?: number; + inclusive?: boolean; +} + +/** @internal */ +export class MockDataSeries { + private static readonly base: DataSeries = { + specId: 'spec1', + seriesKeys: ['spec1'], + yAccessor: 'y', + splitAccessors: new Map(), + key: 'spec1', + data: [], + groupId: 'group1', + seriesType: SeriesType.Bar, + stackMode: undefined, + spec: MockSeriesSpec.bar(), + isStacked: false, + insertIndex: 0, + isFiltered: false, + }; + + static default(partial?: Partial) { + return mergePartial(MockDataSeries.base, partial, { mergeOptionalPartialValues: true }); + } + + static fitFunction( + // eslint-disable-next-line unicorn/no-object-as-default-parameter + options: { shuffle?: boolean; ordinal?: boolean } = { shuffle: true, ordinal: false }, + ): DataSeries { + const ordinalData = options.ordinal + ? fitFunctionData.map((d) => ({ ...d, x: String.fromCharCode(97 + (d.x as number)) })) + : fitFunctionData; + const data = options.shuffle && !options.ordinal ? shuffle(ordinalData) : ordinalData; + + return { + ...MockDataSeries.base, + data, + }; + } + + static fromData(data: DataSeries['data'], seriesIdentifier?: Partial): DataSeries { + return { + ...MockDataSeries.base, + ...seriesIdentifier, + data, + }; + } + + static random( + options: { count?: number; x?: DomainRange; y?: DomainRange; mark?: DomainRange }, + includeMarks = false, + ): DataSeries { + const data = new Array(options?.count ?? 10).fill(0).map(() => MockDataSeriesDatum.random(options, includeMarks)); + return { + ...MockDataSeries.base, + data, + }; + } + + static empty(): DataSeries[] { + return []; + } +} + +/** @internal */ +export class MockDataSeriesDatum { + private static readonly base: DataSeriesDatum = { + x: 1, + y1: 1, + y0: null, + mark: null, + initialY1: null, + initialY0: null, + datum: undefined, + }; + + static default(partial?: Partial): DataSeriesDatum { + const merged = mergePartial(MockDataSeriesDatum.base, partial, { + mergeOptionalPartialValues: true, + }); + if (merged.initialY1 === null) { + merged.initialY1 = merged.y1; + } + + if (merged.initialY0 === null) { + merged.initialY0 = merged.y0; + } + return merged; + } + + /** + * Fill datum with minimal values, default missing required values to `null` + */ + static simple({ + x, + y1 = null, + y0 = null, + mark = null, + filled, + }: Partial & Pick): DataSeriesDatum { + return { + x, + y1, + y0, + mark, + initialY1: y1, + initialY0: y0, + datum: { + x, + y1, + y0, + }, + ...(filled && filled), + }; + } + + /** + * returns "full" datum with minimal values, default missing required values to `null` + * + * "full" - means x and y1 values are `non-nullable` + */ + static full({ + fittingIndex = 0, + ...datum + }: Partial> & + Pick, 'x' | 'y1'>): WithIndex { + return { + ...(MockDataSeriesDatum.simple(datum) as WithIndex), + fittingIndex, + }; + } + + static ordinal(partial?: Partial): DataSeriesDatum { + return mergePartial( + { + ...MockDataSeriesDatum.base, + x: 'a', + }, + partial, + { mergeOptionalPartialValues: true }, + ); + } + + /** + * Psuedo-random values between a specified domain + * + * @param options + */ + static random( + options: { x?: DomainRange; y?: DomainRange; mark?: DomainRange }, + includeMark = false, + ): DataSeriesDatum { + return MockDataSeriesDatum.simple({ + x: rng(options?.x?.min, options?.x?.max, options.x?.fractionDigits, options.x?.inclusive), + y1: rng(options?.y?.min, options?.y?.max, options.y?.fractionDigits, options.y?.inclusive), + ...(includeMark && { + mark: rng(options?.mark?.min, options?.mark?.max, options.mark?.fractionDigits, options.mark?.inclusive), + }), + }); + } +} diff --git a/packages/osd-charts/src/mocks/series/series_identifiers.ts b/packages/osd-charts/src/mocks/series/series_identifiers.ts new file mode 100644 index 000000000000..d8103aa8d3c4 --- /dev/null +++ b/packages/osd-charts/src/mocks/series/series_identifiers.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getDataSeriesFromSpecs, XYChartSeriesIdentifier } from '../../chart_types/xy_chart/utils/series'; +import { BasicSeriesSpec } from '../../specs'; +import { mergePartial } from '../../utils/common'; + +/** @internal */ +export class MockSeriesIdentifier { + private static readonly base: XYChartSeriesIdentifier = { + specId: 'bars', + yAccessor: 'y', + seriesKeys: ['a'], + splitAccessors: new Map().set('g', 'a'), + key: 'spec{bars}yAccessor{y}splitAccessors{g-a}', + smHorizontalAccessorValue: undefined, + smVerticalAccessorValue: undefined, + }; + + static default(partial?: Partial) { + return mergePartial(MockSeriesIdentifier.base, partial, { + mergeOptionalPartialValues: true, + }); + } + + static fromSpecs(specs: BasicSeriesSpec[]): XYChartSeriesIdentifier[] { + const { dataSeries } = getDataSeriesFromSpecs(specs); + + return dataSeries.map( + ({ groupId, seriesType, data, isStacked, stackMode, spec, insertIndex, isFiltered, ...rest }) => rest, + ); + } + + static fromSpec(specs: BasicSeriesSpec): XYChartSeriesIdentifier { + return MockSeriesIdentifier.fromSpecs([specs])[0]; + } +} diff --git a/packages/osd-charts/src/mocks/series/utils.ts b/packages/osd-charts/src/mocks/series/utils.ts new file mode 100644 index 000000000000..1300d548e64c --- /dev/null +++ b/packages/osd-charts/src/mocks/series/utils.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getYDatumValueFn } from '../../chart_types/xy_chart/rendering/utils'; +import { DataSeriesDatum } from '../../chart_types/xy_chart/utils/series'; + +/** + * Helper function to return array of rendered y1 values + * @internal + */ +export const getFilledNullData = (data: DataSeriesDatum[]): (number | undefined)[] => + data.filter(({ filled }) => filled?.y1 !== undefined).map(({ filled }) => filled?.y1); + +/** + * Helper function to return array of rendered y1 values + * @internal + */ +export const getFilledNonNullData = (data: DataSeriesDatum[]): (number | undefined)[] => + data.filter(({ y1, filled }) => y1 !== null && filled?.y1 === undefined).map(({ filled }) => filled?.y1); + +/** + * Helper function to return array of rendered x values + * @internal + */ +export const getXValueData = (data: DataSeriesDatum[]): (number | string)[] => data.map(({ x }) => x); + +/** + * Returns value of `y1` or `filled.y1` or null + * @internal + */ +export const getYResolvedData = (data: DataSeriesDatum[]): (number | null)[] => { + const datumAccessor = getYDatumValueFn(); + return data.map((d) => { + return datumAccessor(d); + }); +}; diff --git a/packages/osd-charts/src/mocks/specs/index.ts b/packages/osd-charts/src/mocks/specs/index.ts new file mode 100644 index 000000000000..dc17aa2d8794 --- /dev/null +++ b/packages/osd-charts/src/mocks/specs/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export * from './specs'; diff --git a/packages/osd-charts/src/mocks/specs/specs.ts b/packages/osd-charts/src/mocks/specs/specs.ts new file mode 100644 index 000000000000..6aae423b601d --- /dev/null +++ b/packages/osd-charts/src/mocks/specs/specs.ts @@ -0,0 +1,373 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartType } from '../../chart_types'; +import { config, percentFormatter } from '../../chart_types/partition_chart/layout/config'; +import { PartitionLayout } from '../../chart_types/partition_chart/layout/types/config_types'; +import { ShapeTreeNode } from '../../chart_types/partition_chart/layout/types/viewmodel_types'; +import { AGGREGATE_KEY, PrimitiveValue } from '../../chart_types/partition_chart/layout/utils/group_by_rollup'; +import { PartitionSpec } from '../../chart_types/partition_chart/specs'; +import { + SeriesSpecs, + DEFAULT_GLOBAL_ID, + BarSeriesSpec, + AreaSeriesSpec, + HistogramModeAlignments, + HistogramBarSeriesSpec, + LineSeriesSpec, + BasicSeriesSpec, + SeriesType, + BubbleSeriesSpec, + LineAnnotationSpec, + RectAnnotationSpec, + AnnotationType, + AnnotationDomainType, + AxisSpec, +} from '../../chart_types/xy_chart/utils/specs'; +import { Predicate } from '../../common/predicate'; +import { ScaleType } from '../../scales/constants'; +import { SettingsSpec, SpecType, DEFAULT_SETTINGS_SPEC, SmallMultiplesSpec, GroupBySpec, Spec } from '../../specs'; +import { Datum, mergePartial, Position, RecursivePartial } from '../../utils/common'; +import { LIGHT_THEME } from '../../utils/themes/light_theme'; + +/** @internal */ +export class MockSeriesSpec { + private static readonly barBase: BarSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'spec1', + seriesType: SeriesType.Bar, + groupId: DEFAULT_GLOBAL_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + hideInLegend: false, + enableHistogramMode: false, + data: [] as any[], + }; + + private static readonly histogramBarBase: HistogramBarSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'spec1', + seriesType: SeriesType.Bar, + groupId: DEFAULT_GLOBAL_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + hideInLegend: false, + enableHistogramMode: true, + data: [], + }; + + private static readonly areaBase: AreaSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'spec1', + seriesType: SeriesType.Area, + groupId: DEFAULT_GLOBAL_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + hideInLegend: false, + histogramModeAlignment: HistogramModeAlignments.Center, + data: [], + }; + + private static readonly lineBase: LineSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'spec1', + seriesType: SeriesType.Line, + groupId: DEFAULT_GLOBAL_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + hideInLegend: false, + histogramModeAlignment: HistogramModeAlignments.Center, + data: [], + }; + + private static readonly bubbleBase: BubbleSeriesSpec = { + chartType: ChartType.XYAxis, + specType: SpecType.Series, + id: 'spec1', + seriesType: SeriesType.Bubble, + groupId: DEFAULT_GLOBAL_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + hideInLegend: false, + data: [], + }; + + private static readonly sunburstBase: PartitionSpec = { + chartType: ChartType.Partition, + specType: SpecType.Series, + id: 'spec1', + config: { + ...config, + partitionLayout: PartitionLayout.sunburst, + }, + valueAccessor: (d: Datum) => (typeof d === 'number' ? d : 0), + valueGetter: (n: ShapeTreeNode): number => n[AGGREGATE_KEY], + valueFormatter: (d: number): string => String(d), + percentFormatter, + topGroove: 0, + smallMultiples: null, + layers: [ + { + groupByRollup: (d: Datum, i: number) => i, + nodeLabel: (d: PrimitiveValue) => String(d), + showAccessor: () => true, + fillLabel: {}, + }, + ], + data: [], + }; + + private static readonly treemapBase: PartitionSpec = { + chartType: ChartType.Partition, + specType: SpecType.Series, + id: 'spec1', + config: { + ...config, + partitionLayout: PartitionLayout.treemap, + }, + valueAccessor: (d: Datum) => (typeof d === 'number' ? d : 0), + valueGetter: (n: ShapeTreeNode): number => n[AGGREGATE_KEY], + valueFormatter: (d: number): string => String(d), + percentFormatter, + topGroove: 20, + smallMultiples: null, + layers: [ + { + groupByRollup: (d: Datum, i: number) => i, + nodeLabel: (d: PrimitiveValue) => String(d), + showAccessor: () => true, + fillLabel: {}, + }, + ], + data: [], + }; + + static bar(partial?: Partial): BarSeriesSpec { + return mergePartial(MockSeriesSpec.barBase, partial as RecursivePartial, { + mergeOptionalPartialValues: true, + }); + } + + static histogramBar(partial?: Partial): HistogramBarSeriesSpec { + return mergePartial( + MockSeriesSpec.histogramBarBase, + partial as RecursivePartial, + { + mergeOptionalPartialValues: true, + }, + ); + } + + static area(partial?: Partial): AreaSeriesSpec { + return mergePartial(MockSeriesSpec.areaBase, partial as RecursivePartial, { + mergeOptionalPartialValues: true, + }); + } + + static line(partial?: Partial): LineSeriesSpec { + return mergePartial(MockSeriesSpec.lineBase, partial as RecursivePartial, { + mergeOptionalPartialValues: true, + }); + } + + static bubble(partial?: Partial): BubbleSeriesSpec { + return mergePartial(MockSeriesSpec.bubbleBase, partial as RecursivePartial, { + mergeOptionalPartialValues: true, + }); + } + + static sunburst(partial?: Partial): PartitionSpec { + return mergePartial(MockSeriesSpec.sunburstBase, partial as RecursivePartial, { + mergeOptionalPartialValues: true, + }); + } + + static treemap(partial?: Partial): PartitionSpec { + return mergePartial(MockSeriesSpec.treemapBase, partial as RecursivePartial, { + mergeOptionalPartialValues: true, + }); + } + + static byType(type?: SeriesType | 'histogram'): BasicSeriesSpec { + switch (type) { + case SeriesType.Line: + return MockSeriesSpec.lineBase; + case SeriesType.Area: + return MockSeriesSpec.areaBase; + case SeriesType.Bubble: + return MockSeriesSpec.bubbleBase; + case 'histogram': + return MockSeriesSpec.histogramBarBase; + case SeriesType.Bar: + default: + return MockSeriesSpec.barBase; + } + } + + static byTypePartial(type?: 'line' | 'bar' | 'area' | 'histogram') { + switch (type) { + case 'line': + return MockSeriesSpec.line; + case 'area': + return MockSeriesSpec.area; + case 'histogram': + return MockSeriesSpec.histogramBar; + case 'bar': + default: + return MockSeriesSpec.bar; + } + } +} + +/** @internal */ +export class MockSeriesSpecs { + static fromSpecs(specs: BasicSeriesSpec[]): SeriesSpecs { + return specs; + } + + static fromPartialSpecs(specs: Partial[]): SeriesSpecs { + return specs.map(({ seriesType, ...spec }) => { + const base = MockSeriesSpec.byType(seriesType); + return mergePartial(base, spec as RecursivePartial, { + mergeOptionalPartialValues: true, + }); + }); + } + + static empty(): SeriesSpecs { + return []; + } +} + +/** @internal */ +export class MockGlobalSpec { + private static readonly settingsBase: SettingsSpec = DEFAULT_SETTINGS_SPEC; + + private static readonly axisBase: AxisSpec = { + id: 'yAxis', + chartType: ChartType.XYAxis, + specType: SpecType.Axis, + groupId: DEFAULT_GLOBAL_ID, + hide: false, + showOverlappingTicks: false, + showOverlappingLabels: false, + position: Position.Left, + }; + + private static readonly settingsBaseNoMargings: SettingsSpec = { + ...MockGlobalSpec.settingsBase, + theme: { + ...LIGHT_THEME, + chartMargins: { top: 0, left: 0, right: 0, bottom: 0 }, + chartPaddings: { top: 0, left: 0, right: 0, bottom: 0 }, + scales: { + barsPadding: 0, + histogramPadding: 0, + }, + }, + }; + + private static readonly smallMultipleBase: SmallMultiplesSpec = { + id: 'smallMultiple', + chartType: ChartType.Global, + specType: SpecType.SmallMultiples, + style: { + verticalPanelPadding: { outer: 0, inner: 0 }, + horizontalPanelPadding: { outer: 0, inner: 0 }, + }, + }; + + private static readonly groupByBase: GroupBySpec = { + id: 'groupBy', + chartType: ChartType.Global, + specType: SpecType.IndexOrder, + by: ({ id }: Spec) => id, + sort: Predicate.DataIndex, + }; + + static settings(partial?: Partial): SettingsSpec { + return mergePartial(MockGlobalSpec.settingsBase, partial, { mergeOptionalPartialValues: true }); + } + + static settingsNoMargins(partial?: Partial): SettingsSpec { + return mergePartial(MockGlobalSpec.settingsBaseNoMargings, partial, { + mergeOptionalPartialValues: true, + }); + } + + static axis(partial?: Partial): AxisSpec { + return mergePartial(MockGlobalSpec.axisBase, partial, { mergeOptionalPartialValues: true }); + } + + static smallMultiple(partial?: Partial): SmallMultiplesSpec { + return mergePartial(MockGlobalSpec.smallMultipleBase, partial, { + mergeOptionalPartialValues: true, + }); + } + + static groupBy(partial?: Partial): GroupBySpec { + return mergePartial(MockGlobalSpec.groupByBase, partial, { + mergeOptionalPartialValues: true, + }); + } +} + +/** @internal */ +export class MockAnnotationSpec { + private static readonly lineBase: LineAnnotationSpec = { + id: 'line_annotation_1', + groupId: DEFAULT_GLOBAL_ID, + chartType: ChartType.XYAxis, + specType: SpecType.Annotation, + annotationType: AnnotationType.Line, + dataValues: [], + domainType: AnnotationDomainType.XDomain, + }; + + private static readonly rectBase: RectAnnotationSpec = { + id: 'rect_annotation_1', + groupId: DEFAULT_GLOBAL_ID, + chartType: ChartType.XYAxis, + specType: SpecType.Annotation, + annotationType: AnnotationType.Rectangle, + dataValues: [], + }; + + static line(partial?: Partial): LineAnnotationSpec { + return mergePartial(MockAnnotationSpec.lineBase, partial, { mergeOptionalPartialValues: true }); + } + + static rect(partial?: Partial): RectAnnotationSpec { + return mergePartial(MockAnnotationSpec.rectBase, partial, { mergeOptionalPartialValues: true }); + } +} diff --git a/packages/osd-charts/src/mocks/store/index.ts b/packages/osd-charts/src/mocks/store/index.ts new file mode 100644 index 000000000000..80ea22c76c6c --- /dev/null +++ b/packages/osd-charts/src/mocks/store/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export * from './store'; diff --git a/packages/osd-charts/src/mocks/store/store.ts b/packages/osd-charts/src/mocks/store/store.ts new file mode 100644 index 000000000000..8cecad3d15a1 --- /dev/null +++ b/packages/osd-charts/src/mocks/store/store.ts @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createStore, Store } from 'redux'; + +import { DEFAULT_SETTINGS_SPEC, Spec } from '../../specs'; +import { updateParentDimensions } from '../../state/actions/chart_settings'; +import { upsertSpec, specParsed } from '../../state/actions/specs'; +import { chartStoreReducer, GlobalChartState } from '../../state/chart_state'; + +/** @internal */ +export class MockStore { + static default( + { width, height, top, left } = { width: 100, height: 100, top: 0, left: 0 }, + chartId = 'chartId', + ): Store { + const storeReducer = chartStoreReducer(chartId); + const store = createStore(storeReducer); + store.dispatch(updateParentDimensions({ width, height, top, left })); + return store; + } + + static addSpecs(specs: Spec | Array, store: Store) { + if (Array.isArray(specs)) { + const actions = specs.map(upsertSpec); + actions.forEach(store.dispatch); + if (!specs.some((s) => s.id === DEFAULT_SETTINGS_SPEC.id)) { + store.dispatch(upsertSpec(DEFAULT_SETTINGS_SPEC)); + } + } else { + store.dispatch(upsertSpec(specs)); + if (specs.id !== DEFAULT_SETTINGS_SPEC.id) { + store.dispatch(upsertSpec(DEFAULT_SETTINGS_SPEC)); + } + } + store.dispatch(specParsed()); + } + + static updateDimensions( + { width, height, top, left } = { width: 100, height: 100, top: 0, left: 0 }, + store: Store, + ) { + store.dispatch(updateParentDimensions({ width, height, top, left })); + } +} diff --git a/packages/osd-charts/src/mocks/theme.ts b/packages/osd-charts/src/mocks/theme.ts new file mode 100644 index 000000000000..56be07cbbafd --- /dev/null +++ b/packages/osd-charts/src/mocks/theme.ts @@ -0,0 +1,125 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RecursivePartial, mergePartial } from '../utils/common'; +import { + GeometryStateStyle, + RectBorderStyle, + RectStyle, + AreaStyle, + LineStyle, + PointStyle, +} from '../utils/themes/theme'; + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export class MockStyles { + static rect(partial: RecursivePartial = {}): RectStyle { + return mergePartial( + { + fill: 'blue', + opacity: 1, + }, + partial, + { mergeOptionalPartialValues: true }, + ); + } + + static rectBorder(partial: RecursivePartial = {}): RectBorderStyle { + return mergePartial( + { + visible: false, + stroke: 'blue', + strokeWidth: 1, + strokeOpacity: 1, + }, + partial, + { mergeOptionalPartialValues: true }, + ); + } + + static area(partial: RecursivePartial = {}): AreaStyle { + return mergePartial( + { + visible: true, + fill: 'blue', + opacity: 1, + }, + partial, + { mergeOptionalPartialValues: true }, + ); + } + + static line(partial: RecursivePartial = {}): LineStyle { + return mergePartial( + { + visible: true, + stroke: 'blue', + strokeWidth: 1, + opacity: 1, + dash: [1, 2, 1], + }, + partial, + { mergeOptionalPartialValues: true }, + ); + } + + static point(partial: RecursivePartial = {}): PointStyle { + return mergePartial( + { + visible: true, + stroke: 'blue', + strokeWidth: 1, + fill: 'blue', + opacity: 1, + radius: 10, + }, + partial, + { mergeOptionalPartialValues: true }, + ); + } + + static geometryState(partial: RecursivePartial = {}): GeometryStateStyle { + return mergePartial( + { + opacity: 1, + }, + partial, + { mergeOptionalPartialValues: true }, + ); + } +} diff --git a/packages/osd-charts/src/mocks/utils.ts b/packages/osd-charts/src/mocks/utils.ts new file mode 100644 index 000000000000..e1e36cae224c --- /dev/null +++ b/packages/osd-charts/src/mocks/utils.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import seedrandom from 'seedrandom'; + +import { DataGenerator, RandomNumberGenerator } from '../utils/data_generators/data_generator'; + +/** + * Forces object to be partial type for mocking tests + * + * SHOULD NOT BE USED OUTSIDE OF TESTS!!! + * + * @param obj partial object type + * @internal + */ +export const forcedType = >(obj: Partial): T => obj as T; + +/** + * Returns rng seed from `process.env` + * @internal + */ +export const getRNGSeed = (fallback?: string): string | undefined => + process.env.RNG_SEED ?? (process.env.VRT ? 'elastic-charts' : fallback); + +/** + * Returns rng function with optional `min`, `max` and `fractionDigits` params + * + * @param string seed for deterministic algorithm + * @internal + */ +export const getRandomNumberGenerator = (seed = getRNGSeed()): RandomNumberGenerator => { + const rng = seedrandom(seed); + + /** + * Random number generator + * + * @param {} min=0 + * @param {} max=1 + * @param {} fractionDigits=0 + */ + return function randomNumberGenerator(min = 0, max = 1, fractionDigits = 0, inclusive = true) { + const precision = Math.pow(10, Math.max(fractionDigits, 0)); + const scaledMax = max * precision; + const scaledMin = min * precision; + const offset = inclusive ? 1 : 0; + const num = Math.floor(rng() * (scaledMax - scaledMin + offset)) + scaledMin; + + return num / precision; + }; +}; + +/** @internal */ +export class SeededDataGenerator extends DataGenerator { + constructor(frequency = 500) { + super(frequency, getRandomNumberGenerator()); + } +} + +/** + * Returns random array or object value + * @internal + */ +export const getRandomEntryFn = (seed = getRNGSeed()) => { + const rng = seedrandom(seed); + + return function getRandomEntryClosure(entries: T[] | Record) { + if (Array.isArray(entries)) { + const index = Math.floor(rng() * entries.length); + + return entries[index]; + } + + const keys = Object.keys(entries); + const index = Math.floor(rng() * keys.length); + const key = keys[index]; + + return entries[key]; + }; +}; diff --git a/packages/osd-charts/src/mocks/xy/domains.ts b/packages/osd-charts/src/mocks/xy/domains.ts new file mode 100644 index 000000000000..5d3f752d36f1 --- /dev/null +++ b/packages/osd-charts/src/mocks/xy/domains.ts @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { XDomain, YDomain } from '../../chart_types/xy_chart/domains/types'; +import { + getXNiceFromSpec, + getXScaleTypeFromSpec, + getYNiceFromSpec, + getYScaleTypeFromSpec, +} from '../../chart_types/xy_chart/scales/get_api_scales'; +import { X_SCALE_DEFAULT, Y_SCALE_DEFAULT } from '../../chart_types/xy_chart/scales/scale_defaults'; +import { DEFAULT_GLOBAL_ID, XScaleType } from '../../chart_types/xy_chart/utils/specs'; +import { ScaleContinuousType } from '../../scales'; +import { ScaleType } from '../../scales/constants'; +import { mergePartial, RecursivePartial } from '../../utils/common'; + +/** @internal */ +export class MockXDomain { + private static readonly base: XDomain = { + ...X_SCALE_DEFAULT, + isBandScale: X_SCALE_DEFAULT.type !== ScaleType.Ordinal, + minInterval: 0, + timeZone: undefined, + domain: [0, 1], + }; + + static default(partial?: RecursivePartial) { + return mergePartial(MockXDomain.base, partial, { mergeOptionalPartialValues: true }); + } + + static fromScaleType(scaleType: XScaleType, partial?: RecursivePartial) { + return mergePartial(MockXDomain.base, partial, { mergeOptionalPartialValues: true }, [ + { + type: getXScaleTypeFromSpec(scaleType), + nice: getXNiceFromSpec(), + }, + ]); + } +} + +/** @internal */ +export class MockYDomain { + private static readonly base: YDomain = { + ...Y_SCALE_DEFAULT, + isBandScale: false, + groupId: DEFAULT_GLOBAL_ID, + domain: [0, 1], + }; + + static default(partial?: RecursivePartial) { + return mergePartial(MockYDomain.base, partial, { mergeOptionalPartialValues: true }); + } + + static fromScaleType(scaleType: ScaleContinuousType, partial?: RecursivePartial) { + return mergePartial(MockYDomain.base, partial, { mergeOptionalPartialValues: true }, [ + { + type: getYScaleTypeFromSpec(scaleType), + nice: getYNiceFromSpec(), + }, + ]); + } +} diff --git a/packages/osd-charts/src/renderers/canvas/index.ts b/packages/osd-charts/src/renderers/canvas/index.ts new file mode 100644 index 000000000000..25e56cb1b5d4 --- /dev/null +++ b/packages/osd-charts/src/renderers/canvas/index.ts @@ -0,0 +1,127 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Coordinate } from '../../common/geometry'; +import { Rect } from '../../geoms/types'; +import { getRadians } from '../../utils/common'; +import { ClippedRanges } from '../../utils/geometry'; +import { Point } from '../../utils/point'; + +/** + * withContext abstracts out the otherwise error-prone save/restore pairing; it can be nested and/or put into sequence + * The idea is that you just set what's needed for the enclosed snippet, which may temporarily override values in the + * outer withContext. Example: we use a +y = top convention, so when doing text rendering, y has to be flipped (ctx.scale) + * otherwise the text will render upside down. + * @param ctx + * @param fun + * @internal + */ +export function withContext(ctx: CanvasRenderingContext2D, fun: (ctx: CanvasRenderingContext2D) => void) { + ctx.save(); + fun(ctx); + ctx.restore(); +} + +/** @internal */ +export function clearCanvas(ctx: CanvasRenderingContext2D, width: Coordinate, height: Coordinate) { + withContext(ctx, (ctx) => { + ctx.clearRect(-width, -height, 2 * width, 2 * height); // remove past contents + }); +} + +// order of rendering is important; determined by the order of layers in the array +/** @internal */ +export function renderLayers(ctx: CanvasRenderingContext2D, layers: Array<(ctx: CanvasRenderingContext2D) => void>) { + layers.forEach((renderLayer) => renderLayer(ctx)); +} + +/** @internal */ +export function withClip( + ctx: CanvasRenderingContext2D, + clippings: Rect, + fun: (ctx: CanvasRenderingContext2D) => void, + shouldClip = true, +) { + withContext(ctx, (ctx) => { + if (shouldClip) { + const { x, y, width, height } = clippings; + ctx.beginPath(); + ctx.rect(x, y, width, height); + ctx.clip(); + } + withContext(ctx, (ctx) => { + fun(ctx); + }); + }); +} + +/** + * Create clip from a set of clipped ranges + * @internal + */ +export function withClipRanges( + ctx: CanvasRenderingContext2D, + clippedRanges: ClippedRanges, + clippings: Rect, + negate = false, + fun: (ctx: CanvasRenderingContext2D) => void, +) { + withContext(ctx, (context) => { + const { length } = clippedRanges; + const { width, height, y } = clippings; + context.beginPath(); + if (negate) { + clippedRanges.forEach(([x0, x1]) => { + context.rect(x0, y, x1 - x0, height); + }); + } else { + if (length > 0) { + context.rect(0, -0.5, clippedRanges[0][0], height); + const lastX = clippedRanges[length - 1][1]; + context.rect(lastX, y, width - lastX, height); + } + + if (length > 1) { + for (let i = 1; i < length; i++) { + const [, x0] = clippedRanges[i - 1]; + const [x1] = clippedRanges[i]; + context.rect(x0, y, x1 - x0, height); + } + } + } + context.clip(); + fun(context); + }); +} + +/** @internal */ +export function withRotatedOrigin( + ctx: CanvasRenderingContext2D, + origin: Point, + rotation: number = 0, + fn: (ctx: CanvasRenderingContext2D) => void, +) { + withContext(ctx, (ctx) => { + const { x, y } = origin; + ctx.translate(x, y); + ctx.rotate(getRadians(rotation)); + ctx.translate(-x, -y); + fn(ctx); + }); +} diff --git a/packages/osd-charts/src/reset_dark.scss b/packages/osd-charts/src/reset_dark.scss new file mode 100644 index 000000000000..f0319f8d10a0 --- /dev/null +++ b/packages/osd-charts/src/reset_dark.scss @@ -0,0 +1,6 @@ +@import '../../../node_modules/@elastic/eui/src/themes/eui/eui_colors_dark'; +@import 'style/themes/colors_dark'; +@import '../../../node_modules/@elastic/eui/src/global_styling/functions/index'; +@import '../../../node_modules/@elastic/eui/src/global_styling/variables/index'; +@import '../../../node_modules/@elastic/eui/src/global_styling/mixins/index'; +@import '../../../node_modules/@elastic/eui/src/global_styling/reset/index'; diff --git a/packages/osd-charts/src/reset_light.scss b/packages/osd-charts/src/reset_light.scss new file mode 100644 index 000000000000..e3206f48ff63 --- /dev/null +++ b/packages/osd-charts/src/reset_light.scss @@ -0,0 +1,6 @@ +@import '../../../node_modules/@elastic/eui/src/themes/eui/eui_colors_light'; +@import 'style/themes/colors_light'; +@import '../../../node_modules/@elastic/eui/src/global_styling/functions/index'; +@import '../../../node_modules/@elastic/eui/src/global_styling/variables/index'; +@import '../../../node_modules/@elastic/eui/src/global_styling/mixins/index'; +@import '../../../node_modules/@elastic/eui/src/global_styling/reset/index'; diff --git a/packages/osd-charts/src/scales/constants.ts b/packages/osd-charts/src/scales/constants.ts new file mode 100644 index 000000000000..e83a10acb5d7 --- /dev/null +++ b/packages/osd-charts/src/scales/constants.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $Values } from 'utility-types'; + +/** + * The scale type + * @public + */ +export const ScaleType = Object.freeze({ + Linear: 'linear' as const, + Ordinal: 'ordinal' as const, + Log: 'log' as const, + Sqrt: 'sqrt' as const, + Time: 'time' as const, + Quantize: 'quantize' as const, + Quantile: 'quantile' as const, + Threshold: 'threshold' as const, +}); + +/** + * The scale type + * @public + */ +export type ScaleType = $Values; + +/** @internal */ +export const LOG_MIN_ABS_DOMAIN = 1; diff --git a/packages/osd-charts/src/scales/index.ts b/packages/osd-charts/src/scales/index.ts new file mode 100644 index 000000000000..1e9f460236e6 --- /dev/null +++ b/packages/osd-charts/src/scales/index.ts @@ -0,0 +1,82 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PrimitiveValue } from '../chart_types/partition_chart/layout/utils/group_by_rollup'; +import { ScaleType } from './constants'; + +/** @public */ +export type ScaleContinuousType = + | typeof ScaleType.Linear + | typeof ScaleType.Time + | typeof ScaleType.Log + | typeof ScaleType.Sqrt; + +/** @public */ +export type ScaleOrdinalType = typeof ScaleType.Ordinal; + +/** @public */ +export type ScaleBandType = ScaleOrdinalType; + +/** + * A `Scale` interface. A scale can map an input value within a specified domain + * to an output value from a specified range. + * The the value is mapped depending on the `type` (linear, log, sqrt, time, ordinal) + * @internal + */ +export interface Scale { + domain: any[]; + range: number[]; + /** + * Returns the distance between the starts of adjacent bands. + */ + step: number; + ticks: () => any[]; + scale: (value?: PrimitiveValue) => number | null; + scaleOrThrow(value?: PrimitiveValue): number; + pureScale: (value?: PrimitiveValue) => number | null; + invert: (value: number) => any; + invertWithStep: ( + value: number, + data: any[], + ) => { + value: any; + withinBandwidth: boolean; + } | null; + isSingleValue: () => boolean; + /** Check if the passed value is within the scale domain */ + isValueInDomain: (value: any) => boolean; + bandwidth: number; + bandwidthPadding: number; + minInterval: number; + type: ScaleContinuousType | ScaleOrdinalType; + /** + * @todo + * designates unit of scale to compare to other Chart axis + */ + unit?: string; + isInverted: boolean; + barsPadding: number; +} + +/** @internal */ +export { ScaleBand } from './scale_band'; +/** @internal */ +export { ScaleContinuous } from './scale_continuous'; + +export { LogBase, LogScaleOptions } from './scale_continuous'; diff --git a/packages/osd-charts/src/scales/scale_band.test.ts b/packages/osd-charts/src/scales/scale_band.test.ts new file mode 100644 index 000000000000..88402ce3eac2 --- /dev/null +++ b/packages/osd-charts/src/scales/scale_band.test.ts @@ -0,0 +1,149 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ScaleBand } from '.'; + +describe('Scale Band', () => { + it('shall clone domain and range arrays', () => { + const domain = [0, 1, 2, 3]; + const range = [0, 100] as [number, number]; + const scale = new ScaleBand(domain, range); + expect(scale.domain).not.toBe(domain); + expect(scale.range).not.toBe(range); + expect(scale.domain).toEqual(domain); + expect(scale.range).toEqual(range); + }); + it('shall scale a numeric domain', () => { + const domain = [0, 1, 2, 3]; + const range = [0, 100] as [number, number]; + const scale = new ScaleBand(domain, range); + expect(scale.bandwidth).toBe(25); + expect(scale.scale(0)).toBe(0); + expect(scale.scale(1)).toBe(25); + expect(scale.scale(2)).toBe(50); + expect(scale.scale(3)).toBe(75); + }); + it('shall scale a string domain', () => { + const scale = new ScaleBand(['a', 'b', 'c', 'd'], [0, 100]); + expect(scale.bandwidth).toBe(25); + expect(scale.scale('a')).toBe(0); + expect(scale.scale('b')).toBe(25); + expect(scale.scale('c')).toBe(50); + expect(scale.scale('d')).toBe(75); + }); + it('is value within domain', () => { + const scale = new ScaleBand(['a', 'b', 'c', 'd'], [0, 100]); + expect(scale.bandwidth).toBe(25); + expect(scale.isValueInDomain('a')).toBe(true); + expect(scale.isValueInDomain('b')).toBe(true); + expect(scale.isValueInDomain('z')).toBe(false); + expect(scale.isValueInDomain(null)).toBe(false); + }); + it('shall scale a any domain', () => { + const scale = new ScaleBand(['a', 1, null, 'd', undefined], [0, 100]); + expect(scale.bandwidth).toBe(20); + expect(scale.scale('a')).toBe(0); + expect(scale.scale(1)).toBe(20); + expect(scale.scale(null)).toBe(40); + expect(scale.scale('d')).toBe(60); + expect(scale.scale()).toBe(80); + }); + it('shall scale remove domain duplicates', () => { + const scale = new ScaleBand(['a', 'a', 'b', 'c', 'c', 'd'], [0, 100]); + expect(scale.bandwidth).toBe(25); + expect(scale.scale('a')).toBe(0); + expect(scale.scale('b')).toBe(25); + expect(scale.scale('c')).toBe(50); + expect(scale.scale('d')).toBe(75); + }); + it('shall scale a domain with inverted range', () => { + const scale = new ScaleBand(['a', 'b', 'c', 'd'], [100, 0]); + expect(scale.bandwidth).toBe(25); + expect(scale.scale('a')).toBe(75); + expect(scale.scale('b')).toBe(50); + expect(scale.scale('c')).toBe(25); + expect(scale.scale('d')).toBe(0); + }); + it('shall return null for out of domain values', () => { + const scale = new ScaleBand(['a', 'b', 'c', 'd'], [0, 100]); + expect(scale.scale('e')).toBeNull(); + expect(scale.scale(0)).toBeNull(); + expect(scale.scale(null)).toBeNull(); + }); + it('shall scale a numeric domain with padding', () => { + const scale = new ScaleBand([0, 1, 2], [0, 120], undefined, 0.5); + expect(scale.bandwidth).toBe(20); + expect(scale.step).toBe(40); + // an empty 1 step place at the beginning + expect(scale.scale(0)).toBe(10); // padding + expect(scale.scale(1)).toBe(50); // padding + step + expect(scale.scale(2)).toBe(90); + // an empty 1 step place at the end + + const scale2 = new ScaleBand([0, 1, 2, 3], [0, 100], undefined, 0.5); + expect(scale2.bandwidth).toBe(12.5); + expect(scale2.step).toBe(25); + // an empty 1/2 step place at the beginning + expect(scale2.scale(0)).toBe(6.25); + expect(scale2.scale(1)).toBe(31.25); + expect(scale2.scale(2)).toBe(56.25); + expect(scale2.scale(3)).toBe(81.25); + // an empty 1/2 step place at the end + }); + it('shall not scale scale null values', () => { + const scale = new ScaleBand([0, 1, 2], [0, 120], undefined, 0.5); + expect(scale.scale(-1)).toBeNull(); + expect(scale.scale(3)).toBeNull(); + }); + it('shall invert all values in range', () => { + const domain = ['a', 'b', 'c', 'd']; + const minRange = 0; + const maxRange = 100; + const scale = new ScaleBand(domain, [minRange, maxRange]); + expect(scale.invert(0)).toBe('a'); + expect(scale.invert(15)).toBe('a'); + expect(scale.invert(24)).toBe('a'); + expect(scale.invert(24.99999)).toBe('a'); + expect(scale.invert(25)).toBe('b'); + expect(scale.invert(99.99999)).toBe('d'); + expect(scale.invert(100)).toBe('d'); + }); + describe('isSingleValue', () => { + it('should return true for single value scale', () => { + const scale = new ScaleBand(['a'], [0, 100]); + expect(scale.isSingleValue()).toBe(true); + }); + it('should return false for multi value scale', () => { + const scale = new ScaleBand(['a', 'b'], [0, 100]); + expect(scale.isSingleValue()).toBe(false); + }); + }); + + describe('#scaleOrThrow', () => { + const scale = new ScaleBand(['a', 'b'], [0, 100]); + + it('should NOT throw for values in domain', () => { + expect(() => scale.scaleOrThrow('a')).not.toThrow(); + }); + + it('should throw for values not in domain', () => { + expect(() => scale.scaleOrThrow('c')).toThrow(); + }); + }); +}); diff --git a/packages/osd-charts/src/scales/scale_band.ts b/packages/osd-charts/src/scales/scale_band.ts new file mode 100644 index 000000000000..d376a4240cb0 --- /dev/null +++ b/packages/osd-charts/src/scales/scale_band.ts @@ -0,0 +1,157 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { scaleBand, scaleQuantize, ScaleQuantize, ScaleBand as D3ScaleBand } from 'd3-scale'; + +import { Scale, ScaleBandType } from '.'; +import { PrimitiveValue } from '../chart_types/partition_chart/layout/utils/group_by_rollup'; +import { Ratio } from '../common/geometry'; +import { RelativeBandsPadding } from '../specs'; +import { maxValueWithUpperLimit, stringifyNullsUndefined } from '../utils/common'; +import { Range } from '../utils/domain'; +import { ScaleType } from './constants'; + +/** + * Categorical scale + * @internal + */ +export class ScaleBand implements Scale { + readonly bandwidth: number; + + readonly bandwidthPadding: number; + + readonly step: number; + + readonly outerPadding: number; + + readonly innerPadding: number; + + readonly originalBandwidth: number; + + readonly type: ScaleBandType; + + readonly domain: any[]; + + readonly range: number[]; + + readonly isInverted: boolean; + + readonly invertedScale: ScaleQuantize; + + readonly minInterval: number; + + readonly barsPadding: number; + + private readonly d3Scale: D3ScaleBand>; + + constructor( + domain: any[], + range: Range, + overrideBandwidth?: number, + /** + * The proportion of the range that is reserved for blank space between bands + * A number between 0 and 1. + * @defaultValue 0 + */ + barsPadding: Ratio | RelativeBandsPadding = 0, + ) { + this.type = ScaleType.Ordinal; + this.d3Scale = scaleBand>(); + this.d3Scale.domain(domain); + this.d3Scale.range(range); + let safeBarPadding = 0; + if (typeof barsPadding === 'object') { + this.d3Scale.paddingInner(barsPadding.inner); + this.d3Scale.paddingOuter(barsPadding.outer); + this.barsPadding = barsPadding.inner; + } else { + safeBarPadding = maxValueWithUpperLimit(barsPadding, 0, 1); + this.d3Scale.paddingInner(safeBarPadding); + this.barsPadding = safeBarPadding; + this.d3Scale.paddingOuter(safeBarPadding / 2); + } + + this.outerPadding = this.d3Scale.paddingOuter(); + this.innerPadding = this.d3Scale.paddingInner(); + this.bandwidth = this.d3Scale.bandwidth() || 0; + this.originalBandwidth = this.d3Scale.bandwidth() || 0; + this.step = this.d3Scale.step(); + this.domain = this.d3Scale.domain(); + this.range = range.slice(); + if (overrideBandwidth) { + this.bandwidth = overrideBandwidth * (1 - safeBarPadding); + } + this.bandwidthPadding = this.bandwidth; + // TO FIX: we are assuming that it's ordered + this.isInverted = this.domain[0] > this.domain[1]; + this.invertedScale = scaleQuantize().domain(range).range(this.domain); + this.minInterval = 0; + } + + private getScaledValue(value?: PrimitiveValue): number | null { + const scaleValue = this.d3Scale(stringifyNullsUndefined(value)); + + if (scaleValue === undefined || isNaN(scaleValue)) { + return null; + } + + return scaleValue; + } + + scaleOrThrow(value?: PrimitiveValue): number { + const scaleValue = this.scale(value); + + if (scaleValue === null) { + throw new Error(`Unable to scale value: ${scaleValue})`); + } + + return scaleValue; + } + + scale(value?: PrimitiveValue) { + return this.getScaledValue(value); + } + + pureScale(value?: PrimitiveValue) { + return this.getScaledValue(value); + } + + ticks() { + return this.domain; + } + + invert(value: any) { + return this.invertedScale(value); + } + + invertWithStep(value: any) { + return { + value: this.invertedScale(value), + withinBandwidth: true, + }; + } + + isSingleValue() { + return this.domain.length < 2; + } + + isValueInDomain(value: any) { + return this.domain.includes(value); + } +} diff --git a/packages/osd-charts/src/scales/scale_continuous.test.ts b/packages/osd-charts/src/scales/scale_continuous.test.ts new file mode 100644 index 000000000000..e687f6957fd0 --- /dev/null +++ b/packages/osd-charts/src/scales/scale_continuous.test.ts @@ -0,0 +1,572 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DateTime, Settings } from 'luxon'; + +import { ScaleContinuous, ScaleBand } from '.'; +import { computeXScale } from '../chart_types/xy_chart/utils/scales'; +import { MockXDomain } from '../mocks/xy/domains'; +import { ContinuousDomain, Range } from '../utils/domain'; +import { LOG_MIN_ABS_DOMAIN, ScaleType } from './constants'; +import { limitLogScaleDomain } from './scale_continuous'; +import { isLogarithmicScale } from './types'; + +describe('Scale Continuous', () => { + test('shall invert on continuous scale linear', () => { + const domain: ContinuousDomain = [0, 2]; + const minRange = 0; + const maxRange = 100; + const scale = new ScaleContinuous({ type: ScaleType.Linear, domain, range: [minRange, maxRange] }); + expect(scale.invert(0)).toBe(0); + expect(scale.invert(50)).toBe(1); + expect(scale.invert(100)).toBe(2); + }); + test('is value within domain', () => { + const domain: ContinuousDomain = [0, 2]; + const minRange = 0; + const maxRange = 100; + const scale = new ScaleContinuous({ type: ScaleType.Linear, domain, range: [minRange, maxRange] }); + expect(scale.isValueInDomain(0)).toBe(true); + expect(scale.isValueInDomain(2)).toBe(true); + expect(scale.isValueInDomain(-1)).toBe(false); + expect(scale.isValueInDomain(3)).toBe(false); + }); + test('shall invert on continuous scale time', () => { + const startTime = DateTime.fromISO('2019-01-01T00:00:00.000', { zone: 'utc' }); + const midTime = DateTime.fromISO('2019-01-02T00:00:00.000', { zone: 'utc' }); + const endTime = DateTime.fromISO('2019-01-03T00:00:00.000', { zone: 'utc' }); + const domain = [startTime.toMillis(), endTime.toMillis()]; + const minRange = 0; + const maxRange = 100; + const scale = new ScaleContinuous({ type: ScaleType.Time, domain, range: [minRange, maxRange] }); + expect(scale.invert(0)).toBe(startTime.toMillis()); + expect(scale.invert(50)).toBe(midTime.toMillis()); + expect(scale.invert(100)).toBe(endTime.toMillis()); + }); + test('check if a scale is log scale', () => { + const domain: ContinuousDomain = [0, 2]; + const range: Range = [0, 100]; + const scaleLinear = new ScaleContinuous({ type: ScaleType.Linear, domain, range }); + const scaleLog = new ScaleContinuous({ type: ScaleType.Log, domain, range }); + const scaleTime = new ScaleContinuous({ type: ScaleType.Time, domain, range }); + const scaleSqrt = new ScaleContinuous({ type: ScaleType.Sqrt, domain, range }); + const scaleBand = new ScaleBand(domain, range); + expect(isLogarithmicScale(scaleLinear)).toBe(false); + expect(isLogarithmicScale(scaleLog)).toBe(true); + expect(isLogarithmicScale(scaleTime)).toBe(false); + expect(isLogarithmicScale(scaleSqrt)).toBe(false); + expect(isLogarithmicScale(scaleBand)).toBe(false); + }); + test('can get the right x value on linear scale', () => { + const domain: ContinuousDomain = [0, 2]; + const data = [0, 0.5, 0.8, 2]; + const range: Range = [0, 2]; + const scaleLinear = new ScaleContinuous({ type: ScaleType.Linear, domain, range }); + expect(scaleLinear.bandwidth).toBe(0); + expect(scaleLinear.invertWithStep(0, data)).toEqual({ value: 0, withinBandwidth: true }); + expect(scaleLinear.invertWithStep(0.1, data)).toEqual({ value: 0, withinBandwidth: true }); + + expect(scaleLinear.invertWithStep(0.4, data)).toEqual({ value: 0.5, withinBandwidth: true }); + expect(scaleLinear.invertWithStep(0.5, data)).toEqual({ value: 0.5, withinBandwidth: true }); + expect(scaleLinear.invertWithStep(0.6, data)).toEqual({ value: 0.5, withinBandwidth: true }); + + expect(scaleLinear.invertWithStep(0.7, data)).toEqual({ value: 0.8, withinBandwidth: true }); + expect(scaleLinear.invertWithStep(0.8, data)).toEqual({ value: 0.8, withinBandwidth: true }); + expect(scaleLinear.invertWithStep(0.9, data)).toEqual({ value: 0.8, withinBandwidth: true }); + + expect(scaleLinear.invertWithStep(2, data)).toEqual({ value: 2, withinBandwidth: true }); + + expect(scaleLinear.invertWithStep(1.7, data)).toEqual({ value: 2, withinBandwidth: true }); + + expect(scaleLinear.invertWithStep(0.8 + (2 - 0.8) / 2, data)).toEqual({ value: 0.8, withinBandwidth: true }); + expect(scaleLinear.invertWithStep(0.8 + (2 - 0.8) / 2 - 0.01, data)).toEqual({ value: 0.8, withinBandwidth: true }); + + expect(scaleLinear.invertWithStep(0.8 + (2 - 0.8) / 2 + 0.01, data)).toEqual({ value: 2, withinBandwidth: true }); + }); + test('invert with step x value on linear band scale', () => { + const data = [0, 1, 2]; + const xDomain = MockXDomain.fromScaleType(ScaleType.Linear, { + domain: [0, 2], + isBandScale: true, + minInterval: 1, + }); + + const scaleLinear = computeXScale({ xDomain, totalBarsInCluster: 1, range: [0, 119], barsPadding: 0 }); + expect(scaleLinear.bandwidth).toBe(119 / 3); + expect(scaleLinear.invertWithStep(0, data)).toEqual({ value: 0, withinBandwidth: true }); + + expect(scaleLinear.invertWithStep(40, data)).toEqual({ value: 1, withinBandwidth: true }); + expect(scaleLinear.invertWithStep(41, data)).toEqual({ value: 1, withinBandwidth: true }); + expect(scaleLinear.invertWithStep(79, data)).toEqual({ value: 1, withinBandwidth: true }); + + expect(scaleLinear.invertWithStep(80, data)).toEqual({ value: 2, withinBandwidth: true }); + expect(scaleLinear.invertWithStep(81, data)).toEqual({ value: 2, withinBandwidth: true }); + + expect(scaleLinear.invertWithStep(120, data)).toEqual({ value: 3, withinBandwidth: false }); + }); + test('can get the right x value on linear scale with regular band 1', () => { + const domain = [0, 100]; + const data = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]; + + // we tweak the maxRange removing the bandwidth to correctly compute + // a band linear scale in computeXScale + const range: Range = [0, 100 - 10]; + const scaleLinear = new ScaleContinuous( + { type: ScaleType.Linear, domain, range }, + { bandwidth: 10, minInterval: 10 }, + ); + expect(scaleLinear.bandwidth).toBe(10); + expect(scaleLinear.invertWithStep(0, data)).toEqual({ value: 0, withinBandwidth: true }); + expect(scaleLinear.invertWithStep(10, data)).toEqual({ value: 10, withinBandwidth: true }); + expect(scaleLinear.invertWithStep(20, data)).toEqual({ value: 20, withinBandwidth: true }); + expect(scaleLinear.invertWithStep(90, data)).toEqual({ value: 90, withinBandwidth: true }); + }); + test('can get the right x value on linear scale with band', () => { + const data = [0, 10, 20, 50, 90]; + + const xDomain = MockXDomain.fromScaleType(ScaleType.Linear, { + domain: [0, 100], + isBandScale: true, + minInterval: 10, + }); + // we tweak the maxRange removing the bandwidth to correctly compute + // a band linear scale in computeXScale + const scaleLinear = computeXScale({ xDomain, totalBarsInCluster: 1, range: [0, 109], barsPadding: 0 }); + expect(scaleLinear.bandwidth).toBe(109 / 11); + + expect(scaleLinear.invertWithStep(0, data)).toEqual({ value: 0, withinBandwidth: true }); + expect(scaleLinear.invertWithStep(9, data)).toEqual({ value: 0, withinBandwidth: true }); + + expect(scaleLinear.invertWithStep(10, data)).toEqual({ value: 10, withinBandwidth: true }); + expect(scaleLinear.invertWithStep(19, data)).toEqual({ value: 10, withinBandwidth: true }); + + expect(scaleLinear.invertWithStep(20, data)).toEqual({ value: 20, withinBandwidth: true }); + expect(scaleLinear.invertWithStep(29, data)).toEqual({ value: 20, withinBandwidth: true }); + + expect(scaleLinear.invertWithStep(30, data)).toEqual({ value: 30, withinBandwidth: false }); + expect(scaleLinear.invertWithStep(39, data)).toEqual({ value: 30, withinBandwidth: false }); + + expect(scaleLinear.invertWithStep(40, data)).toEqual({ value: 40, withinBandwidth: false }); + + expect(scaleLinear.invertWithStep(50, data)).toEqual({ value: 50, withinBandwidth: true }); + expect(scaleLinear.invertWithStep(59, data)).toEqual({ value: 50, withinBandwidth: true }); + + expect(scaleLinear.invertWithStep(60, data)).toEqual({ value: 60, withinBandwidth: false }); + + expect(scaleLinear.invertWithStep(90, data)).toEqual({ value: 90, withinBandwidth: true }); + + expect(scaleLinear.invertWithStep(100, data)).toEqual({ value: 100, withinBandwidth: false }); + }); + + describe('isSingleValue', () => { + test('should return true for domain with fewer than 2 values', () => { + const scale = new ScaleContinuous({ type: ScaleType.Linear, domain: [], range: [0, 100] }); + expect(scale.isSingleValue()).toBe(true); + }); + test('should return true for domain with equal min and max values', () => { + const scale = new ScaleContinuous({ type: ScaleType.Linear, domain: [1, 1], range: [0, 100] }); + expect(scale.isSingleValue()).toBe(true); + }); + test('should return false for domain with differing min and max values', () => { + const scale = new ScaleContinuous({ type: ScaleType.Linear, domain: [1, 2], range: [0, 100] }); + expect(scale.isSingleValue()).toBe(false); + }); + }); + + describe('xScale values with minInterval and bandwidth', () => { + const domain = [7.053400039672852, 1070.1354763603908]; + + it('should return nice ticks when minInterval & bandwidth are 0', () => { + const scale = new ScaleContinuous( + { + type: ScaleType.Linear, + domain, + range: [0, 100], + }, + { minInterval: 0, bandwidth: 0 }, + ); + expect(scale.ticks()).toEqual([100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]); + }); + }); + + describe('time ticks', () => { + const timezonesToTest = ['Asia/Tokyo', 'Europe/Berlin', 'UTC', 'America/New_York', 'America/Los_Angeles']; + + function getTicksForDomain(domainStart: number, domainEnd: number) { + const scale = new ScaleContinuous( + { type: ScaleType.Time, domain: [domainStart, domainEnd], range: [0, 100] }, + { bandwidth: 0, minInterval: 0, timeZone: Settings.defaultZoneName, integersOnly: false }, + ); + return scale.tickValues; + } + + const currentTz = Settings.defaultZoneName; + + afterEach(() => { + Settings.defaultZoneName = currentTz; + }); + + timezonesToTest.forEach((tz) => { + describe(`standard tests in ${tz}`, () => { + beforeEach(() => { + Settings.defaultZoneName = tz; + }); + + test('should return nice daily ticks', () => { + const ticks = getTicksForDomain( + DateTime.fromISO('2019-04-04T00:00:00.000').toMillis(), + DateTime.fromISO('2019-04-08T00:00:00.000').toMillis(), + ); + + expect(ticks).toEqual([ + DateTime.fromISO('2019-04-04T00:00:00.000').toMillis(), + DateTime.fromISO('2019-04-04T12:00:00.000').toMillis(), + DateTime.fromISO('2019-04-05T00:00:00.000').toMillis(), + DateTime.fromISO('2019-04-05T12:00:00.000').toMillis(), + DateTime.fromISO('2019-04-06T00:00:00.000').toMillis(), + DateTime.fromISO('2019-04-06T12:00:00.000').toMillis(), + DateTime.fromISO('2019-04-07T00:00:00.000').toMillis(), + DateTime.fromISO('2019-04-07T12:00:00.000').toMillis(), + DateTime.fromISO('2019-04-08T00:00:00.000').toMillis(), + ]); + }); + + test('should return nice hourly ticks', () => { + const ticks = getTicksForDomain( + DateTime.fromISO('2019-04-04T00:00:00.000').toMillis(), + DateTime.fromISO('2019-04-04T08:00:00.000').toMillis(), + ); + + expect(ticks).toEqual([ + DateTime.fromISO('2019-04-04T00:00:00.000').toMillis(), + DateTime.fromISO('2019-04-04T01:00:00.000').toMillis(), + DateTime.fromISO('2019-04-04T02:00:00.000').toMillis(), + DateTime.fromISO('2019-04-04T03:00:00.000').toMillis(), + DateTime.fromISO('2019-04-04T04:00:00.000').toMillis(), + DateTime.fromISO('2019-04-04T05:00:00.000').toMillis(), + DateTime.fromISO('2019-04-04T06:00:00.000').toMillis(), + DateTime.fromISO('2019-04-04T07:00:00.000').toMillis(), + DateTime.fromISO('2019-04-04T08:00:00.000').toMillis(), + ]); + }); + + test('should return nice yearly ticks', () => { + const ticks = getTicksForDomain( + DateTime.fromISO('2010-04-04T00:00:00.000').toMillis(), + DateTime.fromISO('2019-04-04T04:00:00.000').toMillis(), + ); + + expect(ticks).toEqual([ + DateTime.fromISO('2011-01-01T00:00:00.000').toMillis(), + DateTime.fromISO('2012-01-01T00:00:00.000').toMillis(), + DateTime.fromISO('2013-01-01T00:00:00.000').toMillis(), + DateTime.fromISO('2014-01-01T00:00:00.000').toMillis(), + DateTime.fromISO('2015-01-01T00:00:00.000').toMillis(), + DateTime.fromISO('2016-01-01T00:00:00.000').toMillis(), + DateTime.fromISO('2017-01-01T00:00:00.000').toMillis(), + DateTime.fromISO('2018-01-01T00:00:00.000').toMillis(), + DateTime.fromISO('2019-01-01T00:00:00.000').toMillis(), + ]); + }); + + test('should return nice yearly ticks from leap year to leap year', () => { + const ticks = getTicksForDomain( + DateTime.fromISO('2016-02-29T00:00:00.000').toMillis(), + DateTime.fromISO('2024-04-29T00:00:00.000').toMillis(), + ); + + expect(ticks).toEqual([ + DateTime.fromISO('2017-01-01T00:00:00.000').toMillis(), + DateTime.fromISO('2018-01-01T00:00:00.000').toMillis(), + DateTime.fromISO('2019-01-01T00:00:00.000').toMillis(), + DateTime.fromISO('2020-01-01T00:00:00.000').toMillis(), + DateTime.fromISO('2021-01-01T00:00:00.000').toMillis(), + DateTime.fromISO('2022-01-01T00:00:00.000').toMillis(), + DateTime.fromISO('2023-01-01T00:00:00.000').toMillis(), + DateTime.fromISO('2024-01-01T00:00:00.000').toMillis(), + ]); + }); + }); + }); + + describe('dst switch', () => { + test('should not leave gaps in hourly ticks on dst switch winter to summer time', () => { + Settings.defaultZoneName = 'Europe/Berlin'; + + const ticks = getTicksForDomain( + DateTime.fromISO('2019-03-31T01:00:00.000').toMillis(), + DateTime.fromISO('2019-03-31T10:00:00.000').toMillis(), + ); + + expect(ticks).toEqual([ + DateTime.fromISO('2019-03-31T01:00:00.000').toMillis(), + DateTime.fromISO('2019-03-31T02:00:00.000').toMillis(), + // 3 AM is missing because it is the same as 2 AM + DateTime.fromISO('2019-03-31T04:00:00.000').toMillis(), + DateTime.fromISO('2019-03-31T05:00:00.000').toMillis(), + DateTime.fromISO('2019-03-31T06:00:00.000').toMillis(), + DateTime.fromISO('2019-03-31T07:00:00.000').toMillis(), + DateTime.fromISO('2019-03-31T08:00:00.000').toMillis(), + DateTime.fromISO('2019-03-31T09:00:00.000').toMillis(), + DateTime.fromISO('2019-03-31T10:00:00.000').toMillis(), + ]); + }); + + test.skip('should not leave gaps in hourly ticks on dst switch summer to winter time', () => { + Settings.defaultZoneName = 'Europe/Berlin'; + + const ticks = getTicksForDomain( + DateTime.fromISO('2019-10-27T01:00:00.000').toMillis(), + DateTime.fromISO('2019-10-27T09:00:00.000').toMillis(), + ); + + expect(ticks).toEqual([ + DateTime.fromISO('2019-10-27T01:00:00.000').toMillis(), + DateTime.fromISO('2019-10-27T02:00:00.000').toMillis(), + // this is the "first" 3 o'clock still in summer time + DateTime.fromISO('2019-10-27T03:00:00.000+02:00').toMillis(), + DateTime.fromISO('2019-10-27T03:00:00.000').toMillis(), + DateTime.fromISO('2019-10-27T04:00:00.000').toMillis(), + DateTime.fromISO('2019-10-27T05:00:00.000').toMillis(), + DateTime.fromISO('2019-10-27T06:00:00.000').toMillis(), + DateTime.fromISO('2019-10-27T07:00:00.000').toMillis(), + DateTime.fromISO('2019-10-27T08:00:00.000').toMillis(), + DateTime.fromISO('2019-10-27T09:00:00.000').toMillis(), + ]); + }); + + test('should set nice daily ticks on dst switch summer to winter time', () => { + Settings.defaultZoneName = 'Europe/Berlin'; + + const ticks = getTicksForDomain( + DateTime.fromISO('2019-10-25T16:00:00.000').toMillis(), + DateTime.fromISO('2019-11-03T08:00:00.000').toMillis(), + ); + + expect(ticks).toEqual([ + DateTime.fromISO('2019-10-26T00:00:00.000').toMillis(), + DateTime.fromISO('2019-10-27T00:00:00.000').toMillis(), + DateTime.fromISO('2019-10-28T00:00:00.000').toMillis(), + DateTime.fromISO('2019-10-29T00:00:00.000').toMillis(), + DateTime.fromISO('2019-10-30T00:00:00.000').toMillis(), + DateTime.fromISO('2019-10-31T00:00:00.000').toMillis(), + DateTime.fromISO('2019-11-01T00:00:00.000').toMillis(), + DateTime.fromISO('2019-11-02T00:00:00.000').toMillis(), + DateTime.fromISO('2019-11-03T00:00:00.000').toMillis(), + ]); + }); + + test('should set nice daily ticks on dst switch winter to summer time', () => { + Settings.defaultZoneName = 'Europe/Berlin'; + + const ticks = getTicksForDomain( + DateTime.fromISO('2019-03-29T16:00:00.000').toMillis(), + DateTime.fromISO('2019-04-07T08:00:00.000').toMillis(), + ); + + expect(ticks).toEqual([ + DateTime.fromISO('2019-03-30T00:00:00.000').toMillis(), + DateTime.fromISO('2019-03-31T00:00:00.000').toMillis(), + DateTime.fromISO('2019-04-01T00:00:00.000').toMillis(), + DateTime.fromISO('2019-04-02T00:00:00.000').toMillis(), + DateTime.fromISO('2019-04-03T00:00:00.000').toMillis(), + DateTime.fromISO('2019-04-04T00:00:00.000').toMillis(), + DateTime.fromISO('2019-04-05T00:00:00.000').toMillis(), + DateTime.fromISO('2019-04-06T00:00:00.000').toMillis(), + DateTime.fromISO('2019-04-07T00:00:00.000').toMillis(), + ]); + }); + + test('should set nice monthly ticks on two dst switches from winter to winter time', () => { + Settings.defaultZoneName = 'Europe/Berlin'; + + const ticks = getTicksForDomain( + DateTime.fromISO('2019-03-29T00:00:00.000').toMillis(), + DateTime.fromISO('2019-11-02T00:00:00.000').toMillis(), + ); + + expect(ticks).toEqual([ + DateTime.fromISO('2019-04-01T00:00:00.000').toMillis(), + DateTime.fromISO('2019-05-01T00:00:00.000').toMillis(), + DateTime.fromISO('2019-06-01T00:00:00.000').toMillis(), + DateTime.fromISO('2019-07-01T00:00:00.000').toMillis(), + DateTime.fromISO('2019-08-01T00:00:00.000').toMillis(), + DateTime.fromISO('2019-09-01T00:00:00.000').toMillis(), + DateTime.fromISO('2019-10-01T00:00:00.000').toMillis(), + DateTime.fromISO('2019-11-01T00:00:00.000').toMillis(), + ]); + }); + + test('should set nice monthly ticks on two dst switches from summer to summer time', () => { + Settings.defaultZoneName = 'Europe/Berlin'; + + const ticks = getTicksForDomain( + DateTime.fromISO('2018-10-26T00:00:00.000').toMillis(), + DateTime.fromISO('2019-03-31T20:00:00.000').toMillis(), + ); + + expect(ticks).toEqual([ + DateTime.fromISO('2018-11-01T00:00:00.000').toMillis(), + DateTime.fromISO('2018-12-01T00:00:00.000').toMillis(), + DateTime.fromISO('2019-01-01T00:00:00.000').toMillis(), + DateTime.fromISO('2019-02-01T00:00:00.000').toMillis(), + DateTime.fromISO('2019-03-01T00:00:00.000').toMillis(), + ]); + }); + }); + }); + + describe('Domain pixel padding', () => { + const scaleOptions = Object.freeze({ + type: ScaleType.Linear, + range: [0, 100] as Range, + domain: [10, 60], + }); + + it('should add pixel padding to domain', () => { + const scale = new ScaleContinuous(scaleOptions, { domainPixelPadding: 10 }); + expect(scale.domain).toEqual([3.75, 66.25]); + }); + + it('should handle inverted domain pixel padding', () => { + const scale = new ScaleContinuous({ ...scaleOptions, domain: [60, 10] }, { domainPixelPadding: 10 }); + expect(scale.domain).toEqual([66.25, 3.75]); + }); + + it('should handle negative domain pixel padding', () => { + const scale = new ScaleContinuous({ ...scaleOptions, domain: [-60, -20] }, { domainPixelPadding: 10 }); + expect(scale.domain).toEqual([-65, -15]); + }); + + it('should handle negative inverted domain pixel padding', () => { + const scale = new ScaleContinuous({ ...scaleOptions, domain: [-20, -60] }, { domainPixelPadding: 10 }); + expect(scale.domain).toEqual([-15, -65]); + }); + + it('should constrain pixel padding to zero', () => { + const scale = new ScaleContinuous(scaleOptions, { domainPixelPadding: 20 }); + expect(scale.domain).toEqual([0, 75]); + }); + + it('should not constrain pixel padding to zero', () => { + const scale = new ScaleContinuous(scaleOptions, { domainPixelPadding: 18, constrainDomainPadding: false }); + expect(scale.domain).toEqual([-4.0625, 74.0625]); + }); + + it('should nice domain after pixel padding is applied', () => { + const scale = new ScaleContinuous( + { ...scaleOptions, nice: true }, + { domainPixelPadding: 18, constrainDomainPadding: false }, + ); + expect(scale.domain).toEqual([-10, 80]); + }); + + it('should not handle pixel padding when pixel is greater than half the total range', () => { + const criticalPadding = Math.abs(scaleOptions.range[0] - scaleOptions.range[1]) / 2; + const scale = new ScaleContinuous(scaleOptions, { domainPixelPadding: criticalPadding }); + expect(scale.domain).toEqual(scaleOptions.domain); + }); + }); + + describe('ticks as integers or floats', () => { + const domain: ContinuousDomain = [0, 7]; + const minRange = 0; + const maxRange = 100; + let scale: ScaleContinuous; + + beforeEach(() => { + scale = new ScaleContinuous({ type: ScaleType.Linear, domain, range: [minRange, maxRange] }); + }); + test('should return only integer ticks', () => { + expect(scale.getTicks(10, true)).toEqual([0, 1, 2, 3, 4, 5, 6, 7]); + }); + test('should return normal ticks', () => { + expect(scale.getTicks(10, false)).toEqual([0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7]); + }); + }); + + describe('#scaleOrThrow', () => { + const scale = new ScaleContinuous({ type: ScaleType.Linear, domain: [0, 100], range: [0, 100] }); + + it('should NOT throw for values in domain', () => { + expect(() => scale.scaleOrThrow(10)).not.toThrow(); + }); + + it('should throw for NaN values', () => { + // @ts-ignore + jest.spyOn(scale, 'd3Scale').mockImplementationOnce(() => NaN); + expect(() => scale.scaleOrThrow(1)).toThrow(); + }); + + it('should throw for string values', () => { + expect(() => scale.scaleOrThrow('c')).toThrow(); + }); + + it('should throw for null values', () => { + expect(() => scale.scaleOrThrow(null)).toThrow(); + }); + + it('should throw for undefined values', () => { + expect(() => scale.scaleOrThrow()).toThrow(); + }); + }); + + describe('#limitLogScaleDomain', () => { + const LIMIT = 2; + const ZERO_LIMIT = 0; + + test.each` + domain | logMinLimit | expectedDomain + ${[0, 10]} | ${undefined} | ${[LOG_MIN_ABS_DOMAIN, 10]} + ${[0, 10]} | ${ZERO_LIMIT} | ${[LOG_MIN_ABS_DOMAIN, 10]} + ${[0, -10]} | ${undefined} | ${[-LOG_MIN_ABS_DOMAIN, -10]} + ${[0, -10]} | ${ZERO_LIMIT} | ${[-LOG_MIN_ABS_DOMAIN, -10]} + ${[0, 10]} | ${LIMIT} | ${[LIMIT, 10]} + ${[0, -10]} | ${LIMIT} | ${[-LIMIT, -10]} + ${[10, 0]} | ${undefined} | ${[10, LOG_MIN_ABS_DOMAIN]} + ${[10, 0]} | ${ZERO_LIMIT} | ${[10, LOG_MIN_ABS_DOMAIN]} + ${[-10, 0]} | ${undefined} | ${[-10, -LOG_MIN_ABS_DOMAIN]} + ${[-10, 0]} | ${ZERO_LIMIT} | ${[-10, -LOG_MIN_ABS_DOMAIN]} + ${[10, 0]} | ${LIMIT} | ${[10, LIMIT]} + ${[-10, 0]} | ${LIMIT} | ${[-10, -LIMIT]} + ${[0, 0]} | ${undefined} | ${[LOG_MIN_ABS_DOMAIN, LOG_MIN_ABS_DOMAIN]} + ${[0, 0]} | ${ZERO_LIMIT} | ${[LOG_MIN_ABS_DOMAIN, LOG_MIN_ABS_DOMAIN]} + ${[0, 0]} | ${LIMIT} | ${[LIMIT, LIMIT]} + ${[-10, 10]} | ${undefined} | ${[1, 10]} + ${[-10, 10]} | ${ZERO_LIMIT} | ${[1, 10]} + ${[-10, 10]} | ${LIMIT} | ${[LIMIT, 10]} + ${[10, -10]} | ${undefined} | ${[10, 1]} + ${[10, -10]} | ${ZERO_LIMIT} | ${[10, 1]} + ${[10, -10]} | ${LIMIT} | ${[10, LIMIT]} + ${[10, 100]} | ${undefined} | ${[10, 100]} + ${[10, 100]} | ${ZERO_LIMIT} | ${[10, 100]} + ${[10, 100]} | ${LIMIT} | ${[10, 100]} + ${[LIMIT + 1, 100]} | ${LIMIT} | ${[LIMIT + 1, 100]} + ${[0.1, 100]} | ${LIMIT} | ${[LIMIT, 100]} + ${[0.1, 0.12]} | ${LIMIT} | ${[LIMIT, LIMIT]} + ${[-100, -0.1]} | ${LIMIT} | ${[-100, -LIMIT]} + ${[-0.12, -0.1]} | ${LIMIT} | ${[-LIMIT, -LIMIT]} + `( + 'should limit $domain with limit of $logMinLimit to $expectedDomain', + ({ domain, logMinLimit, expectedDomain }) => { + expect(limitLogScaleDomain(domain, logMinLimit)).toEqual(expectedDomain); + }, + ); + }); +}); diff --git a/packages/osd-charts/src/scales/scale_continuous.ts b/packages/osd-charts/src/scales/scale_continuous.ts new file mode 100644 index 000000000000..6b19770dfbe4 --- /dev/null +++ b/packages/osd-charts/src/scales/scale_continuous.ts @@ -0,0 +1,566 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { bisectLeft } from 'd3-array'; +import { + scaleLinear, + scaleLog, + scaleSqrt, + scaleUtc, + ScaleLinear, + ScaleLogarithmic, + ScalePower, + ScaleTime, + ScaleContinuousNumeric, +} from 'd3-scale'; +import { $Values, Required } from 'utility-types'; + +import { ScaleContinuousType, Scale } from '.'; +import { PrimitiveValue } from '../chart_types/partition_chart/layout/utils/group_by_rollup'; +import { screenspaceMarkerScaleCompressor } from '../solvers/screenspace_marker_scale_compressor'; +import { maxValueWithUpperLimit, mergePartial } from '../utils/common'; +import { getMomentWithTz } from '../utils/data/date_time'; +import { ContinuousDomain, Range } from '../utils/domain'; +import { LOG_MIN_ABS_DOMAIN, ScaleType } from './constants'; + +/** + * d3 scales excluding time scale + */ +type D3ScaleNonTime = ScaleLinear | ScaleLogarithmic | ScalePower; + +/** + * All possible d3 scales + */ +type D3Scale = D3ScaleNonTime | ScaleTime; + +const SCALES = { + [ScaleType.Linear]: scaleLinear, + [ScaleType.Log]: scaleLog, + [ScaleType.Sqrt]: scaleSqrt, + [ScaleType.Time]: scaleUtc, +}; + +const isUnitRange = ([r1, r2]: Range) => r1 === 0 && r2 === 1; + +/** + * As log(0) = -Infinite, a log scale domain must be strictly-positive + * or strictly-negative; the domain must not include or cross zero value. + * We need to limit the domain scale to the right value on all possible cases. + * + * @param domain the domain to limit + * @internal + */ +export function limitLogScaleDomain([min, max]: ContinuousDomain, logMinLimit?: number) { + const absLimit = logMinLimit !== undefined ? Math.abs(logMinLimit) : undefined; + if (absLimit !== undefined && absLimit > 0) { + if (min > 0 && min < absLimit) { + if (max > absLimit) { + return [absLimit, max]; + } + return [absLimit, absLimit]; + } + + if (max < 0 && max > -absLimit) { + if (min < -absLimit) { + return [min, -absLimit]; + } + return [-absLimit, -absLimit]; + } + } + + const fallbackLimit = absLimit || LOG_MIN_ABS_DOMAIN; + + if (min === 0) { + if (max > 0) { + return [fallbackLimit, max]; + } + if (max < 0) { + return [-fallbackLimit, max]; + } + return [fallbackLimit, fallbackLimit]; + } + if (max === 0) { + if (min > 0) { + return [min, fallbackLimit]; + } + if (min < 0) { + return [min, -fallbackLimit]; + } + return [fallbackLimit, fallbackLimit]; + } + if (min < 0 && max > 0) { + const isD0Min = Math.abs(max) - Math.abs(min) >= 0; + if (isD0Min) { + return [fallbackLimit, max]; + } + return [min, -fallbackLimit]; + } + if (min > 0 && max < 0) { + const isD0Max = Math.abs(min) - Math.abs(max) >= 0; + if (isD0Max) { + return [min, fallbackLimit]; + } + return [-fallbackLimit, max]; + } + return [min, max]; +} + +function getPixelPaddedDomain( + chartHeight: number, + domain: [number, number], + desiredPixelPadding: number, + constrainDomainPadding?: boolean, + intercept = 0, +) { + const inverted = domain[1] < domain[0]; + const orderedDomain: [number, number] = inverted ? (domain.slice().reverse() as [number, number]) : domain; + const { scaleMultiplier } = screenspaceMarkerScaleCompressor( + orderedDomain, + [2 * desiredPixelPadding, 2 * desiredPixelPadding], + chartHeight, + ); + let paddedDomainLo = orderedDomain[0] - desiredPixelPadding / scaleMultiplier; + let paddedDomainHigh = orderedDomain[1] + desiredPixelPadding / scaleMultiplier; + + if (constrainDomainPadding) { + if (paddedDomainLo < intercept && orderedDomain[0] >= intercept) { + const { scaleMultiplier } = screenspaceMarkerScaleCompressor( + [intercept, orderedDomain[1]], + [0, 2 * desiredPixelPadding], + chartHeight, + ); + paddedDomainLo = intercept; + paddedDomainHigh = orderedDomain[1] + desiredPixelPadding / scaleMultiplier; + } else if (paddedDomainHigh > 0 && orderedDomain[1] <= 0) { + const { scaleMultiplier } = screenspaceMarkerScaleCompressor( + [orderedDomain[0], intercept], + [2 * desiredPixelPadding, 0], + chartHeight, + ); + paddedDomainLo = orderedDomain[0] - desiredPixelPadding / scaleMultiplier; + paddedDomainHigh = intercept; + } + } + + return inverted ? [paddedDomainHigh, paddedDomainLo] : [paddedDomainLo, paddedDomainHigh]; +} + +/** @public */ +export const LogBase = Object.freeze({ + /** + * log base `10` + */ + Common: 'common' as const, + /** + * log base `2` + */ + Binary: 'binary' as const, + /** + * log base `e` (aka ln) + */ + Natural: 'natural' as const, +}); +/** + * Log bases + * @public + */ +export type LogBase = $Values; + +/** @internal */ +export const logBaseMap: Record = { + [LogBase.Common]: 10, + [LogBase.Binary]: 2, + [LogBase.Natural]: Math.E, +}; + +interface ScaleData { + /** The Type of continuous scale */ + type: ScaleContinuousType; + /** The data input domain */ + domain: any[]; + /** The data output range */ + range: Range; + nice?: boolean; +} + +/** + * Options specific to log scales + * @public + */ +export interface LogScaleOptions { + /** + * Min value to render on log scale + * + * Defaults to min value of domain, or LOG_MIN_ABS_DOMAIN if mixed polarity + */ + logMinLimit?: number; + /** + * Base for log scale + * + * @defaultValue `common` {@link (LogBase:type) | LogBase.Common} + * (i.e. log base 10) + */ + logBase?: LogBase; +} + +type ScaleOptions = Required & { + /** + * The desidered bandwidth for a linear band scale. + * @defaultValue 0 + */ + bandwidth: number; + /** + * The min interval computed on the XDomain. Not available for yDomains. + * @defaultValue 0 + */ + minInterval: number; + /** + * A time zone identifier. Can be any IANA zone supported by he host environment, + * or a fixed-offset name of the form 'utc+3', or the strings 'local' or 'utc'. + * @defaultValue `utc` + */ + timeZone: string; + /** + * The number of bars in the cluster. Used to correctly compute scales when + * using padding between bars. + * @defaultValue 1 + */ + totalBarsInCluster: number; + /** + * The proportion of the range that is reserved for blank space between bands + * A number between 0 and 1. + * @defaultValue 0 + */ + barsPadding: number; + /** + * Pixel value to extend the domain. Applied __before__ nicing. + * + * Does not apply to time scales + * @defaultValue 0 + */ + domainPixelPadding: number; + /** + * Constrains domain pixel padding to the zero baseline + * Does not apply to time scales + */ + constrainDomainPadding?: boolean; + /** + * The approximated number of ticks. + * @defaultValue 10 + */ + desiredTickCount: number; + /** + * true if the scale was adjusted to fit one single value histogram + */ + isSingleValueHistogram: boolean; + /** + * Show only integer values + */ + integersOnly: boolean; +}; + +const defaultScaleOptions: ScaleOptions = { + bandwidth: 0, + minInterval: 0, + timeZone: 'utc', + totalBarsInCluster: 1, + barsPadding: 0, + constrainDomainPadding: true, + domainPixelPadding: 0, + desiredTickCount: 10, + isSingleValueHistogram: false, + integersOnly: false, + logBase: LogBase.Common, +}; + +/** + * Continuous scale + * @internal + */ +export class ScaleContinuous implements Scale { + readonly bandwidth: number; + + readonly totalBarsInCluster: number; + + readonly bandwidthPadding: number; + + readonly minInterval: number; + + readonly step: number; + + readonly type: ScaleContinuousType; + + readonly domain: any[]; + + readonly range: Range; + + readonly isInverted: boolean; + + readonly tickValues: number[]; + + readonly timeZone: string; + + readonly barsPadding: number; + + readonly isSingleValueHistogram: boolean; + + private readonly d3Scale: D3Scale; + + constructor( + { type = ScaleType.Linear, domain = [0, 1], range = [0, 1], nice = false }: ScaleData, + options?: Partial, + ) { + const { + bandwidth, + minInterval, + timeZone, + totalBarsInCluster, + barsPadding, + desiredTickCount, + isSingleValueHistogram, + integersOnly, + logBase, + logMinLimit, + domainPixelPadding, + constrainDomainPadding, + } = mergePartial(defaultScaleOptions, options, { mergeOptionalPartialValues: true }); + this.d3Scale = SCALES[type](); + + if (type === ScaleType.Log) { + (this.d3Scale as ScaleLogarithmic).base(logBaseMap[logBase]); + this.domain = limitLogScaleDomain(domain as [number, number], logMinLimit); + } else { + this.domain = domain; + } + + this.d3Scale.domain(this.domain); + + if (nice && type !== ScaleType.Time) { + (this.d3Scale as ScaleContinuousNumeric).domain(this.domain).nice(desiredTickCount); + this.domain = this.d3Scale.domain(); + } + + const safeBarPadding = maxValueWithUpperLimit(barsPadding, 0, 1); + this.barsPadding = safeBarPadding; + this.bandwidth = bandwidth * (1 - safeBarPadding); + this.bandwidthPadding = bandwidth * safeBarPadding; + this.d3Scale.range(range); + this.step = this.bandwidth + this.barsPadding + this.bandwidthPadding; + this.type = type; + this.range = range; + this.minInterval = minInterval; + this.isInverted = this.domain[0] > this.domain[1]; + this.timeZone = timeZone; + this.totalBarsInCluster = totalBarsInCluster; + this.isSingleValueHistogram = isSingleValueHistogram; + + const [r1, r2] = this.range; + const totalRange = Math.abs(r1 - r2); + + if (type !== ScaleType.Time && domainPixelPadding && !isUnitRange(range) && domainPixelPadding * 2 < totalRange) { + const newDomain = getPixelPaddedDomain( + totalRange, + this.domain as [number, number], + domainPixelPadding, + constrainDomainPadding, + ); + + if (nice) { + (this.d3Scale as ScaleContinuousNumeric).domain(newDomain).nice(desiredTickCount); + this.domain = this.d3Scale.domain(); + } else { + this.domain = newDomain; + this.d3Scale.domain(newDomain); + } + } + + if (type === ScaleType.Time) { + const startDomain = getMomentWithTz(this.domain[0], this.timeZone); + const endDomain = getMomentWithTz(this.domain[1], this.timeZone); + const offset = startDomain.utcOffset(); + const shiftedDomainMin = startDomain.add(offset, 'minutes').valueOf(); + const shiftedDomainMax = endDomain.add(offset, 'minutes').valueOf(); + const tzShiftedScale = scaleUtc().domain([shiftedDomainMin, shiftedDomainMax]); + + const rawTicks = tzShiftedScale.ticks(desiredTickCount); + const timePerTick = (shiftedDomainMax - shiftedDomainMin) / rawTicks.length; + const hasHourTicks = timePerTick < 1000 * 60 * 60 * 12; + + this.tickValues = rawTicks.map((d: Date) => { + const currentDateTime = getMomentWithTz(d, this.timeZone); + const currentOffset = hasHourTicks ? offset : currentDateTime.utcOffset(); + return currentDateTime.subtract(currentOffset, 'minutes').valueOf(); + }); + } else { + // This case is for the xScale (minInterval is > 0) when we want to show bars (bandwidth > 0) + // We want to avoid displaying inner ticks between bars in a bar chart when using linear x scale + if (minInterval > 0 && bandwidth > 0) { + const intervalCount = Math.floor((this.domain[1] - this.domain[0]) / this.minInterval); + this.tickValues = new Array(intervalCount + 1).fill(0).map((_, i) => this.domain[0] + i * this.minInterval); + } else { + this.tickValues = this.getTicks(desiredTickCount, integersOnly); + } + } + } + + private getScaledValue(value?: PrimitiveValue): number | null { + if (typeof value !== 'number' || isNaN(value)) { + return null; + } + + const scaledValue = this.d3Scale(value); + + return isNaN(scaledValue) ? null : scaledValue; + } + + getTicks(ticks: number, integersOnly: boolean) { + // TODO: cleanup types for ticks btw time and non-time scales + // This is forcing a return type of number[] but is really (number|Date)[] + return integersOnly + ? (this.d3Scale as D3ScaleNonTime) + .ticks(ticks) + .filter((item: number) => typeof item === 'number' && item % 1 === 0) + .map((item: number) => parseInt(item.toFixed(0), 10)) + : (this.d3Scale as D3ScaleNonTime).ticks(ticks); + } + + scaleOrThrow(value?: PrimitiveValue): number { + const scaleValue = this.scale(value); + + if (scaleValue === null) { + throw new Error(`Unable to scale value: ${scaleValue})`); + } + + return scaleValue; + } + + scale(value?: PrimitiveValue) { + const scaledValue = this.getScaledValue(value); + + return scaledValue === null ? null : scaledValue + (this.bandwidthPadding / 2) * this.totalBarsInCluster; + } + + pureScale(value?: PrimitiveValue) { + if (this.bandwidth === 0) { + return this.getScaledValue(value); + } + + if (typeof value !== 'number' || isNaN(value)) { + return null; + } + + return this.getScaledValue(value + this.minInterval / 2); + } + + ticks() { + return this.tickValues; + } + + invert(value: number): number { + let invertedValue = this.d3Scale.invert(value); + if (this.type === ScaleType.Time) { + invertedValue = getMomentWithTz(invertedValue, this.timeZone).valueOf(); + } + + return invertedValue as number; + } + + invertWithStep( + value: number, + data: number[], + ): { + value: number; + withinBandwidth: boolean; + } | null { + if (data.length === 0) { + return null; + } + const invertedValue = this.invert(value); + const bisectValue = this.bandwidth === 0 ? invertedValue + this.minInterval / 2 : invertedValue; + const leftIndex = bisectLeft(data, bisectValue); + + if (leftIndex === 0) { + if (invertedValue < data[0]) { + return { + value: data[0] - this.minInterval * Math.ceil((data[0] - invertedValue) / this.minInterval), + withinBandwidth: false, + }; + } + return { + value: data[0], + withinBandwidth: true, + }; + } + const currentValue = data[leftIndex - 1]; + // pure linear scale + if (this.minInterval === 0) { + const nextValue = data[leftIndex]; + const nextDiff = Math.abs(nextValue - invertedValue); + const prevDiff = Math.abs(invertedValue - currentValue); + return { + value: nextDiff <= prevDiff ? nextValue : currentValue, + withinBandwidth: true, + }; + } + if (invertedValue - currentValue <= this.minInterval) { + return { + value: currentValue, + withinBandwidth: true, + }; + } + return { + value: currentValue + this.minInterval * Math.floor((invertedValue - currentValue) / this.minInterval), + withinBandwidth: false, + }; + } + + isSingleValue() { + if (this.isSingleValueHistogram) { + return true; + } + if (this.domain.length < 2) { + return true; + } + + const min = this.domain[0]; + const max = this.domain[this.domain.length - 1]; + return max === min; + } + + isValueInDomain(value: number) { + return value >= this.domain[0] && value <= this.domain[1]; + } + + handleDomainPadding() {} +} + +/** @internal */ +export function getDomainPolarity(domain: number[]): number { + const [min, max] = domain; + // all positive or zero + if (min >= 0 && max >= 0) { + return 1; + } + // all negative or zero + if (min <= 0 && max <= 0) { + return -1; + } + // mixed + return 0; +} diff --git a/packages/osd-charts/src/scales/scale_time.test.ts b/packages/osd-charts/src/scales/scale_time.test.ts new file mode 100644 index 000000000000..ba4f8d5402d7 --- /dev/null +++ b/packages/osd-charts/src/scales/scale_time.test.ts @@ -0,0 +1,267 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DateTime } from 'luxon'; + +import { ScaleContinuous } from '.'; +import { ScaleType } from './constants'; + +describe('[Scale Time] - timezones', () => { + describe('timezone checks', () => { + // these tests are only for have a better understanding on how to deal with + // timezones, isos and formattings + test('[UTC] check equity of luxon and js Date', () => { + const DATE_STRING = '2019-01-01T00:00:00.000Z'; + const dateA = DateTime.fromISO(DATE_STRING, { setZone: true }); + const dateAInLocalTime = DateTime.fromISO(DATE_STRING, { setZone: false }); + const dateB = new Date(DATE_STRING); + expect(dateA.toMillis()).toBe(dateB.getTime()); + expect(dateA.zone.name).toBe('UTC'); + expect(dateA.toISO()).toEqual(DATE_STRING); + expect(dateB.toISOString()).toEqual(DATE_STRING); + expect(dateA.toISO()).toEqual(dateB.toISOString()); + // only valid if current timezone is +1 + // expect(dateAInLocalTime.toISO()).toEqual('2019-01-01T01:00:00.000+01:00'); + // if the date is already UTC, doesn't matter if you convert it to utc + expect(dateA.toUTC().toISO()).toEqual(DATE_STRING); + expect(dateB.toISOString()).toEqual(DATE_STRING); + expect(dateB.toISOString()).toEqual(dateA.toUTC().toISO()); + expect(dateB.toISOString()).toEqual(dateAInLocalTime.toUTC().toISO()); + }); + test('[with timezone] check equity of luxon and js Date', () => { + const DATE_STRING = '2019-01-01T00:00:00.000+05:00'; + const dateA = DateTime.fromISO(DATE_STRING, { setZone: true }); + const dateAInLocalTime = DateTime.fromISO(DATE_STRING, { setZone: false }); + const dateB = new Date(DATE_STRING); + expect(dateA.toMillis()).toBe(dateB.getTime()); + expect(dateAInLocalTime.toMillis()).toBe(dateB.getTime()); + expect(dateA.zone.name).toBe('UTC+5'); + // setting the setZone to true, the outputted ISO will keep the timezone + expect(dateA.toISO()).toEqual(DATE_STRING); + // js date toISOString is always in UTC + expect(dateB.toISOString()).toEqual('2018-12-31T19:00:00.000Z'); + // if we need the UTC version of the date, just call toUtC() + expect(dateB.toISOString()).toEqual(dateA.toUTC().toISO()); + expect(dateB.toISOString()).toEqual(dateAInLocalTime.toUTC().toISO()); + // moving everything to UTC is locale independent + expect(dateA.toUTC().toISO()).toEqual(dateAInLocalTime.toUTC().toISO()); + }); + test('[with timezone from millis] check equity of luxon and js Date', () => { + const DATE_STRING = '2019-01-01T00:00:00.000+05:00'; + const dateAFromString = DateTime.fromISO(DATE_STRING, { setZone: true }); + expect(dateAFromString.zone.name).toBe('UTC+5'); + expect(dateAFromString.toISO()).toBe(DATE_STRING); + + const dateAMillis = dateAFromString.toMillis(); + const dateAFromMillis = DateTime.fromMillis(dateAMillis, { setZone: true }); + // we cannot reconstruct Timezone from millis, millis specifies UTC only + expect(dateAFromMillis.toUTC().toISO()).toBe('2018-12-31T19:00:00.000Z'); + + const dateAFromStringLocale = DateTime.fromISO(DATE_STRING, { setZone: false }); + // if we don't use setZone we are using locale timezone + expect(dateAFromStringLocale.zone.name).toBe(Intl.DateTimeFormat().resolvedOptions().timeZone); + + const dateAMillisFromLocale = dateAFromStringLocale.toMillis(); + expect(dateAMillisFromLocale).toEqual(dateAMillis); + const dateAFromMillisLocale = DateTime.fromMillis(dateAMillis, { setZone: true }); + // we cannot reconstruct Timezone from millis, millis specifies UTC only + expect(dateAFromMillisLocale.toUTC().toISO()).toBe('2018-12-31T19:00:00.000Z'); + }); + }); + describe('invert and ticks on different timezone', () => { + test('shall invert local', () => { + const startTime = DateTime.fromISO('2019-01-01T00:00:00.000').toMillis(); + const midTime = DateTime.fromISO('2019-01-02T00:00:00.000').toMillis(); + const endTime = DateTime.fromISO('2019-01-03T00:00:00.000').toMillis(); + const data = [startTime, midTime, endTime]; + const domain = [startTime, endTime]; + const minRange = 0; + const maxRange = 99; + const minInterval = (endTime - startTime) / 2; + const scale = new ScaleContinuous( + { + type: ScaleType.Time, + domain, + range: [minRange, maxRange], + }, + { bandwidth: undefined, minInterval, timeZone: 'local' }, + ); + expect(scale.invert(0)).toBe(startTime); + expect(scale.invert(49.5)).toBe(midTime); + expect(scale.invert(99)).toBe(endTime); + expect(scale.invertWithStep(0, data)).toEqual({ value: startTime, withinBandwidth: true }); + expect(scale.invertWithStep(24, data)).toEqual({ value: startTime, withinBandwidth: true }); + expect(scale.invertWithStep(25, data)).toEqual({ value: midTime, withinBandwidth: true }); + expect(scale.invertWithStep(50, data)).toEqual({ value: midTime, withinBandwidth: true }); + expect(scale.invertWithStep(74, data)).toEqual({ value: midTime, withinBandwidth: true }); + expect(scale.invertWithStep(76, data)).toEqual({ value: endTime, withinBandwidth: true }); + expect(scale.invertWithStep(100, data)).toEqual({ value: endTime, withinBandwidth: true }); + expect(scale.tickValues.length).toBe(9); + expect(scale.tickValues[0]).toEqual(startTime); + expect(scale.tickValues[4]).toEqual(midTime); + expect(scale.tickValues[8]).toEqual(endTime); + }); + test('shall invert UTC', () => { + const startTime = DateTime.fromISO('2019-01-01T00:00:00.000Z').toMillis(); + const midTime = DateTime.fromISO('2019-01-02T00:00:00.000Z').toMillis(); + const endTime = DateTime.fromISO('2019-01-03T00:00:00.000Z').toMillis(); + const data = [startTime, midTime, endTime]; + const domain = [startTime, endTime]; + const minRange = 0; + const maxRange = 99; + const minInterval = (endTime - startTime) / 2; + const scale = new ScaleContinuous( + { type: ScaleType.Time, domain, range: [minRange, maxRange] }, + { bandwidth: undefined, minInterval, timeZone: 'utc' }, + ); + expect(scale.invert(0)).toBe(startTime); + expect(scale.invert(49.5)).toBe(midTime); + expect(scale.invert(99)).toBe(endTime); + expect(scale.invertWithStep(0, data)).toEqual({ value: startTime, withinBandwidth: true }); + expect(scale.invertWithStep(24, data)).toEqual({ value: startTime, withinBandwidth: true }); + expect(scale.invertWithStep(25, data)).toEqual({ value: midTime, withinBandwidth: true }); + expect(scale.invertWithStep(50, data)).toEqual({ value: midTime, withinBandwidth: true }); + expect(scale.invertWithStep(74, data)).toEqual({ value: midTime, withinBandwidth: true }); + expect(scale.invertWithStep(75, data)).toEqual({ value: endTime, withinBandwidth: true }); + expect(scale.invertWithStep(100, data)).toEqual({ value: endTime, withinBandwidth: true }); + expect(scale.tickValues.length).toBe(9); + expect(scale.tickValues[0]).toEqual(startTime); + expect(scale.tickValues[4]).toEqual(midTime); + expect(scale.tickValues[8]).toEqual(endTime); + }); + test('shall invert +08:00', () => { + const startTime = DateTime.fromISO('2019-01-01T00:00:00.000+08:00').toMillis(); + const midTime = DateTime.fromISO('2019-01-02T00:00:00.000+08:00').toMillis(); + const endTime = DateTime.fromISO('2019-01-03T00:00:00.000+08:00').toMillis(); + const data = [startTime, midTime, endTime]; + const domain = [startTime, endTime]; + const minRange = 0; + const maxRange = 99; + const minInterval = (endTime - startTime) / 2; + const scale = new ScaleContinuous( + { + type: ScaleType.Time, + domain, + range: [minRange, maxRange], + }, + { bandwidth: undefined, minInterval, timeZone: 'utc+8' }, + ); + expect(scale.invert(0)).toBe(startTime); + expect(scale.invert(49.5)).toBe(midTime); + expect(scale.invert(99)).toBe(endTime); + expect(scale.invertWithStep(0, data)).toEqual({ value: startTime, withinBandwidth: true }); + expect(scale.invertWithStep(24, data)).toEqual({ value: startTime, withinBandwidth: true }); + + expect(scale.invertWithStep(25, data)).toEqual({ value: midTime, withinBandwidth: true }); + expect(scale.invertWithStep(50, data)).toEqual({ value: midTime, withinBandwidth: true }); + expect(scale.invertWithStep(74, data)).toEqual({ value: midTime, withinBandwidth: true }); + + expect(scale.invertWithStep(75, data)).toEqual({ value: endTime, withinBandwidth: true }); + expect(scale.invertWithStep(100, data)).toEqual({ value: endTime, withinBandwidth: true }); + expect(scale.tickValues.length).toBe(9); + expect(scale.tickValues[0]).toEqual(startTime); + expect(scale.tickValues[4]).toEqual(midTime); + expect(scale.tickValues[8]).toEqual(endTime); + }); + test('shall invert -08:00', () => { + const startTime = DateTime.fromISO('2019-01-01T00:00:00.000-08:00').toMillis(); + const midTime = DateTime.fromISO('2019-01-02T00:00:00.000-08:00').toMillis(); + const endTime = DateTime.fromISO('2019-01-03T00:00:00.000-08:00').toMillis(); + const data = [startTime, midTime, endTime]; + const domain = [startTime, endTime]; + const minRange = 0; + const maxRange = 99; + const minInterval = (endTime - startTime) / 2; + const scale = new ScaleContinuous( + { + type: ScaleType.Time, + domain, + range: [minRange, maxRange], + }, + { bandwidth: undefined, minInterval, timeZone: 'utc-8' }, + ); + expect(scale.invert(0)).toBe(startTime); + expect(scale.invert(49.5)).toBe(midTime); + expect(scale.invert(99)).toBe(endTime); + + expect(scale.invertWithStep(0, data)).toEqual({ value: startTime, withinBandwidth: true }); + expect(scale.invertWithStep(24, data)).toEqual({ value: startTime, withinBandwidth: true }); + + expect(scale.invertWithStep(25, data)).toEqual({ value: midTime, withinBandwidth: true }); + expect(scale.invertWithStep(50, data)).toEqual({ value: midTime, withinBandwidth: true }); + expect(scale.invertWithStep(74, data)).toEqual({ value: midTime, withinBandwidth: true }); + + expect(scale.invertWithStep(75, data)).toEqual({ value: endTime, withinBandwidth: true }); + expect(scale.invertWithStep(100, data)).toEqual({ value: endTime, withinBandwidth: true }); + expect(scale.tickValues.length).toBe(9); + expect(scale.tickValues[0]).toEqual(startTime); + expect(scale.tickValues[4]).toEqual(midTime); + expect(scale.tickValues[8]).toEqual(endTime); + }); + test('shall invert all timezones', () => { + for (let i = -11; i <= 12; i++) { + const timezone = i === 0 ? 'utc' : i > 0 ? `utc+${i}` : `utc${i}`; + const startTime = DateTime.fromISO('2019-01-01T00:00:00.000', { + zone: timezone, + }).toMillis(); + const midTime = DateTime.fromISO('2019-01-02T00:00:00.000', { zone: timezone }).toMillis(); + const endTime = DateTime.fromISO('2019-01-03T00:00:00.000', { zone: timezone }).toMillis(); + const data = [startTime, midTime, endTime]; + const domain = [startTime, endTime]; + const minRange = 0; + const maxRange = 99; + const minInterval = (endTime - startTime) / 2; + const scale = new ScaleContinuous( + { type: ScaleType.Time, domain, range: [minRange, maxRange] }, + { bandwidth: undefined, minInterval, timeZone: timezone }, + ); + const formatFunction = (d: number) => DateTime.fromMillis(d, { zone: timezone }).toISO(); + expect(scale.invert(0)).toBe(startTime); + expect(scale.invert(49.5)).toBe(midTime); + expect(scale.invert(99)).toBe(endTime); + expect(scale.invertWithStep(0, data)).toEqual({ value: startTime, withinBandwidth: true }); + expect(scale.invertWithStep(24, data)).toEqual({ value: startTime, withinBandwidth: true }); + expect(scale.invertWithStep(25, data)).toEqual({ value: midTime, withinBandwidth: true }); + expect(scale.invertWithStep(50, data)).toEqual({ value: midTime, withinBandwidth: true }); + expect(scale.invertWithStep(74, data)).toEqual({ value: midTime, withinBandwidth: true }); + expect(scale.invertWithStep(75, data)).toEqual({ value: endTime, withinBandwidth: true }); + expect(scale.invertWithStep(100, data)).toEqual({ value: endTime, withinBandwidth: true }); + expect(scale.tickValues.length).toBe(9); + expect(scale.tickValues[0]).toEqual(startTime); + expect(scale.tickValues[4]).toEqual(midTime); + expect(scale.tickValues[8]).toEqual(endTime); + expect(formatFunction(scale.tickValues[0])).toEqual( + DateTime.fromISO('2019-01-01T00:00:00.000', { + zone: timezone, + }).toISO(), + ); + expect(formatFunction(scale.tickValues[4])).toEqual( + DateTime.fromISO('2019-01-02T00:00:00.000', { + zone: timezone, + }).toISO(), + ); + expect(formatFunction(scale.tickValues[8])).toEqual( + DateTime.fromISO('2019-01-03T00:00:00.000', { + zone: timezone, + }).toISO(), + ); + } + }); + }); +}); diff --git a/packages/osd-charts/src/scales/scales.test.ts b/packages/osd-charts/src/scales/scales.test.ts new file mode 100644 index 000000000000..9a8664ab0b69 --- /dev/null +++ b/packages/osd-charts/src/scales/scales.test.ts @@ -0,0 +1,237 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DateTime } from 'luxon'; + +import { ScaleBand } from '.'; +import { ScaleType } from './constants'; +import { limitLogScaleDomain, ScaleContinuous } from './scale_continuous'; + +describe('Scale Test', () => { + test('Create an ordinal scale', () => { + const data = ['a', 'b', 'c', 'd', 'a', 'b', 'c']; + const minRange = 0; + const maxRange = 100; + const ordinalScale = new ScaleBand(data, [minRange, maxRange]); + const { domain, range, bandwidth } = ordinalScale; + expect(domain).toEqual(['a', 'b', 'c', 'd']); + expect(range).toEqual([minRange, maxRange]); + expect(bandwidth).toEqual(maxRange / domain.length); + const scaledValue1 = ordinalScale.scale('a'); + expect(scaledValue1).toBe(0); + const scaledValue2 = ordinalScale.scale('b'); + expect(scaledValue2).toBe(bandwidth); + const scaledValue3 = ordinalScale.scale('c'); + expect(scaledValue3).toBe(bandwidth * 2); + const scaledValue4 = ordinalScale.scale('d'); + expect(scaledValue4).toBe(bandwidth * 3); + }); + test('Create an linear scale', () => { + const data = [0, 10]; + const minRange = 0; + const maxRange = 100; + const linearScale = new ScaleContinuous({ + type: ScaleType.Linear, + domain: data, + range: [minRange, maxRange], + nice: false, + }); + const { domain, range } = linearScale; + expect(domain).toEqual([0, 10]); + expect(range).toEqual([minRange, maxRange]); + const scaledValue1 = linearScale.scale(0); + expect(scaledValue1).toBe(0); + const scaledValue2 = linearScale.scale(1); + expect(scaledValue2).toBe(10); + const scaledValue3 = linearScale.scale(5); + expect(scaledValue3).toBe(50); + const scaledValue4 = linearScale.scale(10); + expect(scaledValue4).toBe(100); + }); + test('Create an time scale', () => { + const date1 = DateTime.fromISO('2019-01-01T00:00:00.000', { zone: 'utc' }).toMillis(); + const date2 = DateTime.fromISO('2019-01-01T00:00:00.000', { zone: 'utc' }).plus({ days: 90 }).toMillis(); + const date3 = DateTime.fromISO('2019-01-01T00:00:00.000', { zone: 'utc' }).plus({ days: 180 }).toMillis(); + const data = [date1, date3]; + const minRange = 0; + const maxRange = 100; + const timeScale = new ScaleContinuous({ + type: ScaleType.Time, + domain: data, + range: [minRange, maxRange], + nice: false, + }); + const { domain, range } = timeScale; + expect(domain).toEqual([date1, date3]); + expect(range).toEqual([minRange, maxRange]); + const scaledValue1 = timeScale.scale(date1); + expect(scaledValue1).toBe(0); + const scaledValue2 = timeScale.scale(date2); + expect(scaledValue2).toBe(50); + const scaledValue3 = timeScale.scale(date3); + expect(scaledValue3).toBe(100); + }); + test('Create an log scale', () => { + const data = [1, 10]; + const minRange = 0; + const maxRange = 100; + const logScale = new ScaleContinuous({ + type: ScaleType.Log, + domain: data, + range: [minRange, maxRange], + nice: false, + }); + const { domain, range } = logScale; + expect(domain).toEqual([1, 10]); + expect(range).toEqual([minRange, maxRange]); + const scaledValue1 = logScale.scale(1); + expect(scaledValue1).toBe(0); + const scaledValue3 = logScale.scale(5); + expect(scaledValue3).toBe((Math.log(5) / Math.log(10)) * 100); + }); + test('Create an log scale starting with 0 as min', () => { + const data = [0, 10]; + const minRange = 0; + const maxRange = 100; + const logScale = new ScaleContinuous({ + type: ScaleType.Log, + domain: data, + range: [minRange, maxRange], + nice: false, + }); + const { domain, range } = logScale; + expect(domain).toEqual([1, 10]); + expect(range).toEqual([minRange, maxRange]); + const scaledValue1 = logScale.scale(1); + expect(scaledValue1).toBe(0); + const scaledValue3 = logScale.scale(5); + expect(scaledValue3).toBe((Math.log(5) / Math.log(10)) * 100); + }); + test('Create an sqrt scale', () => { + const data = [0, 10]; + const minRange = 0; + const maxRange = 100; + const sqrtScale = new ScaleContinuous({ + type: ScaleType.Sqrt, + domain: data, + range: [minRange, maxRange], + nice: false, + }); + const { domain, range } = sqrtScale; + expect(domain).toEqual([0, 10]); + expect(range).toEqual([minRange, maxRange]); + const scaledValue1 = sqrtScale.scale(0); + expect(scaledValue1).toBe(0); + const scaledValue3 = sqrtScale.scale(5); + expect(scaledValue3).toBe((Math.sqrt(5) / Math.sqrt(10)) * 100); + }); + test('Check log scale domain limiting', () => { + let limitedDomain = limitLogScaleDomain([10, 20]); + expect(limitedDomain).toEqual([10, 20]); + + limitedDomain = limitLogScaleDomain([0, 100]); + expect(limitedDomain).toEqual([1, 100]); + + limitedDomain = limitLogScaleDomain([100, 0]); + expect(limitedDomain).toEqual([100, 1]); + + limitedDomain = limitLogScaleDomain([0, 0]); + expect(limitedDomain).toEqual([1, 1]); + + limitedDomain = limitLogScaleDomain([-100, 0]); + expect(limitedDomain).toEqual([-100, -1]); + + limitedDomain = limitLogScaleDomain([0, -100]); + expect(limitedDomain).toEqual([-1, -100]); + + limitedDomain = limitLogScaleDomain([-100, 100]); + expect(limitedDomain).toEqual([1, 100]); + + limitedDomain = limitLogScaleDomain([-100, 50]); + expect(limitedDomain).toEqual([-100, -1]); + + limitedDomain = limitLogScaleDomain([-100, 150]); + expect(limitedDomain).toEqual([1, 150]); + + limitedDomain = limitLogScaleDomain([100, -100]); + expect(limitedDomain).toEqual([100, 1]); + + limitedDomain = limitLogScaleDomain([100, -50]); + expect(limitedDomain).toEqual([100, 1]); + + limitedDomain = limitLogScaleDomain([150, -100]); + expect(limitedDomain).toEqual([150, 1]); + + limitedDomain = limitLogScaleDomain([50, -100]); + expect(limitedDomain).toEqual([-1, -100]); + }); + + test('compare ordinal scale and linear/band invertWithStep 3 bars', () => { + const data = [0, 1, 2]; + const domainLinear = [0, 2]; + const domainOrdinal = [0, 1, 2]; + const minRange = 0; + const maxRange = 120; + const bandwidth = maxRange / 3; + const linearScale = new ScaleContinuous( + { type: ScaleType.Linear, domain: domainLinear, range: [minRange, maxRange - bandwidth], nice: false }, // we currently limit the range like that a band linear scale + { bandwidth, minInterval: 1 }, + ); + const ordinalScale = new ScaleBand(domainOrdinal, [minRange, maxRange]); + expect(ordinalScale.invertWithStep(0)).toEqual({ value: 0, withinBandwidth: true }); + expect(ordinalScale.invertWithStep(40)).toEqual({ value: 1, withinBandwidth: true }); + expect(ordinalScale.invertWithStep(80)).toEqual({ value: 2, withinBandwidth: true }); + // linear scale have 1 pixel difference... + expect(linearScale.invertWithStep(0, data)).toEqual({ value: 0, withinBandwidth: true }); + expect(linearScale.invertWithStep(41, data)).toEqual({ value: 1, withinBandwidth: true }); + expect(linearScale.invertWithStep(81, data)).toEqual({ value: 2, withinBandwidth: true }); + }); + test('compare ordinal scale and linear/band 2 bars', () => { + const dataLinear = [0, 1]; + const dataOrdinal = [0, 1]; + const minRange = 0; + const maxRange = 100; + const bandwidth = maxRange / 2; + const linearScale = new ScaleContinuous( + { + type: ScaleType.Linear, + domain: dataLinear, + range: [minRange, maxRange - bandwidth], + nice: false, + }, // we currently limit the range like that a band linear scale + { bandwidth, minInterval: 1 }, + ); + const ordinalScale = new ScaleBand(dataOrdinal, [minRange, maxRange]); + + expect(ordinalScale.scale(0)).toBe(0); + expect(ordinalScale.scale(1)).toBe(50); + expect(linearScale.scale(0)).toBe(0); + expect(linearScale.scale(1)).toBe(50); + + expect(ordinalScale.invertWithStep(0)).toEqual({ value: 0, withinBandwidth: true }); + expect(ordinalScale.invertWithStep(50)).toEqual({ value: 1, withinBandwidth: true }); + expect(ordinalScale.invertWithStep(100)).toEqual({ value: 1, withinBandwidth: true }); + // linear scale have 1 pixel difference... + expect(linearScale.invertWithStep(0, dataLinear)).toEqual({ value: 0, withinBandwidth: true }); + expect(linearScale.invertWithStep(51, dataLinear)).toEqual({ value: 1, withinBandwidth: true }); + expect(linearScale.invertWithStep(100, dataLinear)).toEqual({ value: 1, withinBandwidth: true }); + expect(linearScale.bandwidth).toBe(50); + expect(linearScale.range).toEqual([0, 50]); + }); +}); diff --git a/packages/osd-charts/src/scales/types.ts b/packages/osd-charts/src/scales/types.ts new file mode 100644 index 000000000000..bad00090b59b --- /dev/null +++ b/packages/osd-charts/src/scales/types.ts @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Scale } from '.'; +import { ScaleType } from './constants'; +import { ScaleBand } from './scale_band'; +import { ScaleContinuous } from './scale_continuous'; + +/** + * Check if a scale is logaritmic + * @internal + */ +export function isLogarithmicScale(scale: Scale): scale is ScaleContinuous { + return scale.type === ScaleType.Log; +} + +/** + * Check if a scale is Band + * @internal + */ +export function isBandScale(scale: Scale): scale is ScaleBand { + return scale.type === ScaleType.Ordinal; +} + +/** + * Check if a scale is continuous + * @internal + */ +export function isContinuousScale(scale: Scale): scale is ScaleContinuous { + return scale.type !== ScaleType.Ordinal; +} diff --git a/packages/osd-charts/src/solvers/monotonic_hill_climb.ts b/packages/osd-charts/src/solvers/monotonic_hill_climb.ts new file mode 100644 index 000000000000..00b0536dc2e6 --- /dev/null +++ b/packages/osd-charts/src/solvers/monotonic_hill_climb.ts @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export function integerSnap(n: number) { + return Math.floor(n); +} + +type NumberMap = (n: number) => number; + +/** + * `monotonicHillClimb` attempts to return a variable value that's associated with the highest valued response (as returned by invoking `getResponse` + * with said variable) yet still within the bounds for that response value, ie. constrained to smaller than or equal `responseUpperConstraint`. + * `minVar` and `maxVar` represent a closed interval constraint on the variable itself. + * `domainSnap` is useful if all real values in the range can't be assumed by the variable; typically, if the variable is integer only, + * such as the number of characters, or avoiding fractional font sizes. + * It is required that `getResponse` is a monotonic function over [minVar, maxVar], ie. a larger `n` value in this domain can't lead to + * a smaller return value. However, as it's an internal function with known use cases, there's no runtime check to assert this. + * Which is why the name expresses it prominently. + */ +/** @internal */ +export function monotonicHillClimb( + getResponse: NumberMap, + maxVar: number, + responseUpperConstraint: number, + domainSnap: NumberMap = (n: number) => n, + minVar: number = 0, +) { + let loVar = domainSnap(minVar); + const loResponse = getResponse(loVar); + let hiVar = domainSnap(maxVar); + let hiResponse = getResponse(hiVar); + + if (loResponse > responseUpperConstraint || loVar > hiVar) { + // bail if even the lowest value doesn't satisfy the constraint + return NaN; + } + + if (hiResponse <= responseUpperConstraint) { + return hiVar; // early bail if maxVar is compliant + } + + let pivotVar: number = NaN; + let pivotResponse: number = NaN; + let lastPivotResponse: number = NaN; + while (loVar < hiVar) { + const newPivotVar = (loVar + hiVar) / 2; + const newPivotResponse = getResponse(domainSnap(newPivotVar)); + if (newPivotResponse === pivotResponse || newPivotResponse === lastPivotResponse) { + return domainSnap(loVar); // bail if we're good and not making further progress + } + pivotVar = newPivotVar; + lastPivotResponse = pivotResponse; // for prevention of bistable oscillation around discretization snap + pivotResponse = newPivotResponse; + const pivotIsCompliant = pivotResponse <= responseUpperConstraint; + if (pivotIsCompliant) { + loVar = pivotVar; + } else { + hiVar = pivotVar; + hiResponse = pivotResponse; + } + } + return domainSnap(pivotVar); +} diff --git a/packages/osd-charts/src/solvers/screenspace_marker_scale_compressor.ts b/packages/osd-charts/src/solvers/screenspace_marker_scale_compressor.ts new file mode 100644 index 000000000000..8c207416a308 --- /dev/null +++ b/packages/osd-charts/src/solvers/screenspace_marker_scale_compressor.ts @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Cartesian, Pixels, Ratio } from '../common/geometry'; + +/** @internal */ +export type ArrayIndex = number; + +/** @internal */ +export interface ScaleCompression { + bounds: ArrayIndex[]; + scaleMultiplier: Ratio; +} + +/** + * A set of Cartesian positioned items with screenspace size (eg. axis tick labels, or scatterplot points) are represented as: + * - a column vector of Cartesian positions in the domain (can be any unit) + * - a column vector of screenspace size (eg. widths in pixels) which has the same number of elements + * The available room in the same screenspace units (practically, pixels) is supplied. + * + * Returns the scale multiplier, as well as the index of the elements determining (compressing) the scale, if solvable. + * If not solvable, it returns a non-finite number in `scaleMultiplier` and no indices in `bounds`. + * @internal + */ +export const screenspaceMarkerScaleCompressor = ( + domainPositions: Cartesian[], + itemWidths: Pixels[], + outerWidth: Pixels, +): ScaleCompression => { + const result: ScaleCompression = { bounds: [], scaleMultiplier: Infinity }; + const itemCount = Math.min(domainPositions.length, itemWidths.length); + for (let left = 0; left < itemCount; left++) { + for (let right = 0; right < itemCount; right++) { + if (domainPositions[left] > domainPositions[right]) continue; // must adhere to left <= right + + const range = outerWidth - itemWidths[left] / 2 - itemWidths[right] / 2; // negative if not enough room + const domain = domainPositions[right] - domainPositions[left]; // always non-negative and finite + const scaleMultiplier = range / domain; // may not be finite, and that's OK + + if (scaleMultiplier < result.scaleMultiplier || Number.isNaN(scaleMultiplier)) { + result.bounds[0] = left; + result.bounds[1] = right; + result.scaleMultiplier = scaleMultiplier; // will persist a Number.finite() value for solvable pairs + } + } + } + + return result; +}; diff --git a/packages/osd-charts/src/specs/constants.ts b/packages/osd-charts/src/specs/constants.ts new file mode 100644 index 000000000000..c4e16dfbfaec --- /dev/null +++ b/packages/osd-charts/src/specs/constants.ts @@ -0,0 +1,177 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $Values } from 'utility-types'; + +import { ChartType } from '../chart_types'; +import { BOTTOM, CENTER, LEFT, MIDDLE, RIGHT, TOP } from '../common/constants'; +import { Position } from '../utils/common'; +import { LIGHT_THEME } from '../utils/themes/light_theme'; +import { SettingsSpec } from './settings'; + +/** @public */ +export const SpecType = Object.freeze({ + Series: 'series' as const, + Axis: 'axis' as const, + Annotation: 'annotation' as const, + Settings: 'settings' as const, + IndexOrder: 'index_order' as const, + SmallMultiples: 'small_multiples' as const, +}); +/** @public */ +export type SpecType = $Values; + +/** + * Type of bin aggregations + * @public + */ +export const BinAgg = Object.freeze({ + /** + * Order by sum of values in bin + */ + Sum: 'sum' as const, + /** + * Order of values are used as is + */ + None: 'none' as const, +}); +/** @public */ +export type BinAgg = $Values; + +/** + * Direction of sorting + * @public + */ +export const Direction = Object.freeze({ + /** + * Least to greatest + */ + Ascending: 'ascending' as const, + /** + * Greatest to least + */ + Descending: 'descending' as const, +}); +/** @public */ +export type Direction = $Values; + +/** @public */ +export const PointerEventType = Object.freeze({ + Over: 'Over' as const, + Out: 'Out' as const, +}); +/** @public */ +export type PointerEventType = $Values; + +/** + * This enums provides the available tooltip types + * @public + */ +export const TooltipType = Object.freeze({ + /** Vertical cursor parallel to x axis */ + VerticalCursor: 'vertical' as const, + /** Vertical and horizontal cursors */ + Crosshairs: 'cross' as const, + /** Follow the mouse coordinates */ + Follow: 'follow' as const, + /** Hide every tooltip */ + None: 'none' as const, +}); +/** + * The TooltipType + * @public + */ +export type TooltipType = $Values; + +/** @public */ +export const BrushAxis = Object.freeze({ + X: 'x' as const, + Y: 'y' as const, + Both: 'both' as const, +}); +/** @public */ +export type BrushAxis = $Values; + +/** + * The position to stick the tooltip to + * @public + */ +export const TooltipStickTo = Object.freeze({ + Top: TOP, + Bottom: BOTTOM, + Middle: MIDDLE, + Left: LEFT, + Right: RIGHT, + Center: CENTER, + MousePosition: 'MousePosition' as const, +}); +/** @public */ +export type TooltipStickTo = $Values; +/** + * Default value for the tooltip type + * @defaultValue `vertical` {@link (TooltipType:type) | TooltipType.VerticalCursor} + * @public + */ +export const DEFAULT_TOOLTIP_TYPE = TooltipType.VerticalCursor; + +/** + * Default value for the tooltip snap + * @defaultValue `true` + * @public + */ +export const DEFAULT_TOOLTIP_SNAP = true; + +/** + * Default legend config + * @internal + */ +export const DEFAULT_LEGEND_CONFIG = { + showLegend: false, + showLegendExtra: false, + legendMaxDepth: Infinity, + legendPosition: Position.Right, +}; + +/** @public */ +export const DEFAULT_SETTINGS_SPEC: SettingsSpec = { + id: '__global__settings___', + chartType: ChartType.Global, + specType: SpecType.Settings, + rendering: 'canvas' as const, + rotation: 0 as const, + animateData: true, + resizeDebounce: 10, + debug: false, + tooltip: { + type: DEFAULT_TOOLTIP_TYPE, + snap: DEFAULT_TOOLTIP_SNAP, + }, + externalPointerEvents: { + tooltip: { + visible: false, + }, + }, + hideDuplicateAxes: false, + baseTheme: LIGHT_THEME, + brushAxis: BrushAxis.X, + minBrushDelta: 2, + ariaUseDefaultSummary: true, + ariaLabelHeadingLevel: 'p', + ...DEFAULT_LEGEND_CONFIG, +}; diff --git a/packages/osd-charts/src/specs/group_by.ts b/packages/osd-charts/src/specs/group_by.ts new file mode 100644 index 000000000000..0686106d16da --- /dev/null +++ b/packages/osd-charts/src/specs/group_by.ts @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { Spec } from '.'; +import { ChartType } from '../chart_types'; +import { Predicate } from '../common/predicate'; +import { getConnect, specComponentFactory } from '../state/spec_factory'; +import { SpecType } from './constants'; + +/** @public */ +export type GroupByAccessor = (spec: Spec, datum: any) => string | number; +/** @alpha */ +export type GroupBySort = Predicate; + +/** + * Title formatter that handles any value returned from the GroupByAccessor + * @public + */ +export type GroupByFormatter = (value: ReturnType) => string; + +/** @alpha */ +export interface GroupBySpec extends Spec { + /** + * Function to return a unique value __by__ which to group the data + */ + by: GroupByAccessor; + /** + * Sort predicate used to sort grouped data + */ + sort: GroupBySort; + /** + * Formatter used on all `by` values. + * + * Only for displayed values, not used in sorting or other internal computations. + */ + format?: GroupByFormatter; +} +const DEFAULT_GROUP_BY_PROPS = { + chartType: ChartType.Global, + specType: SpecType.IndexOrder, +}; + +type DefaultGroupByProps = 'chartType' | 'specType'; + +/** @alpha */ +export type GroupByProps = Pick; + +/** @alpha */ +export const GroupBy: React.FunctionComponent = getConnect()( + specComponentFactory(DEFAULT_GROUP_BY_PROPS), +); diff --git a/packages/osd-charts/src/specs/index.ts b/packages/osd-charts/src/specs/index.ts new file mode 100644 index 000000000000..6e54c45217b5 --- /dev/null +++ b/packages/osd-charts/src/specs/index.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartType } from '../chart_types'; + +/** @public */ +export interface Spec { + /** unique Spec identifier */ + id: string; + /** Chart type define the type of chart that use this spec */ + chartType: ChartType; + /** The type of spec, can be series, axis, annotation, settings etc */ + specType: string; +} + +export * from './group_by'; +export * from './small_multiples'; +export * from './settings'; +export * from './constants'; +export * from '../chart_types/specs'; diff --git a/packages/osd-charts/src/specs/settings.test.tsx b/packages/osd-charts/src/specs/settings.test.tsx new file mode 100644 index 000000000000..94965def337a --- /dev/null +++ b/packages/osd-charts/src/specs/settings.test.tsx @@ -0,0 +1,208 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { createStore, Store } from 'redux'; + +import { chartStoreReducer, GlobalChartState } from '../state/chart_state'; +import { getChartThemeSelector } from '../state/selectors/get_chart_theme'; +import { getSettingsSpecSelector } from '../state/selectors/get_settings_specs'; +import { Position, Rendering, Rotation } from '../utils/common'; +import { DARK_THEME } from '../utils/themes/dark_theme'; +import { LIGHT_THEME } from '../utils/themes/light_theme'; +import { PartialTheme } from '../utils/themes/theme'; +import { TooltipType } from './constants'; +import { Settings, SettingsSpec } from './settings'; +import { SpecsParser } from './specs_parser'; + +const getProxy = (chartStore: Store) => + function SettingsProxy({ settings }: { settings?: Partial }) { + return ( + + + + + + ); + }; +describe('Settings spec component', () => { + let chartStore: Store; + let SettingsProxy: ({ settings }: { settings?: Partial }) => JSX.Element; + beforeEach(() => { + const storeReducer = chartStoreReducer('chart_id'); + chartStore = createStore(storeReducer); + expect(chartStore.getState().specsInitialized).toBe(false); + SettingsProxy = getProxy(chartStore); + }); + test('should update store on mount if spec has a chart store', () => { + mount( + + + , + ); + expect(getSettingsSpecSelector(chartStore.getState()).rotation).toBe(0); + + mount( + + + + + , + ); + expect(getSettingsSpecSelector(chartStore.getState()).rotation).toBe(90); + }); + + test('should update store on component update', () => { + const component = mount(); + let settingSpec = getSettingsSpecSelector(chartStore.getState()); + expect(settingSpec.baseTheme).toEqual(LIGHT_THEME); + expect(settingSpec.rotation).toBe(0); + component.setProps({ + settings: { + baseTheme: DARK_THEME, + rotation: 90 as Rotation, + rendering: 'svg' as Rendering, + animateData: true, + showLegend: true, + tooltip: { + type: TooltipType.None, + snap: false, + }, + legendPosition: Position.Bottom, + showLegendExtra: false, + debug: true, + xDomain: { min: 0, max: 10 }, + }, + }); + settingSpec = getSettingsSpecSelector(chartStore.getState()); + expect(settingSpec.baseTheme).toEqual(DARK_THEME); + expect(settingSpec.rotation).toBe(90); + expect(settingSpec.rendering).toBe('svg'); + expect(settingSpec.animateData).toBe(true); + expect(settingSpec.showLegend).toEqual(true); + expect(settingSpec.tooltip).toEqual({ + type: TooltipType.None, + snap: false, + }); + expect(settingSpec.legendPosition).toBe(Position.Bottom); + expect(settingSpec.showLegendExtra).toEqual(false); + expect(settingSpec.debug).toBe(true); + expect(settingSpec.xDomain).toEqual({ min: 0, max: 10 }); + }); + + test('should set event listeners on chart store', () => { + mount(); + let settingSpec = getSettingsSpecSelector(chartStore.getState()); + + expect(settingSpec.onElementClick).toBeUndefined(); + expect(settingSpec.onElementOver).toBeUndefined(); + expect(settingSpec.onElementOut).toBeUndefined(); + expect(settingSpec.onBrushEnd).toBeUndefined(); + expect(settingSpec.onLegendItemOver).toBeUndefined(); + expect(settingSpec.onLegendItemOut).toBeUndefined(); + expect(settingSpec.onLegendItemClick).toBeUndefined(); + expect(settingSpec.onLegendItemPlusClick).toBeUndefined(); + expect(settingSpec.onLegendItemMinusClick).toBeUndefined(); + + const onElementClick = (): void => {}; + const onElementOver = (): void => {}; + const onOut = () => {}; + const onBrushEnd = (): void => {}; + const onLegendEvent = (): void => {}; + const onPointerUpdateEvent = (): void => {}; + const onRenderChangeEvent = (): void => {}; + + const updatedProps: Partial = { + onElementClick, + onElementOver, + onElementOut: onOut, + onBrushEnd, + onLegendItemOver: onLegendEvent, + onLegendItemOut: onOut, + onLegendItemClick: onLegendEvent, + onLegendItemPlusClick: onLegendEvent, + onLegendItemMinusClick: onLegendEvent, + onPointerUpdate: onPointerUpdateEvent, + onRenderChange: onRenderChangeEvent, + }; + + mount(); + settingSpec = getSettingsSpecSelector(chartStore.getState()); + + expect(settingSpec.onElementClick).toEqual(onElementClick); + expect(settingSpec.onElementOver).toEqual(onElementOver); + expect(settingSpec.onElementOut).toEqual(onOut); + expect(settingSpec.onBrushEnd).toEqual(onBrushEnd); + expect(settingSpec.onLegendItemOver).toEqual(onLegendEvent); + expect(settingSpec.onLegendItemOut).toEqual(onOut); + expect(settingSpec.onLegendItemClick).toEqual(onLegendEvent); + expect(settingSpec.onLegendItemPlusClick).toEqual(onLegendEvent); + expect(settingSpec.onLegendItemMinusClick).toEqual(onLegendEvent); + expect(settingSpec.onPointerUpdate).toEqual(onPointerUpdateEvent); + expect(settingSpec.onRenderChange).toEqual(onRenderChangeEvent); + }); + + test('should allow partial theme', () => { + mount(); + let settingSpec = getSettingsSpecSelector(chartStore.getState()); + expect(settingSpec.baseTheme).toEqual(LIGHT_THEME); + + const partialTheme: PartialTheme = { + colors: { + defaultVizColor: 'aquamarine', + }, + }; + + const updatedProps: Partial = { + theme: partialTheme, + baseTheme: DARK_THEME, + rotation: 90 as Rotation, + rendering: 'svg' as Rendering, + animateData: true, + showLegend: true, + tooltip: { + type: TooltipType.None, + snap: false, + }, + legendPosition: Position.Bottom, + showLegendExtra: false, + hideDuplicateAxes: false, + debug: true, + xDomain: { min: 0, max: 10 }, + }; + + mount(); + + settingSpec = getSettingsSpecSelector(chartStore.getState()); + /* + * the theme is no longer stored into the setting spec. + * it's final theme object is computed through selectors + */ + const theme = getChartThemeSelector(chartStore.getState()); + expect(theme).toEqual({ + ...DARK_THEME, + colors: { + ...DARK_THEME.colors, + ...partialTheme.colors, + }, + }); + }); +}); diff --git a/packages/osd-charts/src/specs/settings.tsx b/packages/osd-charts/src/specs/settings.tsx new file mode 100644 index 000000000000..c0eb9e1be086 --- /dev/null +++ b/packages/osd-charts/src/specs/settings.tsx @@ -0,0 +1,721 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { ComponentType, ReactChild } from 'react'; + +import { CustomXDomain, GroupByAccessor, Spec, TooltipStickTo } from '.'; +import { Cell } from '../chart_types/heatmap/layout/types/viewmodel_types'; +import { PrimitiveValue } from '../chart_types/partition_chart/layout/utils/group_by_rollup'; +import { LegendStrategy } from '../chart_types/partition_chart/layout/utils/highlighted_geoms'; +import { WordModel } from '../chart_types/wordcloud/layout/types/viewmodel_types'; +import { XYChartSeriesIdentifier } from '../chart_types/xy_chart/utils/series'; +import { SeriesIdentifier } from '../common/series_id'; +import { TooltipPortalSettings } from '../components'; +import { CustomTooltip } from '../components/tooltip/types'; +import { ScaleContinuousType, ScaleOrdinalType } from '../scales'; +import { LegendPath } from '../state/actions/legend'; +import { getConnect, specComponentFactory } from '../state/spec_factory'; +import { Accessor } from '../utils/accessor'; +import { + Color, + HorizontalAlignment, + LayoutDirection, + Position, + Rendering, + Rotation, + VerticalAlignment, +} from '../utils/common'; +import { GeometryValue } from '../utils/geometry'; +import { GroupId } from '../utils/ids'; +import { SeriesCompareFn } from '../utils/series_sort'; +import { PartialTheme, Theme } from '../utils/themes/theme'; +import { BinAgg, BrushAxis, DEFAULT_SETTINGS_SPEC, Direction, PointerEventType, TooltipType } from './constants'; + +/** @public */ +export interface LayerValue { + /** + * The category value as retrieved by the `groupByRollup` callback + */ + groupByRollup: PrimitiveValue; + /** + * The small multiples `` `by` accessor value, to specify which small multiples panel is interacted with + */ + smAccessorValue: ReturnType; + /** + * Numerical value of the partition + */ + value: number; + /** + * The position index of the sub-partition within its containing partition + */ + sortIndex: number; + /** + * The depth of the partition in terms of the layered partition tree, where + * 0 is root (single, not visualized root of the partitioning tree), + * 1 is pie chart slices and innermost layer of sunburst, or 1st level treemap/flame/icicle breakdown + * 2 and above are increasingly outer layers + * maximum value is on the deepest leaf node + */ + depth: number; + /** + * It contains the full path of the partition node, which is an array of `{index, value}` tuples + * where `index` corresponds to `sortIndex` and `value` corresponds `groupByRollup` + */ + path: LegendPath; +} + +/** @public */ +export interface GroupBrushExtent { + groupId: GroupId; + extent: [number, number]; +} +/** @public */ +export interface XYBrushArea { + x?: [number, number]; + y?: Array; +} + +/** @public */ +export type XYChartElementEvent = [GeometryValue, XYChartSeriesIdentifier]; +/** @public */ +export type PartitionElementEvent = [Array, SeriesIdentifier]; +/** @public */ +export type HeatmapElementEvent = [Cell, SeriesIdentifier]; +/** @public */ +export type WordCloudElementEvent = [WordModel, SeriesIdentifier]; + +/** + * An object that contains the scaled mouse position based on + * the current chart configuration. + * @public + */ +export type ProjectedValues = { + /** + * The independent variable of the chart + */ + x: PrimitiveValue; + /** + * The set of dependent variable, each one with its own groupId + */ + y: Array<{ value: PrimitiveValue; groupId: string }>; + /** + * The categorical value used for the vertical placement of the chart + * in a small multiple layout + */ + smVerticalValue: PrimitiveValue; + /** + * The categorical value used for the horizontal placement of the chart + * in a small multiple layout + */ + smHorizontalValue: PrimitiveValue; +}; + +/** + * @public + * The listener type for click on the projection area. + */ +export type ProjectionClickListener = (values: ProjectedValues) => void; + +/** @public */ +export type ElementClickListener = ( + elements: Array, +) => void; +/** @public */ +export type ElementOverListener = ( + elements: Array, +) => void; +/** @public */ +export type BrushEndListener = (brushArea: XYBrushArea) => void; +/** @public */ +export type LegendItemListener = (series: SeriesIdentifier[]) => void; +/** @public */ +export type PointerUpdateListener = (event: PointerEvent) => void; +/** + * Listener to be called when chart render state changes + * + * `isRendered` value is `true` when rendering is complete and `false` otherwise + * @public + */ +export type RenderChangeListener = (isRendered: boolean) => void; +/** @public */ +export type BasicListener = () => undefined | void; + +/** @public */ +export interface BasePointerEvent { + chartId: string; + type: PointerEventType; +} +/** + * Event used to synchronize pointers/mouse positions between Charts. + * + * fired as callback argument for `PointerUpdateListener` + * @public + */ +export interface PointerOverEvent extends BasePointerEvent { + type: typeof PointerEventType.Over; + scale: ScaleContinuousType | ScaleOrdinalType; + /** + * Unit for event (i.e. `time`, `feet`, `count`, etc.) Not currently used/implemented + * @alpha + */ + unit?: string; + value: number | string | null; +} +/** @public */ +export interface PointerOutEvent extends BasePointerEvent { + type: typeof PointerEventType.Out; +} + +/** @public */ +export type PointerEvent = PointerOverEvent | PointerOutEvent; + +/** + * This interface describe the properties of single value shown in the tooltip + * @public + */ +export interface TooltipValue { + /** + * The label of the tooltip value + */ + label: string; + /** + * The value + */ + value: any; + /** + * The formatted value to display + */ + formattedValue: string; + /** + * The mark value + */ + markValue?: number | null; + /** + * The mark value to display + */ + formattedMarkValue?: string | null; + /** + * The color of the graphic mark (by default the color of the series) + */ + color: Color; + /** + * True if the mouse is over the graphic mark connected to the tooltip + */ + isHighlighted: boolean; + /** + * True if the tooltip is visible, false otherwise + */ + isVisible: boolean; + /** + * The identifier of the related series + */ + seriesIdentifier: SeriesIdentifier; + /** + * The accessor linked to the current tooltip value + */ + valueAccessor?: Accessor; + + /** + * The datum associated with the current tooltip value + * Maybe not available + */ + datum?: unknown; +} + +/** + * A value formatter of a {@link TooltipValue} + * @public + */ +export type TooltipValueFormatter = (data: TooltipValue) => JSX.Element | string; + +/** + * The advanced configuration for the tooltip + * @public + */ +export type TooltipProps = TooltipPortalSettings<'chart'> & { + /** + * The {@link (TooltipType:type) | TooltipType} of the tooltip + */ + type?: TooltipType; + /** + * Whenever the tooltip needs to snap to the x/band position or not + */ + snap?: boolean; + /** + * A {@link TooltipValueFormatter} to format the header value + */ + headerFormatter?: TooltipValueFormatter; + /** + * Unit for event (i.e. `time`, `feet`, `count`, etc.). + * Not currently used/implemented + * + * @alpha + */ + unit?: string; + /** + * Render custom tooltip given header and values + */ + customTooltip?: CustomTooltip; + /** + * Stick the tooltip to a specific position within the current cursor + * @defaultValue mousePosition + */ + stickTo?: TooltipStickTo; +}; + +/** + * Either a {@link (TooltipType:type)} or an {@link (TooltipProps:type)} configuration + * @public + */ +export type TooltipSettings = TooltipType | TooltipProps; + +/** + * The settings for handling external events. + * @alpha + */ +export interface ExternalPointerEventsSettings { + /** + * Tooltip settings used for external events + */ + tooltip: TooltipPortalSettings<'chart'> & { + /** + * `true` to show the tooltip when the chart receive an + * external pointer event, 'false' to hide the tooltip. + * @defaultValue `false` + */ + visible?: boolean; + }; +} + +/** + * Legend action component props + * + * @public + */ +export interface LegendActionProps { + /** + * Series identifiers for the given series + */ + series: SeriesIdentifier[]; + /** + * Resolved label/name of given series + */ + label: string; + /** + * Resolved color of given series + */ + color: string; +} +/** + * Legend action component used to render actions next to legend items + * + * render slot is constrained to 20px x 16px + * + * @public + */ +export type LegendAction = ComponentType; + +/** @public */ +export interface LegendColorPickerProps { + /** + * Anchor used to position picker + */ + anchor: HTMLElement; + /** + * Current color of the given series + */ + color: Color; + /** + * Callback to close color picker and set persistent color + */ + onClose: () => void; + /** + * Callback to update temporary color state + */ + onChange: (color: Color | null) => void; + /** + * Series ids for the active series + */ + seriesIdentifiers: SeriesIdentifier[]; +} +/** @public */ +export type LegendColorPicker = ComponentType; + +/** + * Buffer between cursor and point to trigger interaction + * @public + */ +export type MarkBuffer = number | ((radius: number) => number); + +/** + * The legend position configuration. + * @public + */ +export type LegendPositionConfig = { + /** + * The vertical alignment of the legend + */ + vAlign: typeof VerticalAlignment.Top | typeof VerticalAlignment.Bottom; // TODO typeof VerticalAlignment.Middle + /** + * The horizontal alignment of the legend + */ + hAlign: typeof HorizontalAlignment.Left | typeof HorizontalAlignment.Right; // TODO typeof HorizontalAlignment.Center + /** + * The direction of the legend items. + * `horizontal` shows all the items listed one a side the other horizontally, wrapping to new lines. + * `vertical` shows the items in a vertical list + */ + direction: LayoutDirection; + /** + * Remove the legend from the outside chart area, making it floating above the chart. + * @defaultValue false + */ + floating: boolean; + /** + * The number of columns in floating configuration + * @defaultValue 1 + */ + floatingColumns?: number; + // TODO add grow factor: fill, shrink, fixed column size +}; + +/** + * The legend configuration + * @public + */ +export interface LegendSpec { + /** + * Show the legend + * @defaultValue false + */ + showLegend: boolean; + /** + * Set legend position + * @defaultValue Position.Right + */ + legendPosition: Position | LegendPositionConfig; + /** + * Show an extra parameter on each legend item defined by the chart type + * @defaultValue `false` + */ + showLegendExtra: boolean; + /** + * Limit the legend to the specified maximal depth when showing a hierarchical legend + */ + legendMaxDepth: number; + /** + * Display the legend as a flat list. If true, legendStrategy is always `LegendStrategy.Key`. + */ + flatLegend?: boolean; + /** + * Choose a partition highlighting strategy for hovering over legend items. It's obligate `LegendStrategy.Key` if `flatLegend` is true. + */ + legendStrategy?: LegendStrategy; + onLegendItemOver?: LegendItemListener; + onLegendItemOut?: BasicListener; + onLegendItemClick?: LegendItemListener; + onLegendItemPlusClick?: LegendItemListener; + onLegendItemMinusClick?: LegendItemListener; + /** + * Render slot to render action for legend + */ + legendAction?: LegendAction; + legendColorPicker?: LegendColorPicker; +} + +/** + * The Spec used for Chart settings + * @public + */ +export interface SettingsSpec extends Spec, LegendSpec { + /** + * Partial theme to be merged with base + * + * or + * + * Array of partial themes to be merged with base + * index `0` being the highest priority + * + * i.e. `[primary, secondary, tertiary]` + */ + theme?: PartialTheme | PartialTheme[]; + /** + * Full default theme to use as base + * + * @defaultValue `LIGHT_THEME` + */ + baseTheme?: Theme; + rendering: Rendering; + rotation: Rotation; + animateData: boolean; + + /** + * The tooltip configuration {@link TooltipSettings} + */ + tooltip: TooltipSettings; + /** + * {@inheritDoc ExternalPointerEventsSettings} + * @alpha + */ + externalPointerEvents: ExternalPointerEventsSettings; + /** + * Show debug shadow elements on chart + */ + debug: boolean; + /** + * Show debug render state on `ChartStatus` component + * @alpha + */ + debugState?: boolean; + + /** + * Removes duplicate axes + * + * Compares title, position and first & last tick labels + */ + hideDuplicateAxes: boolean; + /** + * Attach a listener for click on the projection area. + * The listener will be called with the current x value snapped to the closest + * X axis point, and an array of Y values for every groupId used in the chart. + */ + onProjectionClick?: ProjectionClickListener; + onElementClick?: ElementClickListener; + onElementOver?: ElementOverListener; + onElementOut?: BasicListener; + pointBuffer?: MarkBuffer; + onBrushEnd?: BrushEndListener; + + onPointerUpdate?: PointerUpdateListener; + onRenderChange?: RenderChangeListener; + xDomain?: CustomXDomain; + resizeDebounce?: number; + + /** + * Block the brush tool on a specific axis: x, y or both. + * @defaultValue `x` {@link (BrushAxis:type) | BrushAxis.X} + */ + brushAxis?: BrushAxis; + /** + * The minimum number of pixel to consider for a valid brush event (in both axis if brushAxis prop is BrushAxis.Both). + * E.g. a min value of 2 means that the brush area needs to be at least 2 pixel wide and 2 pixel tall. + * @defaultValue 2 + */ + minBrushDelta?: number; + /** + * Boolean to round brushed values to nearest step bounds. + * + * e.g. + * A brush selection range of [1.23, 3.6] with a domain of [1, 2, 3, 4]. + * + * - when true returns [1, 3] + * - when false returns [1.23, 3.6] + * + * @defaultValue false + */ + roundHistogramBrushValues?: boolean; + /** + * Boolean to allow brushing on last bucket even when outside domain or limit to end of domain. + * + * e.g. + * A brush selection range of [1.23, 3.6] with a domain of [1, 2, 3] + * + * - when true returns [1.23, 3.6] + * - when false returns [1.23, 3] + * + * @defaultValue false + */ + allowBrushingLastHistogramBucket?: boolean; + /** + * Orders ordinal x values + */ + orderOrdinalBinsBy?: OrderBy; + + /** + * A compare function or an object of compare functions to sort + * series in different part of the chart like tooltip, legend and + * the rendering order on the screen. To assign the same compare function. + * @defaultValue the series are sorted in order of appearance in the chart configuration + * @alpha + */ + // sortSeriesBy?: SeriesCompareFn | SortSeriesByConfig; + + /** + * Render component for no results UI + */ + noResults?: ComponentType | ReactChild; + /** + * User can specify the heading level for the label + * @defaultValue 'h2' + */ + ariaLabelHeadingLevel: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p'; + /** + * A text to label the chart + */ + ariaLabel?: string; + /** + * An DOM element ID for the chart label. If provided, it will override the ariaLabel prop. + */ + ariaLabelledBy?: string; + /** + * A description about the chart. + */ + ariaDescription?: string; + /** + * * An DOM element ID for the chart description. If provided, it will override the ariaDescription prop. + */ + ariaDescribedBy?: string; + /** + * Renders an autogenerated summary of the chart + * @defaultValue true + */ + ariaUseDefaultSummary: boolean; +} + +/** + * An object of compare functions to sort + * series in different part of the chart like tooltip, legend and rendering order. + * @public + */ +export interface SortSeriesByConfig { + /** + * A SeriesSortFn to sort the legend values (top-bottom) + * It has precedence over the general one + */ + legend?: SeriesCompareFn; + /** + * A SeriesSortFn to sort tooltip values (top-bottom) + * It has precedence over the general one + */ + tooltip?: SeriesCompareFn; + /** + * A SeriesSortFn to sort the rendering order of series. + * Left/right for cluster, bottom-up for stacked. + * It has precedence over the general one + * Currently available only on XY charts + */ + rendering?: SeriesCompareFn; + /** + * The default SeriesSortFn in case no other specific sorting fn are used. + * The rendering sorting is applied only to XY charts at the moment + */ + default?: SeriesCompareFn; +} + +/** + * Order by options + * @public + */ +export interface OrderBy { + binAgg?: BinAgg; + direction?: Direction; +} + +/** @public */ +export type DefaultSettingsProps = + | 'id' + | 'chartType' + | 'specType' + | 'rendering' + | 'rotation' + | 'resizeDebounce' + | 'animateData' + | 'debug' + | 'tooltip' + | 'theme' + | 'hideDuplicateAxes' + | 'brushAxis' + | 'minBrushDelta' + | 'externalPointerEvents' + | 'showLegend' + | 'showLegendExtra' + | 'legendPosition' + | 'legendMaxDepth' + | 'ariaUseDefaultSummary' + | 'ariaLabelHeadingLevel'; + +/** @public */ +export type SettingsSpecProps = Partial>; + +/** @public */ +export const Settings: React.FunctionComponent = getConnect()( + specComponentFactory(DEFAULT_SETTINGS_SPEC), +); + +/** @internal */ +export function isPointerOutEvent(event: PointerEvent | null | undefined): event is PointerOutEvent { + return event?.type === PointerEventType.Out; +} + +/** @internal */ +export function isPointerOverEvent(event: PointerEvent | null | undefined): event is PointerOverEvent { + return event?.type === PointerEventType.Over; +} + +/** @internal */ +export function isTooltipProps(config: TooltipType | TooltipProps): config is TooltipProps { + return typeof config === 'object'; +} + +/** @internal */ +export function isTooltipType(config: TooltipType | TooltipProps): config is TooltipType { + return typeof config !== 'object'; // TooltipType is 'vertical'|'cross'|'follow'|'none' while TooltipProps is object +} + +/** @internal */ +export function isCrosshairTooltipType(type: TooltipType) { + return type === TooltipType.VerticalCursor || type === TooltipType.Crosshairs; +} + +/** @internal */ +export function isFollowTooltipType(type: TooltipType) { + return type === TooltipType.Follow; +} + +/** @internal */ +export function getTooltipType(settings: SettingsSpec, externalTooltip = false): TooltipType { + const defaultType = TooltipType.VerticalCursor; + if (externalTooltip) { + return getExternalTooltipType(settings); + } + const { tooltip } = settings; + if (tooltip === undefined || tooltip === null) { + return defaultType; + } + if (isTooltipType(tooltip)) { + return tooltip; + } + if (isTooltipProps(tooltip)) { + return tooltip.type || defaultType; + } + return defaultType; +} + +/** + * Always return a Vertical Cursor for external pointer events or None if hidden + * @internal + * @param settings - the SettingsSpec + */ +export function getExternalTooltipType({ + externalPointerEvents: { + tooltip: { visible }, + }, +}: SettingsSpec): TooltipType { + return visible ? TooltipType.VerticalCursor : TooltipType.None; +} diff --git a/packages/osd-charts/src/specs/small_multiples.ts b/packages/osd-charts/src/specs/small_multiples.ts new file mode 100644 index 000000000000..d793e90abe37 --- /dev/null +++ b/packages/osd-charts/src/specs/small_multiples.ts @@ -0,0 +1,96 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { Spec } from '.'; +import { ChartType } from '../chart_types'; +import { Ratio } from '../common/geometry'; +import { getConnect, specComponentFactory } from '../state/spec_factory'; +import { SpecType } from './constants'; + +/** + * Can be used for margin or padding start/end (eg. left/right or top/bottom) + * Todo: this will soon change to `{outer, inner}` for explicit specification + * @alpha + */ +export type RelativeBandsPadding = { + /** + * Outer padding specifies the padding size *next to* a small multiples panel that's on the edge of the small + * multiples grid, expressed as a proportion (ratio) of the panel size + */ + outer: Ratio; + /** + * Inner padding specifies the padding size *between* small multiples panels in the small multiples grid, + * expressed as a proportion (ratio) of the panel size + */ + inner: Ratio; +}; + +/** @internal */ +export const DEFAULT_SM_PANEL_PADDING: RelativeBandsPadding = { outer: 0, inner: 0.1 }; + +/** + * Specifies styling and stylistic layout attributes relating to small multiples + * @alpha + */ +export interface SmallMultiplesStyle { + /** + * Horizontal padding for each panel, expressed as [leftMarginRatio, rightMarginRatio], relative to the gross panel width + */ + horizontalPanelPadding: RelativeBandsPadding; + /** + * Vertical padding for each panel, expressed as [topMarginRatio, bottomMarginRatio], relative to the gross panel height + */ + verticalPanelPadding: RelativeBandsPadding; +} + +/** @alpha */ +export interface SmallMultiplesSpec extends Spec { + /** + * Identifies the `` referenced by `splitHorizontally="foo"`, specifying horizontal tiling + */ + splitHorizontally?: string; + /** + * Identifies the `` referenced by `splitVertically="bar"`, specifying vertical tiling + */ + splitVertically?: string; + /** + * Identifies the `` referenced by `splitVertically="baz"`, specifying space-filling tiling in a Z pattern + */ + splitZigzag?: string; + /** + * Specifies styling and layout properties of the tiling, such as paddings between and outside panels + */ + style?: Partial; +} + +const DEFAULT_SMALL_MULTIPLES_PROPS = { + id: '__global__small_multiples___', + chartType: ChartType.Global, + specType: SpecType.SmallMultiples, +}; + +/** @alpha */ +export type SmallMultiplesProps = Partial>; + +/** @alpha */ +export const SmallMultiples: React.FunctionComponent = getConnect()( + specComponentFactory(DEFAULT_SMALL_MULTIPLES_PROPS), +); diff --git a/packages/osd-charts/src/specs/specs_parser.test.tsx b/packages/osd-charts/src/specs/specs_parser.test.tsx new file mode 100644 index 000000000000..23b447ab5f6f --- /dev/null +++ b/packages/osd-charts/src/specs/specs_parser.test.tsx @@ -0,0 +1,184 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { createStore } from 'redux'; + +import { BarSeries } from '../chart_types/specs'; +import { BarSeriesSpec } from '../chart_types/xy_chart/utils/specs'; +import { chartStoreReducer } from '../state/chart_state'; +import { DEFAULT_SETTINGS_SPEC } from './constants'; +import { SpecsParser } from './specs_parser'; + +describe('Specs parser', () => { + test('can mount the spec parser', () => { + const storeReducer = chartStoreReducer('chart_id'); + const chartStore = createStore(storeReducer); + + expect(chartStore.getState().specsInitialized).toBe(false); + const component = ( + + + + ); + mount(component); + expect(chartStore.getState().specsInitialized).toBe(true); + }); + test('can parse few components', () => { + const storeReducer = chartStoreReducer('chart_id'); + const chartStore = createStore(storeReducer); + + expect(chartStore.getState().specsInitialized).toBe(false); + const component = ( + + + + + + + + ); + mount(component); + const state = chartStore.getState(); + expect(state.specsInitialized).toBe(true); + expect(Object.keys(state.specs)).toEqual([DEFAULT_SETTINGS_SPEC.id, 'bars', 'bars2']); + expect(state.specs.bars).toBeDefined(); + expect(state.specs.bars2).toBeDefined(); + }); + test('can update a component', () => { + const storeReducer = chartStoreReducer('chart_id'); + const chartStore = createStore(storeReducer); + + expect(chartStore.getState().specsInitialized).toBe(false); + const component = ( + + + + + + ); + const wrapper = mount(component); + + wrapper.setProps({ + children: ( + + + + ), + }); + const state = chartStore.getState(); + expect((state.specs.bars as BarSeriesSpec).xAccessor).toBe(1); + }); + test('should remove a spec when replaced with a new', () => { + const storeReducer = chartStoreReducer('chart_id'); + const chartStore = createStore(storeReducer); + + expect(chartStore.getState().specsInitialized).toBe(false); + const component = ( + + + + + + ); + const wrapper = mount(component); + + expect(chartStore.getState().specs.one).toBeDefined(); + + wrapper.setProps({ + children: ( + + + + ), + }); + const state = chartStore.getState(); + expect(state.specs.one).toBeUndefined(); + expect(state.specs.two).toBeDefined(); + }); + test('set initialization to false on unmount', () => { + const storeReducer = chartStoreReducer('chart_id'); + const chartStore = createStore(storeReducer); + const component = mount( + + + , + ); + component.unmount(); + expect(chartStore.getState().specsInitialized).toBe(false); + }); +}); diff --git a/packages/osd-charts/src/specs/specs_parser.tsx b/packages/osd-charts/src/specs/specs_parser.tsx new file mode 100644 index 000000000000..fb33f1eba635 --- /dev/null +++ b/packages/osd-charts/src/specs/specs_parser.tsx @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; + +import { specParsed, specUnmounted } from '../state/actions/specs'; + +const SpecsParserComponent: React.FunctionComponent = (props) => { + const injected = props as DispatchProps; + // clean all specs + useEffect(() => { + injected.specParsed(); + }); + useEffect( + () => () => { + injected.specUnmounted(); + }, + [], // eslint-disable-line react-hooks/exhaustive-deps + ); + return props.children ? (props.children as React.ReactElement) : null; +}; + +interface DispatchProps { + specParsed: () => void; + specUnmounted: () => void; +} + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => + bindActionCreators( + { + specParsed, + specUnmounted, + }, + dispatch, + ); + +/** + * The Spec Parser component + * @internal + */ +export const SpecsParser = connect(null, mapDispatchToProps)(SpecsParserComponent); diff --git a/packages/osd-charts/src/state/actions/chart.ts b/packages/osd-charts/src/state/actions/chart.ts new file mode 100644 index 000000000000..3656da6acac3 --- /dev/null +++ b/packages/osd-charts/src/state/actions/chart.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const CHART_RENDERED = 'CHART_RENDERED'; + +interface ChartRenderedAction { + type: typeof CHART_RENDERED; +} + +/** @internal */ +export function onChartRendered(): ChartRenderedAction { + return { type: CHART_RENDERED }; +} + +/** @internal */ +export type ChartActions = ChartRenderedAction; diff --git a/packages/osd-charts/src/state/actions/chart_settings.ts b/packages/osd-charts/src/state/actions/chart_settings.ts new file mode 100644 index 000000000000..40cf51e76e0e --- /dev/null +++ b/packages/osd-charts/src/state/actions/chart_settings.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Dimensions } from '../../utils/dimensions'; + +/** @internal */ +export const UPDATE_PARENT_DIMENSION = 'UPDATE_PARENT_DIMENSION'; + +interface UpdateParentDimensionAction { + type: typeof UPDATE_PARENT_DIMENSION; + dimensions: Dimensions; +} + +/** @internal */ +export function updateParentDimensions(dimensions: Dimensions): UpdateParentDimensionAction { + return { type: UPDATE_PARENT_DIMENSION, dimensions }; +} + +/** @internal */ +export type ChartSettingsActions = UpdateParentDimensionAction; diff --git a/packages/osd-charts/src/state/actions/colors.ts b/packages/osd-charts/src/state/actions/colors.ts new file mode 100644 index 000000000000..9cbe0ac4cb7b --- /dev/null +++ b/packages/osd-charts/src/state/actions/colors.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SeriesKey } from '../../common/series_id'; +import { Color } from '../../utils/common'; + +/** @internal */ +export const CLEAR_TEMPORARY_COLORS = 'CLEAR_TEMPORARY_COLORS'; + +/** @internal */ +export const SET_TEMPORARY_COLOR = 'SET_TEMPORARY_COLOR'; + +/** @internal */ +export const SET_PERSISTED_COLOR = 'SET_PERSISTED_COLOR'; + +interface ClearTemporaryColors { + type: typeof CLEAR_TEMPORARY_COLORS; +} + +interface SetTemporaryColor { + type: typeof SET_TEMPORARY_COLOR; + keys: SeriesKey[]; + color: Color | null; +} + +interface SetPersistedColor { + type: typeof SET_PERSISTED_COLOR; + keys: SeriesKey[]; + color: Color | null; +} + +/** @internal */ +export function clearTemporaryColors(): ClearTemporaryColors { + return { type: CLEAR_TEMPORARY_COLORS }; +} + +/** @internal */ +export function setTemporaryColor(keys: SeriesKey[], color: Color | null): SetTemporaryColor { + return { type: SET_TEMPORARY_COLOR, keys, color }; +} + +/** @internal */ +export function setPersistedColor(keys: SeriesKey[], color: Color | null): SetPersistedColor { + return { type: SET_PERSISTED_COLOR, keys, color }; +} + +/** @internal */ +export type ColorsActions = ClearTemporaryColors | SetTemporaryColor | SetPersistedColor; diff --git a/packages/osd-charts/src/state/actions/dom_element.ts b/packages/osd-charts/src/state/actions/dom_element.ts new file mode 100644 index 000000000000..53a38fb86ba6 --- /dev/null +++ b/packages/osd-charts/src/state/actions/dom_element.ts @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $Values } from 'utility-types'; + +/** @internal */ +export const ON_DOM_ELEMENT_ENTER = 'ON_DOM_ELEMENT_ENTER'; +/** @internal */ +export const ON_DOM_ELEMENT_LEAVE = 'ON_DOM_ELEMENT_LEAVE'; + +/** @internal */ +export const DOMElementType = Object.freeze({ + LineAnnotationMarker: 'LineAnnotationMarker' as const, +}); +/** @internal */ +export type DOMElementType = $Values; + +/** @internal */ +export interface DOMElement { + type: DOMElementType; + id: string; + createdBySpecId: string; // TODO is that + datum enough to identify the elements? + datum: unknown; +} +interface DOMElementEnterAction { + type: typeof ON_DOM_ELEMENT_ENTER; + element: DOMElement; +} + +interface DOMElementLeaveAction { + type: typeof ON_DOM_ELEMENT_LEAVE; +} + +/** @internal */ +export function onDOMElementLeave(): DOMElementLeaveAction { + return { type: ON_DOM_ELEMENT_LEAVE }; +} + +/** @internal */ +export function onDOMElementEnter(element: DOMElement): DOMElementEnterAction { + return { type: ON_DOM_ELEMENT_ENTER, element }; +} + +/** @internal */ +export type DOMElementActions = DOMElementEnterAction | DOMElementLeaveAction; diff --git a/packages/osd-charts/src/state/actions/events.ts b/packages/osd-charts/src/state/actions/events.ts new file mode 100644 index 000000000000..2f49f1ae9ce1 --- /dev/null +++ b/packages/osd-charts/src/state/actions/events.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PointerEvent } from '../../specs/settings'; + +/** @internal */ +export const EXTERNAL_POINTER_EVENT = 'EXTERNAL_POINTER_EVENT'; + +interface ExternalPointerEvent { + type: typeof EXTERNAL_POINTER_EVENT; + event: PointerEvent; +} + +/** @internal */ +export function onExternalPointerEvent(event: PointerEvent): ExternalPointerEvent { + return { type: EXTERNAL_POINTER_EVENT, event }; +} + +/** @internal */ +export type EventsActions = ExternalPointerEvent; diff --git a/packages/osd-charts/src/state/actions/index.ts b/packages/osd-charts/src/state/actions/index.ts new file mode 100644 index 000000000000..8ddc4b8ffa5b --- /dev/null +++ b/packages/osd-charts/src/state/actions/index.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartActions } from './chart'; +import { ChartSettingsActions } from './chart_settings'; +import { ColorsActions } from './colors'; +import { EventsActions } from './events'; +import { KeyActions } from './key'; +import { LegendActions } from './legend'; +import { MouseActions } from './mouse'; +import { SpecActions } from './specs'; +import { ZIndexActions } from './z_index'; + +/** @internal */ +export type StateActions = + | SpecActions + | ChartActions + | ChartSettingsActions + | LegendActions + | EventsActions + | MouseActions + | KeyActions + | ColorsActions + | ZIndexActions; diff --git a/packages/osd-charts/src/state/actions/key.ts b/packages/osd-charts/src/state/actions/key.ts new file mode 100644 index 000000000000..483de47c1b31 --- /dev/null +++ b/packages/osd-charts/src/state/actions/key.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const ON_KEY_UP = 'ON_KEY_UP'; + +interface KeyUpAction { + type: typeof ON_KEY_UP; + /** + * Keyboard key from event + */ + key: string; +} + +/** + * Action called on `keyup` event + * @param key keyboard key + * @internal + */ +export function onKeyPress(key: string): KeyUpAction { + return { type: ON_KEY_UP, key }; +} + +/** @internal */ +export type KeyActions = KeyUpAction; diff --git a/packages/osd-charts/src/state/actions/legend.ts b/packages/osd-charts/src/state/actions/legend.ts new file mode 100644 index 000000000000..b4cd9f1c0e84 --- /dev/null +++ b/packages/osd-charts/src/state/actions/legend.ts @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CategoryKey } from '../../common/category'; +import { SeriesIdentifier } from '../../common/series_id'; + +/** @internal */ +export const ON_LEGEND_ITEM_OVER = 'ON_LEGEND_ITEM_OVER'; + +/** @internal */ +export const ON_LEGEND_ITEM_OUT = 'ON_LEGEND_ITEM_OUT'; + +/** @internal */ +export const ON_TOGGLE_DESELECT_SERIES = 'ON_TOGGLE_DESELECT_SERIES'; + +/** @public */ +export type LegendPathElement = { index: number; value: CategoryKey }; + +/** @public */ +export type LegendPath = LegendPathElement[]; + +interface LegendItemOverAction { + type: typeof ON_LEGEND_ITEM_OVER; + legendPath: LegendPath; +} +interface LegendItemOutAction { + type: typeof ON_LEGEND_ITEM_OUT; +} + +/** @internal */ +export interface ToggleDeselectSeriesAction { + type: typeof ON_TOGGLE_DESELECT_SERIES; + legendItemIds: SeriesIdentifier[]; + negate: boolean; +} + +/** @internal */ +export function onLegendItemOverAction(legendPath: LegendPath): LegendItemOverAction { + return { type: ON_LEGEND_ITEM_OVER, legendPath }; +} + +/** @internal */ +export function onLegendItemOutAction(): LegendItemOutAction { + return { type: ON_LEGEND_ITEM_OUT }; +} + +/** @internal */ +export function onToggleDeselectSeriesAction( + legendItemIds: SeriesIdentifier[], + negate = false, +): ToggleDeselectSeriesAction { + return { type: ON_TOGGLE_DESELECT_SERIES, legendItemIds, negate }; +} + +/** @internal */ +export type LegendActions = LegendItemOverAction | LegendItemOutAction | ToggleDeselectSeriesAction; diff --git a/packages/osd-charts/src/state/actions/mouse.ts b/packages/osd-charts/src/state/actions/mouse.ts new file mode 100644 index 000000000000..ae13e6cf8d01 --- /dev/null +++ b/packages/osd-charts/src/state/actions/mouse.ts @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Point } from '../../utils/point'; + +/** @internal */ +export const ON_POINTER_MOVE = 'ON_POINTER_MOVE'; + +/** @internal */ +export const ON_MOUSE_DOWN = 'ON_MOUSE_DOWN'; + +/** @internal */ +export const ON_MOUSE_UP = 'ON_MOUSE_UP'; + +interface MouseDownAction { + type: typeof ON_MOUSE_DOWN; + position: Point; + time: number; +} +interface MouseUpAction { + type: typeof ON_MOUSE_UP; + position: Point; + time: number; +} + +interface PointerMoveAction { + type: typeof ON_POINTER_MOVE; + position: Point; + time: number; +} + +/** + * Action called on mouse button down event + * @param position the x and y position (native event offsetX, offsetY) + * @param time the timestamp of the event (native event timeStamp) + * @internal + */ +export function onMouseDown(position: Point, time: number): MouseDownAction { + return { type: ON_MOUSE_DOWN, position, time }; +} + +/** + * Action called on mouse button up event + * @param position the x and y position (native event offsetX, offsetY) + * @param time the timestamp of the event (native event timeStamp) + * @internal + */ +export function onMouseUp(position: Point, time: number): MouseUpAction { + return { type: ON_MOUSE_UP, position, time }; +} + +/** + * Action called with the mouse coordinates relatives to the chart container (exclude the legend) + * @param position the x and y position (native event offsetX, offsetY) + * @param time the timestamp of the event (native event timeStamp) + * @internal + */ +export function onPointerMove(position: Point, time: number): PointerMoveAction { + return { type: ON_POINTER_MOVE, position, time }; +} + +/** @internal */ +export type MouseActions = MouseDownAction | MouseUpAction | PointerMoveAction; diff --git a/packages/osd-charts/src/state/actions/specs.ts b/packages/osd-charts/src/state/actions/specs.ts new file mode 100644 index 000000000000..7d668c6b9a41 --- /dev/null +++ b/packages/osd-charts/src/state/actions/specs.ts @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Spec } from '../../specs'; + +/** @internal */ +export const UPSERT_SPEC = 'UPSERT_SPEC'; + +/** @internal */ +export const REMOVE_SPEC = 'REMOVE_SPEC'; + +/** @internal */ +export const SPEC_PARSED = 'SPEC_PARSED'; + +/** @internal */ +export const SPEC_UNMOUNTED = 'SPEC_UNMOUNTED'; + +interface SpecParsedAction { + type: typeof SPEC_PARSED; +} + +interface SpecUnmountedAction { + type: typeof SPEC_UNMOUNTED; +} + +interface UpsertSpecAction { + type: typeof UPSERT_SPEC; + spec: Spec; +} + +interface RemoveSpecAction { + type: typeof REMOVE_SPEC; + id: string; +} + +/** @internal */ +export function upsertSpec(spec: Spec): UpsertSpecAction { + return { type: UPSERT_SPEC, spec }; +} + +/** @internal */ +export function removeSpec(id: string): RemoveSpecAction { + return { type: REMOVE_SPEC, id }; +} + +/** @internal */ +export function specParsed(): SpecParsedAction { + return { type: SPEC_PARSED }; +} + +/** @internal */ +export function specUnmounted(): SpecUnmountedAction { + return { type: SPEC_UNMOUNTED }; +} + +/** @internal */ +export type SpecActions = SpecParsedAction | SpecUnmountedAction | UpsertSpecAction | RemoveSpecAction; diff --git a/packages/osd-charts/src/state/actions/z_index.ts b/packages/osd-charts/src/state/actions/z_index.ts new file mode 100644 index 000000000000..0d3c38f95b06 --- /dev/null +++ b/packages/osd-charts/src/state/actions/z_index.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const Z_INDEX_EVENT = 'Z_INDEX_EVENT'; + +interface ZIndexEvent { + type: typeof Z_INDEX_EVENT; + zIndex: number; +} + +/** @internal */ +export function onComputedZIndex(zIndex: number): ZIndexEvent { + return { type: Z_INDEX_EVENT, zIndex }; +} + +/** @internal */ +export type ZIndexActions = ZIndexEvent; diff --git a/packages/osd-charts/src/state/chart_state.ts b/packages/osd-charts/src/state/chart_state.ts new file mode 100644 index 000000000000..5e736bad99e5 --- /dev/null +++ b/packages/osd-charts/src/state/chart_state.ts @@ -0,0 +1,439 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { RefObject } from 'react'; + +import { ChartType } from '../chart_types'; +import { GoalState } from '../chart_types/goal_chart/state/chart_state'; +import { HeatmapState } from '../chart_types/heatmap/state/chart_state'; +import { PartitionState } from '../chart_types/partition_chart/state/chart_state'; +import { WordcloudState } from '../chart_types/wordcloud/state/chart_state'; +import { XYAxisChartState } from '../chart_types/xy_chart/state/chart_state'; +import { CategoryKey } from '../common/category'; +import { LegendItem, LegendItemExtraValues } from '../common/legend'; +import { SeriesIdentifier, SeriesKey } from '../common/series_id'; +import { AnchorPosition } from '../components/portal/types'; +import { TooltipInfo } from '../components/tooltip/types'; +import { DEFAULT_SETTINGS_SPEC, PointerEvent, Spec } from '../specs'; +import { Color, keepDistinct } from '../utils/common'; +import { Dimensions } from '../utils/dimensions'; +import { Logger } from '../utils/logger'; +import { Point } from '../utils/point'; +import { StateActions } from './actions'; +import { CHART_RENDERED } from './actions/chart'; +import { UPDATE_PARENT_DIMENSION } from './actions/chart_settings'; +import { CLEAR_TEMPORARY_COLORS, SET_PERSISTED_COLOR, SET_TEMPORARY_COLOR } from './actions/colors'; +import { DOMElement } from './actions/dom_element'; +import { EXTERNAL_POINTER_EVENT } from './actions/events'; +import { LegendPath } from './actions/legend'; +import { REMOVE_SPEC, SPEC_PARSED, SPEC_UNMOUNTED, UPSERT_SPEC } from './actions/specs'; +import { Z_INDEX_EVENT } from './actions/z_index'; +import { interactionsReducer } from './reducers/interactions'; +import { getInternalIsInitializedSelector, InitStatus } from './selectors/get_internal_is_intialized'; +import { getLegendItemsSelector } from './selectors/get_legend_items'; +import { LegendItemLabel } from './selectors/get_legend_items_labels'; +import { DebugState } from './types'; +import { getInitialPointerState } from './utils'; + +/** @internal */ +export type BackwardRef = () => React.RefObject; + +/** + * A set of chart-type-dependant functions that required by all chart type + * @internal + */ +export interface InternalChartState { + /** + * The chart type + */ + chartType: ChartType; + isInitialized(globalState: GlobalChartState): InitStatus; + /** + * Returns a JSX element with the chart rendered (lenged excluded) + * @param containerRef + * @param forwardStageRef + */ + chartRenderer(containerRef: BackwardRef, forwardStageRef: RefObject): JSX.Element | null; + /** + * `true` if the brush is available for this chart type + * @param globalState + */ + isBrushAvailable(globalState: GlobalChartState): boolean; + /** + * `true` if the brush is available for this chart type + * @param globalState + */ + isBrushing(globalState: GlobalChartState): boolean; + /** + * `true` if the chart is empty (no data displayed) + * @param globalState + */ + isChartEmpty(globalState: GlobalChartState): boolean; + + /** + * Returns the list of legend items labels. Mainly used to compute the legend size + * based on labels and their hierarchy depth. + * @param globalState + */ + getLegendItemsLabels(globalState: GlobalChartState): LegendItemLabel[]; + + /** + * Returns the list of legend items. + * @param globalState + */ + getLegendItems(globalState: GlobalChartState): LegendItem[]; + /** + * Returns the list of extra values for each legend item + * @param globalState + */ + getLegendExtraValues(globalState: GlobalChartState): Map; + /** + * Returns the CSS pointer cursor depending on the internal chart state + * @param globalState + */ + getPointerCursor(globalState: GlobalChartState): string; + /** + * Describe if the tooltip is visible and comes from an external source + * @param globalState + */ + isTooltipVisible(globalState: GlobalChartState): { visible: boolean; isExternal: boolean }; + /** + * Get the tooltip information to display + * @param globalState the GlobalChartState + */ + getTooltipInfo(globalState: GlobalChartState): TooltipInfo | undefined; + + /** + * Get the tooltip anchor position + * @param globalState + */ + getTooltipAnchor(globalState: GlobalChartState): AnchorPosition | null; + + /** + * Called on every state change to activate any event callback + * @param globalState + */ + eventCallbacks(globalState: GlobalChartState): void; + + /** + * Get the chart main projection area: exclude legends, axis and other external marks + * @param globalState + */ + getMainProjectionArea(globalState: GlobalChartState): Dimensions; + + /** + * Get the chart container projection area + * @param globalState + */ + getProjectionContainerArea(globalState: GlobalChartState): Dimensions; + + /** + * Get the brushed area if available + * @param globalState + */ + getBrushArea(globalState: GlobalChartState): Dimensions | null; + + /** + * Get debug state of chart + * @param globalState + */ + getDebugState(globalState: GlobalChartState): DebugState; + + /** + * Get the series types for the screen reader summary component + */ + getChartTypeDescription(globalState: GlobalChartState): string; +} + +/** @internal */ +export interface SpecList { + [specId: string]: Spec; +} + +/** @internal */ +export interface PointerState { + position: Point; + time: number; +} +/** @internal */ +export interface DragState { + start: PointerState; + end: PointerState; +} + +/** @internal */ +export interface PointerStates { + dragging: boolean; + current: PointerState; + down: PointerState | null; + up: PointerState | null; + lastDrag: DragState | null; + lastClick: PointerState | null; +} + +/** @internal */ +export interface InteractionsState { + pointer: PointerStates; + highlightedLegendPath: LegendPath; + deselectedDataSeries: SeriesIdentifier[]; + hoveredDOMElement: DOMElement | null; + drilldown: CategoryKey[]; + prevDrilldown: CategoryKey[]; +} + +/** @internal */ +export interface ExternalEventsState { + pointer: PointerEvent | null; +} + +/** @internal */ +export interface ColorOverrides { + temporary: Record; // null (vs. undefined) means that `overrides.persisted[key]` in `series.ts` not be used + persisted: Record; +} + +/** @internal */ +export type ChartId = string; + +/** @internal */ +export interface GlobalChartState { + /** + * a unique ID for each chart used by re-reselect to memoize selector per chart + */ + chartId: ChartId; + /** + * The Z-Index of the chart component + */ + zIndex: number; + /** + * true when all all the specs are parsed ad stored into the specs object + */ + specsInitialized: boolean; + specParsing: boolean; + /** + * true if the chart is rendered on dom + */ + chartRendered: boolean; + /** + * incremental count of the chart rendering + */ + chartRenderedCount: number; + /** + * the map of parsed specs + */ + specs: SpecList; + /** + * the chart type depending on the used specs + */ + chartType: ChartType | null; + /** + * a chart-type-dependant class that is used to render and share chart-type dependant functions + */ + internalChartState: InternalChartState | null; + /** + * the dimensions of the parent container, including the legend + */ + parentDimensions: Dimensions; + /** + * the state of the interactions + */ + interactions: InteractionsState; + /** + * external event state + */ + externalEvents: ExternalEventsState; + /** + * Color map used to persist color picker changes + */ + colors: ColorOverrides; +} + +/** @internal */ +export const getInitialState = (chartId: string): GlobalChartState => ({ + chartId, + zIndex: 0, + specsInitialized: false, + specParsing: false, + chartRendered: false, + chartRenderedCount: 0, + specs: { + [DEFAULT_SETTINGS_SPEC.id]: DEFAULT_SETTINGS_SPEC, + }, + colors: { + temporary: {}, + persisted: {}, + }, + chartType: null, + internalChartState: null, + interactions: { + pointer: getInitialPointerState(), + highlightedLegendPath: [], + deselectedDataSeries: [], + hoveredDOMElement: null, + drilldown: [], + prevDrilldown: [], + }, + externalEvents: { + pointer: null, + }, + parentDimensions: { + height: 0, + width: 0, + left: 0, + top: 0, + }, +}); + +/** @internal */ +export const chartStoreReducer = (chartId: string) => { + const initialState = getInitialState(chartId); + return (state = initialState, action: StateActions): GlobalChartState => { + switch (action.type) { + case Z_INDEX_EVENT: + return { + ...state, + zIndex: action.zIndex, + }; + case SPEC_PARSED: + const chartType = chartTypeFromSpecs(state.specs); + return { + ...state, + specsInitialized: true, + specParsing: false, + chartType, + internalChartState: state.chartType === chartType ? state.internalChartState : newInternalState(chartType), + }; + case SPEC_UNMOUNTED: + return { + ...state, + specsInitialized: false, + chartRendered: false, + }; + case UPSERT_SPEC: + return { + ...state, + specsInitialized: false, + chartRendered: false, + specParsing: true, + specs: state.specParsing + ? { ...state.specs, [action.spec.id]: action.spec } + : { [DEFAULT_SETTINGS_SPEC.id]: DEFAULT_SETTINGS_SPEC, [action.spec.id]: action.spec }, + }; + case REMOVE_SPEC: + const { [action.id]: specToRemove, ...rest } = state.specs; + return { + ...state, + specsInitialized: false, + chartRendered: false, + specParsing: false, + specs: { + ...rest, + }, + }; + case CHART_RENDERED: + const count = state.chartRendered ? state.chartRenderedCount : state.chartRenderedCount + 1; + return { + ...state, + chartRendered: true, + chartRenderedCount: count, + }; + case UPDATE_PARENT_DIMENSION: + return { + ...state, + interactions: { ...state.interactions, prevDrilldown: state.interactions.drilldown }, + parentDimensions: { + ...action.dimensions, + }, + }; + case EXTERNAL_POINTER_EVENT: + // discard events from self if any + return { + ...state, + externalEvents: { + ...state.externalEvents, + pointer: action.event.chartId === chartId ? null : action.event, + }, + }; + case CLEAR_TEMPORARY_COLORS: + return { + ...state, + colors: { + ...state.colors, + temporary: {}, + }, + }; + case SET_TEMPORARY_COLOR: + return { + ...state, + colors: { + ...state.colors, + temporary: { + ...state.colors.temporary, + ...action.keys.reduce>((acc, curr) => { + acc[curr] = action.color; + return acc; + }, {}), + }, + }, + }; + case SET_PERSISTED_COLOR: + return { + ...state, + colors: { + ...state.colors, + persisted: Object.fromEntries( + Object.entries(state.colors.persisted).filter(([key]) => !action.keys.includes(key)), + ), + }, + }; + default: + return getInternalIsInitializedSelector(state) === InitStatus.Initialized + ? { + ...state, + interactions: interactionsReducer(state, action, getLegendItemsSelector(state)), + } + : state; + } + }; +}; + +function chartTypeFromSpecs(specs: SpecList): ChartType | null { + const nonGlobalTypes = Object.values(specs) + .map((s) => s.chartType) + .filter((type) => type !== ChartType.Global) + .filter(keepDistinct); + if (nonGlobalTypes.length !== 1) { + Logger.warn(`${nonGlobalTypes.length === 0 ? 'Zero' : 'Multiple'} chart types in the same configuration`); + return null; + } + return nonGlobalTypes[0]; +} + +const constructors: Record InternalChartState | null> = { + [ChartType.Goal]: () => new GoalState(), + [ChartType.Partition]: () => new PartitionState(), + [ChartType.XYAxis]: () => new XYAxisChartState(), + [ChartType.Heatmap]: () => new HeatmapState(), + [ChartType.Wordcloud]: () => new WordcloudState(), + [ChartType.Global]: () => null, +}; // with no default, TS signals if a new chart type isn't added here too + +function newInternalState(chartType: ChartType | null): InternalChartState | null { + return chartType ? constructors[chartType]() : null; +} diff --git a/packages/osd-charts/src/state/reducers/interactions.ts b/packages/osd-charts/src/state/reducers/interactions.ts new file mode 100644 index 000000000000..879a1b71936d --- /dev/null +++ b/packages/osd-charts/src/state/reducers/interactions.ts @@ -0,0 +1,214 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartType } from '../../chart_types'; +import { drilldownActive } from '../../chart_types/partition_chart/state/selectors/drilldown_active'; +import { getPickedShapesLayerValues } from '../../chart_types/partition_chart/state/selectors/picked_shapes'; +import { LegendItem } from '../../common/legend'; +import { SeriesIdentifier } from '../../common/series_id'; +import { LayerValue } from '../../specs'; +import { getDelta } from '../../utils/point'; +import { DOMElementActions, ON_DOM_ELEMENT_ENTER, ON_DOM_ELEMENT_LEAVE } from '../actions/dom_element'; +import { KeyActions, ON_KEY_UP } from '../actions/key'; +import { + LegendActions, + ON_LEGEND_ITEM_OUT, + ON_LEGEND_ITEM_OVER, + ON_TOGGLE_DESELECT_SERIES, + ToggleDeselectSeriesAction, +} from '../actions/legend'; +import { MouseActions, ON_MOUSE_DOWN, ON_MOUSE_UP, ON_POINTER_MOVE } from '../actions/mouse'; +import { GlobalChartState, InteractionsState } from '../chart_state'; +import { getInitialPointerState } from '../utils'; + +/** + * The minimum amount of time to consider for for dragging purposes + * @internal + */ +export const DRAG_DETECTION_TIMEOUT = 100; +/** + * The minimum number of pixel between two pointer positions to consider for dragging purposes + */ +const DRAG_DETECTION_PIXEL_DELTA = 4; + +/** @internal */ +export function interactionsReducer( + globalState: GlobalChartState, + action: LegendActions | MouseActions | KeyActions | DOMElementActions, + legendItems: LegendItem[], +): InteractionsState { + const { interactions: state } = globalState; + switch (action.type) { + case ON_KEY_UP: + if (action.key === 'Escape') { + return { + ...state, + pointer: getInitialPointerState(), + }; + } + + return state; + + case ON_POINTER_MOVE: + // enable the dragging flag only if the pixel delta between down and move is greater then 4 pixel + const dragging = + !!state.pointer.down && getDelta(state.pointer.down.position, action.position) > DRAG_DETECTION_PIXEL_DELTA; + return { + ...state, + pointer: { + ...state.pointer, + dragging, + current: { + position: { + ...action.position, + }, + time: action.time, + }, + }, + }; + case ON_MOUSE_DOWN: + return { + ...state, + drilldown: getDrilldownData(globalState), + prevDrilldown: state.drilldown, + pointer: { + ...state.pointer, + dragging: false, + up: null, + down: { + position: { + ...action.position, + }, + time: action.time, + }, + }, + }; + case ON_MOUSE_UP: { + return { + ...state, + pointer: { + ...state.pointer, + lastDrag: + state.pointer.down && state.pointer.dragging + ? { + start: { + position: { + ...state.pointer.down.position, + }, + time: state.pointer.down.time, + }, + end: { + position: { + ...state.pointer.current.position, + }, + time: action.time, + }, + } + : null, + lastClick: + state.pointer.down && !state.pointer.dragging + ? { + position: { + ...action.position, + }, + time: action.time, + } + : null, + dragging: false, + down: null, + up: { + position: { + ...action.position, + }, + time: action.time, + }, + }, + }; + } + case ON_LEGEND_ITEM_OUT: + return { + ...state, + highlightedLegendPath: [], + }; + case ON_LEGEND_ITEM_OVER: + const { legendPath: highlightedLegendPath } = action; + return { + ...state, + highlightedLegendPath, + }; + case ON_TOGGLE_DESELECT_SERIES: + return { + ...state, + deselectedDataSeries: toggleDeselectedDataSeries(action, state.deselectedDataSeries, legendItems), + }; + + case ON_DOM_ELEMENT_ENTER: + return { + ...state, + hoveredDOMElement: action.element, + }; + case ON_DOM_ELEMENT_LEAVE: + return { + ...state, + hoveredDOMElement: null, + }; + default: + return state; + } +} + +/** + * Helper functions that currently depend on chart type eg. xy or partition + */ + +function toggleDeselectedDataSeries( + { legendItemIds, negate }: ToggleDeselectSeriesAction, + deselectedDataSeries: SeriesIdentifier[], + legendItems: LegendItem[], +) { + const actionSeriesKeys = legendItemIds.map(({ key }) => key); + const deselectedDataSeriesKeys = new Set(deselectedDataSeries.map(({ key }) => key)); + const legendItemsKeys = legendItems.map(({ seriesIdentifiers }) => seriesIdentifiers); + + const alreadyDeselected = actionSeriesKeys.every((key) => deselectedDataSeriesKeys.has(key)); + + if (negate) { + if (!alreadyDeselected && deselectedDataSeries.length === legendItemsKeys.length - 1) { + return legendItemIds; + } + + return legendItems + .map(({ seriesIdentifiers }) => seriesIdentifiers) + .flat() + .filter(({ key }) => !actionSeriesKeys.includes(key)); + } + + if (alreadyDeselected) { + return deselectedDataSeries.filter(({ key }) => !actionSeriesKeys.includes(key)); + } + return [...deselectedDataSeries, ...legendItemIds]; +} + +function getDrilldownData(globalState: GlobalChartState) { + if (globalState.chartType !== ChartType.Partition || !drilldownActive(globalState)) { + return []; + } + const layerValues: LayerValue[] = getPickedShapesLayerValues(globalState)[0]; + return layerValues ? layerValues[layerValues.length - 1].path.map((n) => n.value) : []; +} diff --git a/packages/osd-charts/src/state/selectors/get_accessibility_config.ts b/packages/osd-charts/src/state/selectors/get_accessibility_config.ts new file mode 100644 index 000000000000..b6d0ae528acb --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_accessibility_config.ts @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { DEFAULT_SETTINGS_SPEC } from '../../specs/constants'; +import { SettingsSpec } from '../../specs/settings'; +import { isDefined } from '../../utils/common'; +import { GlobalChartState } from '../chart_state'; +import { getChartIdSelector } from './get_chart_id'; +import { getSettingsSpecSelector } from './get_settings_specs'; + +/** @internal */ +export const getSpecs = (state: GlobalChartState) => state.specs; + +/** @internal */ +export type A11ySettings = { + label?: string; + labelId?: string; + labelHeadingLevel: SettingsSpec['ariaLabelHeadingLevel']; + description?: string; + descriptionId?: string; + defaultSummaryId?: string; +}; + +/** @internal */ +export const DEFAULT_A11Y_SETTINGS: A11ySettings = { + labelHeadingLevel: DEFAULT_SETTINGS_SPEC.ariaLabelHeadingLevel, +}; + +/** @internal */ +export const getA11ySettingsSelector = createCachedSelector( + [getSettingsSpecSelector, getChartIdSelector], + ( + { ariaDescription, ariaDescribedBy, ariaLabel, ariaLabelledBy, ariaUseDefaultSummary, ariaLabelHeadingLevel }, + chartId, + ) => { + const defaultSummaryId = ariaUseDefaultSummary ? `${chartId}--defaultSummary` : undefined; + // use ariaDescribedBy if present, or create a description element if ariaDescription is present. + // concat also if default summary id if requested + const describeBy = [ariaDescribedBy ?? (ariaDescription && `${chartId}--desc`), defaultSummaryId].filter(isDefined); + + return { + // don't render a label if a labelledBy id is provided + label: ariaLabelledBy ? undefined : ariaLabel, + // use ariaLabelledBy if present, or create an internal label if ariaLabel is present + labelId: ariaLabelledBy ?? (ariaLabel && `${chartId}--label`), + labelHeadingLevel: isValidHeadingLevel(ariaLabelHeadingLevel) + ? ariaLabelHeadingLevel + : DEFAULT_A11Y_SETTINGS.labelHeadingLevel, + // don't use a description if ariaDescribedBy id is provided + description: ariaDescribedBy ? undefined : ariaDescription, + // concat all the ids + descriptionId: describeBy.length > 0 ? describeBy.join(' ') : undefined, + defaultSummaryId, + }; + }, +)(getChartIdSelector); + +function isValidHeadingLevel(ariaLabelHeadingLevel: SettingsSpec['ariaLabelHeadingLevel']): boolean { + return ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'].includes(ariaLabelHeadingLevel); +} diff --git a/packages/osd-charts/src/state/selectors/get_chart_container_dimensions.ts b/packages/osd-charts/src/state/selectors/get_chart_container_dimensions.ts new file mode 100644 index 000000000000..f3fb482bb744 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_chart_container_dimensions.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { LayoutDirection } from '../../utils/common'; +import { Dimensions } from '../../utils/dimensions'; +import { GlobalChartState } from '../chart_state'; +import { getChartIdSelector } from './get_chart_id'; +import { getLegendConfigSelector } from './get_legend_config_selector'; +import { getLegendSizeSelector } from './get_legend_size'; + +const getParentDimension = (state: GlobalChartState) => state.parentDimensions; + +/** @internal */ +export const getChartContainerDimensionsSelector = createCachedSelector( + [getLegendConfigSelector, getLegendSizeSelector, getParentDimension], + ({ showLegend, legendPosition: { floating, direction } }, legendSize, parentDimensions): Dimensions => { + if (!showLegend || floating) { + return parentDimensions; + } + if (direction === LayoutDirection.Vertical) { + return { + left: 0, + top: 0, + width: parentDimensions.width - legendSize.width - legendSize.margin * 2, + height: parentDimensions.height, + }; + } + return { + left: 0, + top: 0, + width: parentDimensions.width, + height: parentDimensions.height - legendSize.height - legendSize.margin * 2, + }; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/state/selectors/get_chart_id.ts b/packages/osd-charts/src/state/selectors/get_chart_id.ts new file mode 100644 index 000000000000..add7928beb53 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_chart_id.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export const getChartIdSelector = (state: GlobalChartState) => state.chartId; diff --git a/packages/osd-charts/src/state/selectors/get_chart_rotation.ts b/packages/osd-charts/src/state/selectors/get_chart_rotation.ts new file mode 100644 index 000000000000..9867869ce11b --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_chart_rotation.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { Rotation } from '../../utils/common'; +import { getChartIdSelector } from './get_chart_id'; +import { getSettingsSpecSelector } from './get_settings_specs'; + +/** @internal */ +export const getChartRotationSelector = createCachedSelector( + [getSettingsSpecSelector], + (settingsSpec): Rotation => settingsSpec.rotation, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/state/selectors/get_chart_theme.ts b/packages/osd-charts/src/state/selectors/get_chart_theme.ts new file mode 100644 index 000000000000..bd514ee601c2 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_chart_theme.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { LIGHT_THEME } from '../../utils/themes/light_theme'; +import { mergeWithDefaultTheme } from '../../utils/themes/merge_utils'; +import { PartialTheme, Theme } from '../../utils/themes/theme'; +import { getChartIdSelector } from './get_chart_id'; +import { getSettingsSpecSelector } from './get_settings_specs'; + +/** @internal */ +export const getChartThemeSelector = createCachedSelector( + [getSettingsSpecSelector], + (settingsSpec): Theme => getTheme(settingsSpec.baseTheme, settingsSpec.theme), +)(getChartIdSelector); + +function getTheme(baseTheme?: Theme, theme?: PartialTheme | PartialTheme[]): Theme { + const base = baseTheme || LIGHT_THEME; + + if (Array.isArray(theme)) { + const [firstTheme, ...axillaryThemes] = theme; + return mergeWithDefaultTheme(firstTheme, base, axillaryThemes); + } + + return theme ? mergeWithDefaultTheme(theme, base) : base; +} diff --git a/packages/osd-charts/src/state/selectors/get_chart_type_components.ts b/packages/osd-charts/src/state/selectors/get_chart_type_components.ts new file mode 100644 index 000000000000..643610be8da7 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_chart_type_components.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { GlobalChartState, BackwardRef } from '../chart_state'; + +type ChartRendererFn = ( + containerRef: BackwardRef, + forwardStageRef: React.RefObject, +) => JSX.Element | null; + +/** @internal */ +export const getInternalChartRendererSelector = (state: GlobalChartState): ChartRendererFn => { + if (state.internalChartState) { + return state.internalChartState.chartRenderer; + } + return () => null; +}; diff --git a/packages/osd-charts/src/state/selectors/get_chart_type_description.ts b/packages/osd-charts/src/state/selectors/get_chart_type_description.ts new file mode 100644 index 000000000000..771a9b26c9d8 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_chart_type_description.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export const getChartTypeDescriptionSelector = (state: GlobalChartState): string => { + if (state.internalChartState) { + return state.internalChartState.getChartTypeDescription(state); + } + // need to return something so there is always a string returned + return 'unknown chart type'; +}; diff --git a/packages/osd-charts/src/state/selectors/get_debug_state.ts b/packages/osd-charts/src/state/selectors/get_debug_state.ts new file mode 100644 index 000000000000..f71421ea43bd --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_debug_state.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { GlobalChartState } from '../chart_state'; +import { DebugState } from '../types'; +import { getChartContainerDimensionsSelector } from './get_chart_container_dimensions'; + +/** @internal */ +export const getDebugStateSelector = (state: GlobalChartState): DebugState => { + if (state.internalChartState) { + const { height, width } = getChartContainerDimensionsSelector(state); + if (height * width > 0) { + return state.internalChartState.getDebugState(state); + } + } + return {}; +}; diff --git a/packages/osd-charts/src/state/selectors/get_deselected_data_series.ts b/packages/osd-charts/src/state/selectors/get_deselected_data_series.ts new file mode 100644 index 000000000000..ecfb6646e9b9 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_deselected_data_series.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export const getDeselectedSeriesSelector = (state: GlobalChartState) => state.interactions.deselectedDataSeries; diff --git a/packages/osd-charts/src/state/selectors/get_internal_brush_area.ts b/packages/osd-charts/src/state/selectors/get_internal_brush_area.ts new file mode 100644 index 000000000000..a5ee48e771d1 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_internal_brush_area.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Dimensions } from '../../utils/dimensions'; +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export const getInternalBrushAreaSelector = (state: GlobalChartState): Dimensions | null => { + if (state.internalChartState) { + return state.internalChartState.getBrushArea(state); + } + return null; +}; diff --git a/packages/osd-charts/src/state/selectors/get_internal_cursor_pointer.ts b/packages/osd-charts/src/state/selectors/get_internal_cursor_pointer.ts new file mode 100644 index 000000000000..c69696019aad --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_internal_cursor_pointer.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DEFAULT_CSS_CURSOR } from '../../common/constants'; +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export const getInternalPointerCursor = (state: GlobalChartState): string => { + return state.internalChartState?.getPointerCursor(state) ?? DEFAULT_CSS_CURSOR; +}; diff --git a/packages/osd-charts/src/state/selectors/get_internal_is_brushing.ts b/packages/osd-charts/src/state/selectors/get_internal_is_brushing.ts new file mode 100644 index 000000000000..f59e852c63b3 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_internal_is_brushing.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export const getInternalIsBrushingSelector = (state: GlobalChartState): boolean => { + if (state.internalChartState) { + return state.internalChartState.isBrushing(state); + } + return false; +}; diff --git a/packages/osd-charts/src/state/selectors/get_internal_is_brushing_available.ts b/packages/osd-charts/src/state/selectors/get_internal_is_brushing_available.ts new file mode 100644 index 000000000000..d010f035c3f3 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_internal_is_brushing_available.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export const getInternalIsBrushingAvailableSelector = (state: GlobalChartState): boolean => { + if (state.internalChartState) { + return state.internalChartState.isBrushAvailable(state); + } + return false; +}; diff --git a/packages/osd-charts/src/state/selectors/get_internal_is_intialized.ts b/packages/osd-charts/src/state/selectors/get_internal_is_intialized.ts new file mode 100644 index 000000000000..98771a1eb027 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_internal_is_intialized.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $Values } from 'utility-types'; + +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export const InitStatus = Object.freeze({ + ParentSizeInvalid: 'ParentSizeInvalid' as const, + SpecNotInitialized: 'SpecNotInitialized' as const, + MissingChartType: 'MissingChartType' as const, + ChartNotInitialized: 'ChartNotInitialized' as const, + Initialized: 'Initialized' as const, +}); + +/** @internal */ +export type InitStatus = $Values; + +/** @internal */ +export const getInternalIsInitializedSelector = (state: GlobalChartState): InitStatus => { + const { + parentDimensions: { width, height }, + specsInitialized, + internalChartState, + } = state; + + if (!specsInitialized) { + return InitStatus.SpecNotInitialized; + } + + if (!internalChartState) { + return InitStatus.MissingChartType; + } + + if (width <= 0 || height <= 0) { + return InitStatus.ParentSizeInvalid; + } + + return internalChartState.isInitialized(state); +}; diff --git a/packages/osd-charts/src/state/selectors/get_internal_is_tooltip_visible.ts b/packages/osd-charts/src/state/selectors/get_internal_is_tooltip_visible.ts new file mode 100644 index 000000000000..ebe096478f4a --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_internal_is_tooltip_visible.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export const getInternalIsTooltipVisibleSelector = ( + state: GlobalChartState, +): { visible: boolean; isExternal: boolean } => { + if (state.internalChartState) { + return state.internalChartState.isTooltipVisible(state); + } + return { visible: false, isExternal: false }; +}; diff --git a/packages/osd-charts/src/state/selectors/get_internal_main_projection_area.ts b/packages/osd-charts/src/state/selectors/get_internal_main_projection_area.ts new file mode 100644 index 000000000000..cdc977088a3a --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_internal_main_projection_area.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Dimensions } from '../../utils/dimensions'; +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export const getInternalMainProjectionAreaSelector = (state: GlobalChartState): Dimensions => { + if (state.internalChartState) { + return state.internalChartState.getMainProjectionArea(state); + } + return { width: 0, height: 0, left: 0, top: 0 }; +}; diff --git a/packages/osd-charts/src/state/selectors/get_internal_projection_container_area.ts b/packages/osd-charts/src/state/selectors/get_internal_projection_container_area.ts new file mode 100644 index 000000000000..6f47605ea7cf --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_internal_projection_container_area.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Dimensions } from '../../utils/dimensions'; +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export const getInternalProjectionContainerAreaSelector = (state: GlobalChartState): Dimensions => { + if (state.internalChartState) { + return state.internalChartState.getProjectionContainerArea(state); + } + return { width: 0, height: 0, left: 0, top: 0 }; +}; diff --git a/packages/osd-charts/src/state/selectors/get_internal_tooltip_anchor_position.ts b/packages/osd-charts/src/state/selectors/get_internal_tooltip_anchor_position.ts new file mode 100644 index 000000000000..ab078a5a03a4 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_internal_tooltip_anchor_position.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AnchorPosition } from '../../components/portal/types'; +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export const getInternalTooltipAnchorPositionSelector = (state: GlobalChartState): AnchorPosition | null => { + if (state.internalChartState) { + return state.internalChartState.getTooltipAnchor(state); + } + return null; +}; diff --git a/packages/osd-charts/src/state/selectors/get_internal_tooltip_info.ts b/packages/osd-charts/src/state/selectors/get_internal_tooltip_info.ts new file mode 100644 index 000000000000..b026d8599ad8 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_internal_tooltip_info.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TooltipInfo } from '../../components/tooltip/types'; +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export const getInternalTooltipInfoSelector = (state: GlobalChartState): TooltipInfo | undefined => { + if (state.internalChartState) { + return state.internalChartState.getTooltipInfo(state); + } +}; diff --git a/packages/osd-charts/src/state/selectors/get_last_click.ts b/packages/osd-charts/src/state/selectors/get_last_click.ts new file mode 100644 index 000000000000..ec17cd04948a --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_last_click.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export function getLastClickSelector(state: GlobalChartState) { + return state.interactions.pointer.lastClick; +} diff --git a/packages/osd-charts/src/state/selectors/get_last_drag.ts b/packages/osd-charts/src/state/selectors/get_last_drag.ts new file mode 100644 index 000000000000..2957395506e9 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_last_drag.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export function getLastDragSelector(state: GlobalChartState) { + return state.interactions.pointer.lastDrag; +} diff --git a/packages/osd-charts/src/state/selectors/get_legend_config_selector.ts b/packages/osd-charts/src/state/selectors/get_legend_config_selector.ts new file mode 100644 index 000000000000..3ed23e02757b --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_legend_config_selector.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { getLegendPositionConfig } from '../../components/legend/position_style'; +import { getChartIdSelector } from './get_chart_id'; +import { getSettingsSpecSelector } from './get_settings_specs'; + +/** @internal */ +export const getLegendConfigSelector = createCachedSelector( + [getSettingsSpecSelector], + ({ + flatLegend, + legendAction, + legendColorPicker, + legendMaxDepth, + legendPosition, + legendStrategy, + onLegendItemClick, + showLegend, + onLegendItemMinusClick, + onLegendItemOut, + onLegendItemOver, + onLegendItemPlusClick, + showLegendExtra, + }) => { + return { + flatLegend, + legendAction, + legendColorPicker, + legendMaxDepth, + legendPosition: getLegendPositionConfig(legendPosition), + legendStrategy, + onLegendItemClick, + showLegend, + onLegendItemMinusClick, + onLegendItemOut, + onLegendItemOver, + onLegendItemPlusClick, + showLegendExtra, + }; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/state/selectors/get_legend_items.ts b/packages/osd-charts/src/state/selectors/get_legend_items.ts new file mode 100644 index 000000000000..c0b645205012 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_legend_items.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegendItem } from '../../common/legend'; +import { GlobalChartState } from '../chart_state'; + +const EMPTY_LEGEND_LIST: LegendItem[] = []; + +/** @internal */ +export const getLegendItemsSelector = (state: GlobalChartState): LegendItem[] => { + if (state.internalChartState) { + return state.internalChartState.getLegendItems(state); + } + return EMPTY_LEGEND_LIST; +}; diff --git a/packages/osd-charts/src/state/selectors/get_legend_items_labels.ts b/packages/osd-charts/src/state/selectors/get_legend_items_labels.ts new file mode 100644 index 000000000000..bc9524bf9077 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_legend_items_labels.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export interface LegendItemLabel { + label: string; + depth: number; +} + +/** @internal */ +export const getLegendItemsLabelsSelector = (state: GlobalChartState): LegendItemLabel[] => + state.internalChartState?.getLegendItemsLabels(state) ?? []; diff --git a/packages/osd-charts/src/state/selectors/get_legend_items_values.ts b/packages/osd-charts/src/state/selectors/get_legend_items_values.ts new file mode 100644 index 000000000000..a486e93c7c6c --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_legend_items_values.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegendItemExtraValues } from '../../common/legend'; +import { SeriesKey } from '../../common/series_id'; +import { GlobalChartState } from '../chart_state'; + +const EMPTY_ITEM_LIST = new Map(); + +/** @internal */ +export const getLegendExtraValuesSelector = (state: GlobalChartState): Map => { + if (state.internalChartState) { + return state.internalChartState.getLegendExtraValues(state); + } + return EMPTY_ITEM_LIST; +}; diff --git a/packages/osd-charts/src/state/selectors/get_legend_size.ts b/packages/osd-charts/src/state/selectors/get_legend_size.ts new file mode 100644 index 000000000000..26ce65042003 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_legend_size.ts @@ -0,0 +1,110 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { LEGEND_HIERARCHY_MARGIN } from '../../components/legend/legend_item'; +import { LEGEND_TO_FULL_CONFIG } from '../../components/legend/position_style'; +import { LegendPositionConfig } from '../../specs/settings'; +import { BBox } from '../../utils/bbox/bbox_calculator'; +import { CanvasTextBBoxCalculator } from '../../utils/bbox/canvas_text_bbox_calculator'; +import { Position, isDefined, LayoutDirection } from '../../utils/common'; +import { GlobalChartState } from '../chart_state'; +import { getChartIdSelector } from './get_chart_id'; +import { getChartThemeSelector } from './get_chart_theme'; +import { getLegendConfigSelector } from './get_legend_config_selector'; +import { getLegendItemsLabelsSelector } from './get_legend_items_labels'; + +const getParentDimensionSelector = (state: GlobalChartState) => state.parentDimensions; + +const SCROLL_BAR_WIDTH = 16; // ~1em +const MARKER_WIDTH = 16; +const SHARED_MARGIN = 4; +const VERTICAL_PADDING = 4; +const TOP_MARGIN = 2; + +/** @internal */ +export type LegendSizing = BBox & { + margin: number; + position: LegendPositionConfig; +}; + +/** @internal */ +export const getLegendSizeSelector = createCachedSelector( + [getLegendConfigSelector, getChartThemeSelector, getParentDimensionSelector, getLegendItemsLabelsSelector], + (legendConfig, theme, parentDimensions, labels): LegendSizing => { + if (!legendConfig.showLegend) { + return { width: 0, height: 0, margin: 0, position: LEGEND_TO_FULL_CONFIG[Position.Right] }; + } + + const bboxCalculator = new CanvasTextBBoxCalculator(); + const bbox = labels.reduce( + (acc, { label, depth }) => { + const labelBBox = bboxCalculator.compute( + label, + 1, + 12, + '"Inter UI", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', + 1.5, + 400, + ); + labelBBox.width += depth * LEGEND_HIERARCHY_MARGIN; + if (acc.height < labelBBox.height) { + acc.height = labelBBox.height; + } + if (acc.width < labelBBox.width) { + acc.width = labelBBox.width; + } + return acc; + }, + { width: 0, height: 0 }, + ); + + bboxCalculator.destroy(); + const { showLegendExtra: showLegendDisplayValue, legendPosition, legendAction } = legendConfig; + const { + legend: { verticalWidth, spacingBuffer, margin }, + } = theme; + + const actionDimension = isDefined(legendAction) ? 24 : 0; // max width plus margin + const legendItemWidth = MARKER_WIDTH + SHARED_MARGIN + bbox.width + (showLegendDisplayValue ? SHARED_MARGIN : 0); + + if (legendPosition.direction === LayoutDirection.Vertical) { + const legendItemHeight = bbox.height + VERTICAL_PADDING * 2; + const legendHeight = legendItemHeight * labels.length + TOP_MARGIN; + const scrollBarDimension = legendHeight > parentDimensions.height ? SCROLL_BAR_WIDTH : 0; + + return { + width: Math.floor( + Math.min(legendItemWidth + spacingBuffer + actionDimension + scrollBarDimension, verticalWidth), + ), + height: legendHeight, + margin, + position: legendPosition, + }; + } + const isSingleLine = (parentDimensions.width - 20) / 200 > labels.length; + return { + height: isSingleLine ? bbox.height + 16 : bbox.height * 2 + 24, + width: Math.floor(Math.min(legendItemWidth + spacingBuffer + actionDimension, verticalWidth)), + margin, + position: legendPosition, + }; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/state/selectors/get_settings_specs.test.ts b/packages/osd-charts/src/state/selectors/get_settings_specs.test.ts new file mode 100644 index 000000000000..fdf16b9a38d4 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_settings_specs.test.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DEFAULT_SETTINGS_SPEC } from '../../specs/constants'; +import { getInitialState } from '../chart_state'; +import { getSettingsSpecSelector } from './get_settings_specs'; + +describe('selectors - getSettingsSpecSelector', () => { + const state = getInitialState('chartId1'); + it('shall return the same reference', () => { + const settings = getSettingsSpecSelector(state); + expect(settings).toBe(DEFAULT_SETTINGS_SPEC); + }); + it('shall avoid recomputations', () => { + getSettingsSpecSelector(state); + expect(getSettingsSpecSelector.recomputations()).toBe(1); + getSettingsSpecSelector(state); + expect(getSettingsSpecSelector.recomputations()).toBe(1); + getSettingsSpecSelector({ ...state, specsInitialized: true }); + expect(getSettingsSpecSelector.recomputations()).toBe(1); + getSettingsSpecSelector({ ...state, parentDimensions: { width: 100, height: 100, top: 100, left: 100 } }); + expect(getSettingsSpecSelector.recomputations()).toBe(1); + }); + it('shall return new settings if settings changed', () => { + const updatedSettings = { + ...DEFAULT_SETTINGS_SPEC, + rotation: 90, + }; + const updatedState = { + ...state, + specs: { + [DEFAULT_SETTINGS_SPEC.id]: updatedSettings, + }, + }; + const settingsSpecToCheck = getSettingsSpecSelector(updatedState); + expect(settingsSpecToCheck).toBe(updatedSettings); + expect(getSettingsSpecSelector.recomputations()).toBe(2); + getSettingsSpecSelector(updatedState); + expect(getSettingsSpecSelector.recomputations()).toBe(2); + }); +}); diff --git a/packages/osd-charts/src/state/selectors/get_settings_specs.ts b/packages/osd-charts/src/state/selectors/get_settings_specs.ts new file mode 100644 index 000000000000..3165ae9d7016 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_settings_specs.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { ChartType } from '../../chart_types'; +import { SpecType, DEFAULT_SETTINGS_SPEC } from '../../specs/constants'; +import { SettingsSpec } from '../../specs/settings'; +import { GlobalChartState } from '../chart_state'; +import { getSpecsFromStore } from '../utils'; +import { getChartIdSelector } from './get_chart_id'; + +/** @internal */ +export const getSpecs = (state: GlobalChartState) => state.specs; + +/** @internal */ +export const getSettingsSpecSelector = createCachedSelector( + [getSpecs], + (specs): SettingsSpec => { + const settingsSpecs = getSpecsFromStore(specs, ChartType.Global, SpecType.Settings); + if (settingsSpecs.length === 1) { + return settingsSpecs[0]; + } + return DEFAULT_SETTINGS_SPEC; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/state/selectors/get_small_multiples_spec.ts b/packages/osd-charts/src/state/selectors/get_small_multiples_spec.ts new file mode 100644 index 000000000000..73e8bc7b1c24 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_small_multiples_spec.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { ChartType } from '../../chart_types'; +import { SpecType } from '../../specs/constants'; +import { SmallMultiplesSpec } from '../../specs/small_multiples'; +import { getSpecsFromStore } from '../utils'; +import { getChartIdSelector } from './get_chart_id'; +import { getSpecs } from './get_settings_specs'; + +/** + * Return the small multiple specs + * @internal + */ +export const getSmallMultiplesSpecs = createCachedSelector([getSpecs], (specs) => + getSpecsFromStore(specs, ChartType.Global, SpecType.SmallMultiples), +)(getChartIdSelector); + +/** + * Return the small multiple spec + * @internal + */ +export const getSmallMultiplesSpec = createCachedSelector([getSmallMultiplesSpecs], (smallMultiples) => + smallMultiples.length === 1 ? smallMultiples : undefined, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/state/selectors/get_tooltip_header_formatter.ts b/packages/osd-charts/src/state/selectors/get_tooltip_header_formatter.ts new file mode 100644 index 000000000000..4d657ef64e5d --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_tooltip_header_formatter.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { SettingsSpec, TooltipValueFormatter, isTooltipProps } from '../../specs/settings'; +import { getChartIdSelector } from './get_chart_id'; +import { getSettingsSpecSelector } from './get_settings_specs'; + +/** @internal */ +export const getTooltipHeaderFormatterSelector = createCachedSelector( + [getSettingsSpecSelector], + getTooltipHeaderFormatter, +)(getChartIdSelector); + +function getTooltipHeaderFormatter(settings: SettingsSpec): TooltipValueFormatter | undefined { + const { tooltip } = settings; + if (tooltip && isTooltipProps(tooltip)) { + return tooltip.headerFormatter; + } +} diff --git a/packages/osd-charts/src/state/selectors/has_external_pointer_event.ts b/packages/osd-charts/src/state/selectors/has_external_pointer_event.ts new file mode 100644 index 000000000000..5fb6e4b7018f --- /dev/null +++ b/packages/osd-charts/src/state/selectors/has_external_pointer_event.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PointerEventType } from '../../specs'; +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export const hasExternalEventSelector = ({ externalEvents: { pointer } }: GlobalChartState) => + pointer !== null && pointer.type !== PointerEventType.Out; diff --git a/packages/osd-charts/src/state/selectors/is_chart_empty.ts b/packages/osd-charts/src/state/selectors/is_chart_empty.ts new file mode 100644 index 000000000000..30a40ea6e059 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/is_chart_empty.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export const isInternalChartEmptySelector = (state: GlobalChartState): boolean | undefined => { + if (state.internalChartState) { + return state.internalChartState.isChartEmpty(state); + } +}; diff --git a/packages/osd-charts/src/state/selectors/is_external_tooltip_visible.ts b/packages/osd-charts/src/state/selectors/is_external_tooltip_visible.ts new file mode 100644 index 000000000000..42d8f9e0a2e3 --- /dev/null +++ b/packages/osd-charts/src/state/selectors/is_external_tooltip_visible.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { computeChartDimensionsSelector } from '../../chart_types/xy_chart/state/selectors/compute_chart_dimensions'; +import { getComputedScalesSelector } from '../../chart_types/xy_chart/state/selectors/get_computed_scales'; +import { PointerEventType } from '../../specs'; +import { GlobalChartState } from '../chart_state'; +import { getChartIdSelector } from './get_chart_id'; +import { getSettingsSpecSelector } from './get_settings_specs'; +import { hasExternalEventSelector } from './has_external_pointer_event'; + +const getExternalEventPointer = ({ externalEvents: { pointer } }: GlobalChartState) => pointer; + +/** @internal */ +export const isExternalTooltipVisibleSelector = createCachedSelector( + [ + getSettingsSpecSelector, + hasExternalEventSelector, + getExternalEventPointer, + getComputedScalesSelector, + computeChartDimensionsSelector, + ], + ({ externalPointerEvents }, hasExternalEvent, pointer, { xScale }, { chartDimensions }): boolean => { + if (!pointer || pointer.type !== PointerEventType.Over || externalPointerEvents.tooltip?.visible === false) { + return false; + } + const x = xScale.pureScale(pointer.value); + + if (x == null || x > chartDimensions.width + chartDimensions.left || x < 0) { + return false; + } + return hasExternalEvent && externalPointerEvents.tooltip?.visible === true; + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/state/spec_factory.test.tsx b/packages/osd-charts/src/state/spec_factory.test.tsx new file mode 100644 index 000000000000..6e2e345baab4 --- /dev/null +++ b/packages/osd-charts/src/state/spec_factory.test.tsx @@ -0,0 +1,94 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; + +import { BarSeries } from '../chart_types/xy_chart/specs/bar_series'; +import { Chart } from '../components/chart'; +import { MockSeriesSpec } from '../mocks/specs/specs'; +import { Settings } from '../specs/settings'; +import { DebugState } from './types'; + +function getDebugState(wrapper: ReactWrapper): DebugState { + const statusComponent = wrapper.find('.echChartStatus'); + const debugState = statusComponent.getDOMNode().getAttribute('data-ech-debug-state'); + const parsedDebugState = JSON.parse(debugState || ''); + return parsedDebugState as DebugState; +} + +describe('Spec factory', () => { + const spec1 = MockSeriesSpec.bar({ id: 'spec1', data: [{ x: 0, y: 1 }] }); + const spec2 = MockSeriesSpec.bar({ id: 'spec2', data: [{ x: 0, y: 2 }] }); + + it('We can switch specs props between react component', () => { + const wrapper = mount( + + + ; + ; + , + ); + let debugState = getDebugState(wrapper); + expect(debugState.bars).toHaveLength(2); + wrapper.setProps({ + children: ( + <> + + ; + ; + + ), + }); + debugState = getDebugState(wrapper); + expect(debugState.bars).toHaveLength(2); + }); + + it('We can switch specs ids between react component', () => { + const wrapper = mount( + + + ; + ; + , + ); + let debugState = getDebugState(wrapper); + expect(debugState.bars).toHaveLength(2); + wrapper.setProps({ + children: ( + <> + + ; + ; + + ), + }); + debugState = getDebugState(wrapper); + + expect(debugState.bars).toHaveLength(2); + + // different id same y + expect(debugState.bars?.[0].name).toBe('spec2'); + expect(debugState.bars?.[0].bars[0].y).toBe(1); + + // different id same y + expect(debugState.bars?.[1].name).toBe('spec1'); + expect(debugState.bars?.[1].bars[0].y).toBe(2); + }); +}); diff --git a/packages/osd-charts/src/state/spec_factory.ts b/packages/osd-charts/src/state/spec_factory.ts new file mode 100644 index 000000000000..0c83682bd715 --- /dev/null +++ b/packages/osd-charts/src/state/spec_factory.ts @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Dispatch, bindActionCreators } from 'redux'; + +import { Spec } from '../specs'; +import { upsertSpec, removeSpec } from './actions/specs'; + +/** @internal */ +export interface DispatchProps { + upsertSpec: (spec: Spec) => void; + removeSpec: (id: string) => void; +} + +/** @internal */ +export function specComponentFactory( + defaultProps: Pick, +) { + /* eslint-disable no-shadow, react-hooks/exhaustive-deps, unicorn/consistent-function-scoping */ + const SpecInstance = (props: U & DispatchProps) => { + const { removeSpec, upsertSpec, ...SpecInstance } = props; + useEffect(() => { + upsertSpec(SpecInstance); + }); + useEffect( + () => () => { + removeSpec(props.id); + }, + [], + ); + return null; + }; + /* eslint-enable */ + SpecInstance.defaultProps = defaultProps; + return SpecInstance; +} + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => + bindActionCreators( + { + upsertSpec, + removeSpec, + }, + dispatch, + ); + +/** @internal */ +export function getConnect() { + /** + * Redux assumes shallowEqual for all connected components + * + * This causes an issue where the specs are cleared and memoized spec components will never be + * re-rendered and thus never re-upserted to the state. Setting pure to false solves this issue + * and doesn't cause traditional performance degradations. + */ + return connect(null, mapDispatchToProps, null, { pure: false }); +} diff --git a/packages/osd-charts/src/state/types.ts b/packages/osd-charts/src/state/types.ts new file mode 100644 index 000000000000..acc01f92a2f7 --- /dev/null +++ b/packages/osd-charts/src/state/types.ts @@ -0,0 +1,131 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { Cell } from '../chart_types/heatmap/layout/types/viewmodel_types'; +import { Pixels } from '../common/geometry'; +import type { Position } from '../utils/common'; +import type { GeometryValue } from '../utils/geometry'; + +/** @public */ +export interface DebugStateAxis { + id: string; + position: Position; + title?: string; + labels: string[]; + values: any[]; + gridlines: { + y: number; + x: number; + }[]; +} + +/** @public */ +export interface DebugStateAxes { + x: DebugStateAxis[]; + y: DebugStateAxis[]; +} + +/** @public */ +export interface DebugStateLegendItem { + key: string; + name: string; + color: string; +} + +/** @public */ +export interface DebugStateLegend { + items: DebugStateLegendItem[]; +} + +interface DebugStateBase { + key: string; + name: string; + color: string; +} + +/** @public */ +export type DebugStateValue = Pick; + +interface DebugStateLineConfig { + visible: boolean; + path: string; + points: DebugStateValue[]; + visiblePoints: boolean; +} + +/** @public */ +export interface DebugStateLine extends DebugStateBase, DebugStateLineConfig {} + +/** @public */ +export type DebugStateArea = Omit & { + path: string; + lines: { + y0?: DebugStateLineConfig; + y1: DebugStateLineConfig; + }; +}; + +/** @public */ +export type DebugStateBar = DebugStateBase & { + visible: boolean; + bars: DebugStateValue[]; + labels: any[]; +}; + +type CellDebug = Pick & { fill: string }; + +type HeatmapDebugState = { + cells: CellDebug[]; + selection: { + area: { x: number; y: number; width: number; height: number } | null; + data: { x: Array; y: Array } | null; + }; +}; + +/** @public */ +export type SinglePartitionDebugState = { + name: string; + depth: number; + color: string; + value: number; + coords: [Pixels, Pixels]; +}; + +/** @public */ +export type PartitionDebugState = { + panelTitle: string; + partitions: Array; +}; + +/** + * Describes _visible_ chart state for use in functional tests + * + * TODO: add other chart types to debug state + * @public + */ +export interface DebugState { + legend?: DebugStateLegend; + axes?: DebugStateAxes; + areas?: DebugStateArea[]; + lines?: DebugStateLine[]; + bars?: DebugStateBar[]; + /** Heatmap chart debug state */ + heatmap?: HeatmapDebugState; + partition?: PartitionDebugState[]; +} diff --git a/packages/osd-charts/src/state/utils.test.ts b/packages/osd-charts/src/state/utils.test.ts new file mode 100644 index 000000000000..192f1a951373 --- /dev/null +++ b/packages/osd-charts/src/state/utils.test.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartType } from '../chart_types'; +import { SpecType } from '../specs/constants'; +import { getSpecsFromStore } from './utils'; + +describe('State utils', () => { + it('getSpecsFromStore shall return always the same object reference excluding the array', () => { + const spec1 = { id: 'id1', chartType: ChartType.XYAxis, specType: SpecType.Series }; + const specs = getSpecsFromStore({ id1: spec1 }, ChartType.XYAxis, SpecType.Series); + expect(specs[0]).toBe(spec1); + }); +}); diff --git a/packages/osd-charts/src/state/utils.ts b/packages/osd-charts/src/state/utils.ts new file mode 100644 index 000000000000..fac6cbc5995e --- /dev/null +++ b/packages/osd-charts/src/state/utils.ts @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartType } from '../chart_types'; +import { Spec } from '../specs'; +import { SpecList, PointerState } from './chart_state'; + +/** @internal */ +export function getSpecsFromStore(specs: SpecList, chartType: ChartType, specType?: string): U[] { + return Object.keys(specs) + .filter((specId) => { + const currentSpec = specs[specId]; + const sameChartType = currentSpec.chartType === chartType; + const sameSpecType = specType ? currentSpec.specType === specType : true; + return sameChartType && sameSpecType; + }) + .map((specId) => specs[specId] as U); +} + +/** @internal */ +export function isClicking(prevClick: PointerState | null, lastClick: PointerState | null) { + if (prevClick === null && lastClick !== null) { + return true; + } + return prevClick !== null && lastClick !== null && prevClick.time !== lastClick.time; +} + +/** @internal */ +export const getInitialPointerState = () => ({ + dragging: false, + current: { + position: { + x: -1, + y: -1, + }, + time: 0, + }, + down: null, + up: null, + lastDrag: null, + lastClick: null, +}); diff --git a/packages/osd-charts/src/theme_dark.scss b/packages/osd-charts/src/theme_dark.scss new file mode 100644 index 000000000000..6177655f5a9d --- /dev/null +++ b/packages/osd-charts/src/theme_dark.scss @@ -0,0 +1,7 @@ +@import '../../../node_modules/@elastic/eui/src/themes/eui/eui_colors_dark'; + +@import 'eui_imports'; +@import '../../../node_modules/@elastic/eui/src/global_styling/reset/index'; + +// Components +@import 'components/index'; diff --git a/packages/osd-charts/src/theme_light.scss b/packages/osd-charts/src/theme_light.scss new file mode 100644 index 000000000000..541c9167eebf --- /dev/null +++ b/packages/osd-charts/src/theme_light.scss @@ -0,0 +1,7 @@ +@import '../../../node_modules/@elastic/eui/src/themes/eui/eui_colors_light'; + +@import 'eui_imports'; +@import '../../../node_modules/@elastic/eui/src/global_styling/reset/index'; + +// Components +@import 'components/index'; diff --git a/packages/osd-charts/src/theme_only_dark.scss b/packages/osd-charts/src/theme_only_dark.scss new file mode 100644 index 000000000000..04cff458c134 --- /dev/null +++ b/packages/osd-charts/src/theme_only_dark.scss @@ -0,0 +1,6 @@ +@import '../../../node_modules/@elastic/eui/src/themes/eui/eui_colors_dark'; + +@import 'eui_imports'; + +// Components +@import 'components/index'; diff --git a/packages/osd-charts/src/theme_only_light.scss b/packages/osd-charts/src/theme_only_light.scss new file mode 100644 index 000000000000..ba976692e09e --- /dev/null +++ b/packages/osd-charts/src/theme_only_light.scss @@ -0,0 +1,6 @@ +@import '../../../node_modules/@elastic/eui/src/themes/eui/eui_colors_light'; + +@import 'eui_imports'; + +// Components +@import 'components/index'; diff --git a/packages/osd-charts/src/utils/__mocks__/common.ts b/packages/osd-charts/src/utils/__mocks__/common.ts new file mode 100644 index 000000000000..7d023ef6fec4 --- /dev/null +++ b/packages/osd-charts/src/utils/__mocks__/common.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const module = jest.requireActual('../common.ts'); + +/** @internal */ +export const { ColorVariant, Position } = module; + +/** @internal */ +export const identity = jest.fn(module.identity); +/** @internal */ +export const compareByValueAsc = jest.fn(module.compareByValueAsc); +/** @internal */ +export const clamp = jest.fn(module.clamp); +/** @internal */ +export const getColorFromVariant = jest.fn(module.getColorFromVariant); +/** @internal */ +export const htmlIdGenerator = jest.fn(module.htmlIdGenerator); +/** @internal */ +export const getPartialValue = jest.fn(module.getPartialValue); +/** @internal */ +export const getAllKeys = jest.fn(module.getAllKeys); +/** @internal */ +export const hasPartialObjectToMerge = jest.fn(module.hasPartialObjectToMerge); +/** @internal */ +export const shallowClone = jest.fn(module.shallowClone); +/** @internal */ +export const mergePartial = jest.fn(module.mergePartial); +/** @internal */ +export const isNumberArray = jest.fn(module.isNumberArray); +/** @internal */ +export const getUniqueValues = jest.fn(module.getUniqueValues); diff --git a/packages/osd-charts/src/utils/accessor.ts b/packages/osd-charts/src/utils/accessor.ts new file mode 100644 index 000000000000..906bb334df04 --- /dev/null +++ b/packages/osd-charts/src/utils/accessor.ts @@ -0,0 +1,117 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Datum } from './common'; + +/** + * Accessor function + * @param datum - the datum + * @public + */ +export interface UnaryAccessorFn { + /** + * Name used as accessor field name in place of function reference + */ + fieldName?: string; + (datum: Datum): Return; +} + +/** + * Accessor function + * @param datum - the datum + * @param index - the index in the array + * @public + */ +export type BinaryAccessorFn = (datum: Datum, index: number) => Return; + +/** + * An accessor function + * @public + */ +export type AccessorFn = UnaryAccessorFn; + +/** + * An indexed accessor function + * @public + */ +export type IndexedAccessorFn = UnaryAccessorFn | BinaryAccessorFn; + +/** + * A key accessor string + * @public + */ +export type AccessorObjectKey = string; + +/** + * An index accessor number + * @public + */ +export type AccessorArrayIndex = number; + +/** + * A datum accessor in form of object key accessor string/number + * @public + */ +export type Accessor = AccessorObjectKey | AccessorArrayIndex; + +/** + * Accessor format for _banded_ series as postfix string or accessor function + * @public + */ +export type AccessorFormat = string | ((value: string) => string); + +/** + * Return an accessor function using the accessor passed as argument + * @param accessor the spec accessor + * @internal + */ +export function getAccessorFn(accessor: Accessor): AccessorFn { + return (datum: Datum) => + typeof datum === 'object' && datum !== null ? datum[accessor as keyof typeof datum] : undefined; +} + +/** + * Return the accessor label given as `AccessorFormat` + * @internal + */ +export function getAccessorFormatLabel(accessor: AccessorFormat, label: string): string { + if (typeof accessor === 'string') { + return `${label}${accessor}`; + } + + return accessor(label); +} + +/** + * Helper function to get accessor value from string, number or function + * @internal + */ +export function getAccessorValue(datum: Datum, accessor: Accessor | AccessorFn) { + if (typeof accessor === 'function') { + return accessor(datum); + } + + return datum[accessor]; +} + +/** + * Additive numbers: numbers whose semantics are conducive to addition; eg. counts and sums are additive, but averages aren't + * @public + */ +export type AdditiveNumber = number; diff --git a/packages/osd-charts/src/utils/bbox/bbox_calculator.ts b/packages/osd-charts/src/utils/bbox/bbox_calculator.ts new file mode 100644 index 000000000000..f02f30e0baaa --- /dev/null +++ b/packages/osd-charts/src/utils/bbox/bbox_calculator.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export interface BBox { + width: number; + height: number; +} + +/** @internal */ +export const DEFAULT_EMPTY_BBOX = { + width: 0, + height: 0, +}; + +/** @internal */ +export interface BBoxCalculator { + compute(text: string, padding: number, fontSize?: number, fontFamily?: string): BBox; + destroy(): void; +} diff --git a/packages/osd-charts/src/utils/bbox/canvas_text_bbox_calculator.test.ts b/packages/osd-charts/src/utils/bbox/canvas_text_bbox_calculator.test.ts new file mode 100644 index 000000000000..ead76e1ccffc --- /dev/null +++ b/packages/osd-charts/src/utils/bbox/canvas_text_bbox_calculator.test.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CanvasTextBBoxCalculator } from './canvas_text_bbox_calculator'; + +describe('CanvasTextBBoxCalculator', () => { + test('can create a canvas for computing text measurement values', () => { + const canvasBboxCalculator = new CanvasTextBBoxCalculator(); + const bbox = canvasBboxCalculator.compute('foo', 0); + expect(Math.abs(bbox.width - 23.2)).toBeLessThanOrEqual(2); + expect(bbox.height).toBe(16); + }); +}); diff --git a/packages/osd-charts/src/utils/bbox/canvas_text_bbox_calculator.ts b/packages/osd-charts/src/utils/bbox/canvas_text_bbox_calculator.ts new file mode 100644 index 000000000000..0da7716f757d --- /dev/null +++ b/packages/osd-charts/src/utils/bbox/canvas_text_bbox_calculator.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BBox, BBoxCalculator, DEFAULT_EMPTY_BBOX } from './bbox_calculator'; + +/** @internal */ +export class CanvasTextBBoxCalculator implements BBoxCalculator { + private attachedRoot: HTMLElement; + + private offscreenCanvas: HTMLCanvasElement; + + private context: CanvasRenderingContext2D | null; + + constructor(rootElement?: HTMLElement) { + this.offscreenCanvas = document.createElement('canvas'); + this.offscreenCanvas.style.position = 'absolute'; + this.offscreenCanvas.style.top = '-99999px'; + this.offscreenCanvas.style.left = '-99999px'; + this.context = this.offscreenCanvas.getContext('2d'); + this.attachedRoot = rootElement || document.documentElement; + this.attachedRoot.appendChild(this.offscreenCanvas); + } + + compute(text: string, padding: number, fontSize = 16, fontFamily = 'Arial', lineHeight = 1, fontWeight = 400): BBox { + if (!this.context) { + return DEFAULT_EMPTY_BBOX; + } + // Padding should be at least one to avoid browser measureText inconsistencies + if (padding < 1) { + padding = 1; + } + this.context.font = `${fontWeight} ${fontSize}px ${fontFamily}`; + const measure = this.context.measureText(text); + + return { + width: measure.width + padding, + height: fontSize * lineHeight, + }; + } + + destroy(): void { + this.attachedRoot.removeChild(this.offscreenCanvas); + } +} diff --git a/packages/osd-charts/src/utils/bbox/dom_text_bbox_calculator.ts b/packages/osd-charts/src/utils/bbox/dom_text_bbox_calculator.ts new file mode 100644 index 000000000000..35f09ca5e30e --- /dev/null +++ b/packages/osd-charts/src/utils/bbox/dom_text_bbox_calculator.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BBox, BBoxCalculator } from './bbox_calculator'; + +/** @internal */ +export class DOMTextBBoxCalculator implements BBoxCalculator { + private attachedRoot: HTMLElement; + + private offscreenCanvas: HTMLSpanElement; + + constructor(rootElement?: HTMLElement) { + this.offscreenCanvas = document.createElement('span'); + this.offscreenCanvas.style.position = 'absolute'; + this.offscreenCanvas.style.top = '-9999px'; + this.offscreenCanvas.style.left = '-9999px'; + + this.attachedRoot = rootElement || document.documentElement; + this.attachedRoot.appendChild(this.offscreenCanvas); + } + + compute(text: string, padding: number, fontSize = 16, fontFamily = 'Arial', lineHeight = 1, fontWeight = 400): BBox { + this.offscreenCanvas.style.fontSize = `${fontSize}px`; + this.offscreenCanvas.style.fontFamily = fontFamily; + this.offscreenCanvas.style.fontWeight = `${fontWeight}`; + this.offscreenCanvas.style.lineHeight = `${lineHeight}px`; + this.offscreenCanvas.innerHTML = text; + + return { + width: Math.ceil(this.offscreenCanvas.clientWidth + padding), + height: Math.ceil(this.offscreenCanvas.clientHeight), + }; + } + + destroy(): void { + this.attachedRoot.removeChild(this.offscreenCanvas); + } +} diff --git a/packages/osd-charts/src/utils/bbox/svg_text_bbox_calculator.ts b/packages/osd-charts/src/utils/bbox/svg_text_bbox_calculator.ts new file mode 100644 index 000000000000..5fa5dd0c3cae --- /dev/null +++ b/packages/osd-charts/src/utils/bbox/svg_text_bbox_calculator.ts @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BBox, BBoxCalculator } from './bbox_calculator'; + +/** @internal */ +export class SvgTextBBoxCalculator implements BBoxCalculator { + svgElem: SVGSVGElement; + + textElem: SVGTextElement; + + attachedRoot: HTMLElement; + + textNode: Text; + + // TODO specify styles for text + // TODO specify how to hide the svg from the current dom view + // like moving it a -9999999px + constructor(rootElement?: HTMLElement) { + const xmlns = 'http://www.w3.org/2000/svg'; + this.svgElem = document.createElementNS(xmlns, 'svg'); + this.textElem = document.createElementNS(xmlns, 'text'); + this.svgElem.appendChild(this.textElem); + this.textNode = document.createTextNode(''); + this.textElem.appendChild(this.textNode); + this.attachedRoot = rootElement || document.documentElement; + this.attachedRoot.appendChild(this.svgElem); + } + + compute(text: string): BBox { + this.textNode.textContent = text; + const rect = this.textElem.getBoundingClientRect(); + return { + width: rect.width, + height: rect.height, + }; + } + + destroy(): void { + this.attachedRoot.removeChild(this.svgElem); + } +} diff --git a/packages/osd-charts/src/utils/chart_size.test.ts b/packages/osd-charts/src/utils/chart_size.test.ts new file mode 100644 index 000000000000..12ddf19cee7c --- /dev/null +++ b/packages/osd-charts/src/utils/chart_size.test.ts @@ -0,0 +1,81 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getChartSize } from './chart_size'; + +describe('chart size utilities', () => { + test('array', () => { + expect(getChartSize([100, 100])).toEqual({ + width: 100, + height: 100, + }); + expect(getChartSize([undefined, 100])).toEqual({ + width: '100%', + height: 100, + }); + expect(getChartSize([100, undefined])).toEqual({ + width: 100, + height: '100%', + }); + expect(getChartSize([undefined, undefined])).toEqual({ + width: '100%', + height: '100%', + }); + expect(getChartSize([0, '100em'])).toEqual({ + width: 0, + height: '100em', + }); + }); + test('value', () => { + expect(getChartSize(1)).toEqual({ + width: 1, + height: 1, + }); + expect(getChartSize('100em')).toEqual({ + width: '100em', + height: '100em', + }); + expect(getChartSize(0)).toEqual({ + width: 0, + height: 0, + }); + }); + test('object', () => { + expect(getChartSize({ width: 100, height: 100 })).toEqual({ + width: 100, + height: 100, + }); + expect(getChartSize({ height: 100 })).toEqual({ + width: '100%', + height: 100, + }); + expect(getChartSize({ width: 100 })).toEqual({ + width: 100, + height: '100%', + }); + expect(getChartSize({})).toEqual({ + width: '100%', + height: '100%', + }); + expect(getChartSize({ width: 0, height: '100em' })).toEqual({ + width: 0, + height: '100em', + }); + }); +}); diff --git a/packages/osd-charts/src/utils/chart_size.ts b/packages/osd-charts/src/utils/chart_size.ts new file mode 100644 index 000000000000..367669fcaa11 --- /dev/null +++ b/packages/osd-charts/src/utils/chart_size.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @public */ +export type ChartSizeArray = [number | string | undefined, number | string | undefined]; +/** @public */ +export interface ChartSizeObject { + width?: number | string; + height?: number | string; +} + +/** @public */ +export type ChartSize = number | string | ChartSizeArray | ChartSizeObject; + +/** @internal */ +export function getChartSize(size?: ChartSize): ChartSizeObject { + if (size === undefined) { + return {}; + } + if (Array.isArray(size)) { + return { + width: size[0] === undefined ? '100%' : size[0], + height: size[1] === undefined ? '100%' : size[1], + }; + } + if (typeof size === 'object') { + return { + width: size.width === undefined ? '100%' : size.width, + height: size.height === undefined ? '100%' : size.height, + }; + } + const sameSize = size === undefined ? '100%' : size; + return { + width: sameSize, + height: sameSize, + }; +} diff --git a/packages/osd-charts/src/utils/common.test.ts b/packages/osd-charts/src/utils/common.test.ts new file mode 100644 index 000000000000..d64db6c19063 --- /dev/null +++ b/packages/osd-charts/src/utils/common.test.ts @@ -0,0 +1,1158 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + maxValueWithUpperLimit, + compareByValueAsc, + identity, + hasPartialObjectToMerge, + mergePartial, + RecursivePartial, + getPartialValue, + getAllKeys, + shallowClone, + minValueWithLowerLimit, + getColorFromVariant, + ColorVariant, + isUniqueArray, + isDefined, + isDefinedFrom, +} from './common'; + +describe('common utilities', () => { + test('return value bounded above', () => { + expect(maxValueWithUpperLimit(0, 0, 1)).toBe(0); + expect(maxValueWithUpperLimit(1, 0, 1)).toBe(1); + + expect(maxValueWithUpperLimit(1.1, 0, 1)).toBe(1); + expect(maxValueWithUpperLimit(-0.1, 0, 1)).toBe(0); + + expect(maxValueWithUpperLimit(0.1, 0, 1)).toBe(0.1); + expect(maxValueWithUpperLimit(0.8, 0, 1)).toBe(0.8); + }); + test('return value bounded below', () => { + expect(minValueWithLowerLimit(10, 20, 0)).toBe(10); + expect(minValueWithLowerLimit(20, 10, 0)).toBe(10); + + expect(minValueWithLowerLimit(10, -20, 0)).toBe(0); + expect(minValueWithLowerLimit(-20, 10, 0)).toBe(0); + }); + + test('identity', () => { + expect(identity('text')).toBe('text'); + expect(identity(2)).toBe(2); + const a = {}; + expect(identity(a)).toBe(a); + expect(identity(null)).toBe(null); + expect(identity(undefined)).toBeUndefined(); + const fn = () => ({}); + expect(identity(fn)).toBe(fn); + }); + + test('compareByValueAsc', () => { + expect([2, 1, 4, 3].sort(compareByValueAsc)).toEqual([1, 2, 3, 4]); + expect(['b', 'a', 'd', 'c'].sort(compareByValueAsc)).toEqual(['a', 'b', 'c', 'd']); + }); + + describe('getPartialValue', () => { + interface TestType { + foo: string; + bar: number; + test?: TestType; + } + const base: TestType = { + foo: 'elastic', + bar: 123, + test: { + foo: 'shay', + bar: 321, + }, + }; + const partial: RecursivePartial = { + foo: 'elastic', + }; + + it('should return partial if it is defined', () => { + const result = getPartialValue(base, partial); + + expect(result).toBe(partial); + }); + + it('should return base if partial is undefined', () => { + const result = getPartialValue(base); + + expect(result).toBe(base); + }); + + it('should return extra partials if partial is undefined', () => { + const result = getPartialValue(base, undefined, [partial]); + + expect(result).toBe(partial); + }); + + it('should return second partial if partial is undefined', () => { + // @ts-ignore + const result = getPartialValue(base, undefined, [undefined, partial]); + + expect(result).toBe(partial); + }); + + it('should return base if no partials are defined', () => { + // @ts-ignore + const result = getPartialValue(base, undefined, [undefined, undefined]); + + expect(result).toBe(base); + }); + }); + + describe('getAllKeys', () => { + const object1 = { + key1: 1, + key2: 2, + }; + const object2 = { + key3: 3, + key4: 4, + }; + const object3 = { + key5: 5, + key6: 6, + }; + + it('should return all keys from single object', () => { + const result = getAllKeys(object1); + + expect(result).toEqual(['key1', 'key2']); + }); + + it('should return all keys from all objects x 2', () => { + const result = getAllKeys(object1, [object2]); + + expect(result).toEqual(['key1', 'key2', 'key3', 'key4']); + }); + + it('should return all keys from single objects x 3', () => { + const result = getAllKeys(object1, [object2, object3]); + + expect(result).toEqual(['key1', 'key2', 'key3', 'key4', 'key5', 'key6']); + }); + + it('should return all keys from only defined objects', () => { + const result = getAllKeys(object1, [null, object2, {}, undefined]); + + expect(result).toEqual(['key1', 'key2', 'key3', 'key4']); + }); + }); + + describe('shallowClone', () => { + const obj = { value: 'test' }; + const arr = ['test']; + + it('should clone object', () => { + const result = shallowClone(obj); + + expect(result).toEqual(obj); + expect(result).not.toBe(obj); + }); + + it('should clone array', () => { + const result = shallowClone(arr); + + expect(result).toEqual(arr); + expect(result).not.toBe(arr); + }); + + it('should return simple values', () => { + expect(shallowClone(false)).toBe(false); + expect(shallowClone(true)).toBe(true); + expect(shallowClone('string')).toBe('string'); + expect(shallowClone(10)).toBe(10); + expect(shallowClone(undefined)).toBeUndefined(); + }); + + it('should return null', () => { + expect(shallowClone(null)).toBeNull(); + }); + }); + + describe('hasPartialObjectToMerge', () => { + it('should return false if base is null', () => { + const result = hasPartialObjectToMerge(null); + expect(result).toBe(false); + }); + + it('should return false if base is an array', () => { + const result = hasPartialObjectToMerge([]); + expect(result).toBe(false); + }); + + it('should return false if base is a Set', () => { + const result = hasPartialObjectToMerge(new Set()); + expect(result).toBe(false); + }); + + it('should return true if base and partial are objects', () => { + const result = hasPartialObjectToMerge({}, {}); + expect(result).toBe(true); + }); + + it('should return false if base is object and patial is null', () => { + const result = hasPartialObjectToMerge({}, null as any); + expect(result).toBe(false); + }); + + it('should return true if base and any additionalPartials are objects', () => { + const result = hasPartialObjectToMerge({}, undefined, ['string', [], {}]); + expect(result).toBe(true); + }); + + it('should return true if base and any additionalPartials are objects even if partial is an array', () => { + const result = hasPartialObjectToMerge({}, [], ['string', [], {}]); + expect(result).toBe(true); + }); + + it('should return false if base is an object but not the partial nor any additionalPartials are not objects', () => { + const result = hasPartialObjectToMerge({}, undefined, ['string', []]); + expect(result).toBe(false); + }); + + it('should return false if base is an object but not the partial nor any additionalPartials are not objects even if partial is an array', () => { + const result = hasPartialObjectToMerge({}, [], ['string', []]); + expect(result).toBe(false); + }); + }); + + describe('mergePartial', () => { + let baseClone: TestType; + interface TestType { + string: string; + number: number; + boolean: boolean; + array1: Partial[]; + array2: number[]; + nested: Partial; + } + type PartialTestType = RecursivePartial; + const base: TestType = { + string: 'string1', + boolean: false, + number: 1, + array1: [ + { + string: 'string2', + }, + ], + array2: [1, 2, 3], + nested: { + string: 'string2', + number: 2, + }, + }; + + beforeAll(() => { + baseClone = JSON.parse(JSON.stringify(base)) as TestType; + }); + + describe('Union types', () => { + type TestObject = { string1?: string; string2?: string }; + interface TestUnionType { + union: 'val1' | 'val2' | TestObject | string[]; + } + + test('should override simple union type with object', () => { + const result = mergePartial( + { union: 'val2' }, + { union: { string1: 'other' } }, + { mergeOptionalPartialValues: true }, + ); + expect(result).toEqual({ + union: { string1: 'other' }, + }); + }); + + test('should override simple union type with array', () => { + const result = mergePartial( + { union: 'val2' }, + { union: ['string'] }, + { mergeOptionalPartialValues: true }, + ); + expect(result).toEqual({ + union: ['string'], + }); + }); + + test('should override simple union type with object from additionalPartials', () => { + const result = mergePartial({ union: 'val2' }, {}, { mergeOptionalPartialValues: true }, [ + {}, + { union: { string1: 'other' } }, + ]); + expect(result).toEqual({ + union: { string1: 'other' }, + }); + }); + + test('should override simple union type with array from additionalPartials', () => { + const result = mergePartial({ union: 'val2' }, {}, { mergeOptionalPartialValues: true }, [ + {}, + { union: ['string'] }, + ]); + expect(result).toEqual({ + union: ['string'], + }); + }); + + test('should override object union type with simple', () => { + const result = mergePartial( + { union: { string1: 'other' } }, + { union: 'val2' }, + { mergeOptionalPartialValues: true }, + ); + expect(result).toEqual({ union: 'val2' }); + }); + + test('should override object union type with array', () => { + const result = mergePartial( + { union: { string1: 'other' } }, + { union: ['string'] }, + { mergeOptionalPartialValues: true }, + ); + expect(result).toEqual({ union: ['string'] }); + }); + + test('should override object union type with simple from additionalPartials', () => { + const result = mergePartial( + { union: { string1: 'other' } }, + {}, + { mergeOptionalPartialValues: true }, + [{}, { union: 'val2' }], + ); + expect(result).toEqual({ union: 'val2' }); + }); + + test('should override object union type with array from additionalPartials', () => { + const result = mergePartial( + { union: { string1: 'other' } }, + {}, + { mergeOptionalPartialValues: true }, + [{}, { union: ['string'] }], + ); + expect(result).toEqual({ union: ['string'] }); + }); + + test('should override array union type with simple', () => { + const result = mergePartial( + { union: ['string'] }, + { union: 'val2' }, + { mergeOptionalPartialValues: true }, + ); + expect(result).toEqual({ union: 'val2' }); + }); + + test('should override array union type with object', () => { + const result = mergePartial( + { union: ['string'] }, + { union: { string1: 'other' } }, + { mergeOptionalPartialValues: true }, + ); + expect(result).toEqual({ union: { string1: 'other' } }); + }); + + test('should override array union type with simple from additionalPartials', () => { + const result = mergePartial({ union: ['string'] }, {}, { mergeOptionalPartialValues: true }, [ + {}, + { union: 'val2' }, + ]); + expect(result).toEqual({ union: 'val2' }); + }); + + test('should override array union type with object from additionalPartials', () => { + const result = mergePartial({ union: ['string'] }, {}, { mergeOptionalPartialValues: true }, [ + {}, + { union: { string1: 'other' } }, + ]); + expect(result).toEqual({ union: { string1: 'other' } }); + }); + }); + + test('should allow partial to be undefined', () => { + expect(mergePartial('test')).toBe('test'); + }); + + test('should override base value with partial', () => { + expect(mergePartial(1 as number, 2)).toBe(2); + }); + + test('should NOT return original base structure', () => { + expect(mergePartial(base)).not.toBe(base); + }); + + test('should override string value in base', () => { + const partial: PartialTestType = { string: 'test' }; + const newBase = mergePartial(base, partial); + expect(newBase).toEqual({ + ...newBase, + string: partial.string, + }); + }); + + test('should override boolean value in base', () => { + const partial: PartialTestType = { boolean: true }; + const newBase = mergePartial(base, partial); + expect(newBase).toEqual({ + ...newBase, + boolean: partial.boolean, + }); + }); + + test('should override number value in base', () => { + const partial: PartialTestType = { number: 3 }; + const newBase = mergePartial(base, partial); + expect(newBase).toEqual({ + ...newBase, + number: partial.number, + }); + }); + + test('should override complex array value in base', () => { + const partial: PartialTestType = { array1: [{ string: 'test' }] }; + const newBase = mergePartial(base, partial); + expect(newBase).toEqual({ + ...newBase, + array1: partial.array1, + }); + }); + + test('should override simple array value in base', () => { + const partial: PartialTestType = { array2: [4, 5, 6] }; + const newBase = mergePartial(base, partial); + expect(newBase).toEqual({ + ...newBase, + array2: partial.array2, + }); + }); + + test('should override nested values in base', () => { + const partial: PartialTestType = { nested: { number: 5 } }; + const newBase = mergePartial(base, partial); + expect(newBase).toEqual({ + ...newBase, + nested: { + ...newBase.nested, + number: partial.nested!.number, + }, + }); + }); + + test('should not mutate base structure', () => { + const partial: PartialTestType = { number: 3 }; + mergePartial(base, partial); + expect(base).toEqual(baseClone); + }); + + describe('Maps', () => { + it('should merge top-level Maps', () => { + const result = mergePartial( + new Map([ + [ + 'a', + { + name: 'Nick', + }, + ], + [ + 'b', + { + name: 'Marco', + }, + ], + ]), + new Map([ + [ + 'b', + { + name: 'rachel', + }, + ], + ]), + { + mergeMaps: true, + }, + ); + expect(result).toEqual( + new Map([ + [ + 'a', + { + name: 'Nick', + }, + ], + [ + 'b', + { + name: 'rachel', + }, + ], + ]), + ); + }); + + it('should merge nested Maps with partail', () => { + const result = mergePartial( + { + test: new Map([ + [ + 'cat', + { + name: 'cat', + }, + ], + ]), + }, + { + test: new Map([ + [ + 'dog', + { + name: 'dog', + }, + ], + ]), + }, + { + mergeMaps: true, + mergeOptionalPartialValues: true, + }, + ); + expect(result).toEqual({ + test: new Map([ + [ + 'cat', + { + name: 'cat', + }, + ], + [ + 'dog', + { + name: 'dog', + }, + ], + ]), + }); + }); + + it('should merge nested Maps', () => { + const result = mergePartial( + { + test: new Map([ + [ + 'cat', + { + name: 'toby', + }, + ], + ]), + }, + { + test: new Map([ + [ + 'cat', + { + name: 'snickers', + }, + ], + ]), + }, + { + mergeMaps: true, + }, + ); + expect(result).toEqual({ + test: new Map([ + [ + 'cat', + { + name: 'snickers', + }, + ], + ]), + }); + }); + + it('should merge nested Maps with mergeOptionalPartialValues', () => { + const result = mergePartial( + { + test: new Map([ + [ + 'cat', + { + name: 'toby', + }, + ], + ]), + }, + { + test: new Map([ + [ + 'dog', + { + name: 'lucky', + }, + ], + ]), + }, + { + mergeMaps: true, + mergeOptionalPartialValues: true, + }, + ); + expect(result).toEqual({ + test: new Map([ + [ + 'cat', + { + name: 'toby', + }, + ], + [ + 'dog', + { + name: 'lucky', + }, + ], + ]), + }); + }); + + it('should merge nested Maps from additionalPartials', () => { + const result = mergePartial( + { + test: new Map([ + [ + 'cat', + { + name: 'toby', + }, + ], + ]), + }, + undefined, + { + mergeMaps: true, + }, + [ + { + test: new Map([ + [ + 'cat', + { + name: 'snickers', + }, + ], + ]), + }, + ], + ); + expect(result).toEqual({ + test: new Map([ + [ + 'cat', + { + name: 'toby', + }, + ], + [ + 'cat', + { + name: 'snickers', + }, + ], + ]), + }); + }); + + it('should replace Maps when mergeMaps is false', () => { + const result = mergePartial( + { + test: new Map([ + [ + 'cat', + { + name: 'toby', + }, + ], + ]), + }, + { + test: new Map([ + [ + 'dog', + { + name: 'snickers', + }, + ], + ]), + }, + ); + expect(result).toEqual({ + test: new Map([ + [ + 'dog', + { + name: 'snickers', + }, + ], + ]), + }); + }); + + it('should replace Maps when mergeMaps is false from additionalPartials', () => { + const result = mergePartial( + { + test: new Map([ + [ + 'cat', + { + name: 'toby', + }, + ], + ]), + }, + undefined, + undefined, + [ + { + test: new Map([ + [ + 'dog', + { + name: 'snickers', + }, + ], + ]), + }, + ], + ); + expect(result).toEqual({ + test: new Map([ + [ + 'dog', + { + name: 'snickers', + }, + ], + ]), + }); + }); + }); + + describe('Sets', () => { + it('should merge Sets like arrays', () => { + const result = mergePartial( + { + animals: new Set(['cat', 'dog']), + }, + { + animals: new Set(['cat', 'dog', 'bird']), + }, + ); + expect(result).toEqual({ + animals: new Set(['cat', 'dog', 'bird']), + }); + }); + + it('should merge Sets like arrays with mergeOptionalPartialValues', () => { + interface Test { + animals: Set; + numbers?: Set; + } + const result = mergePartial( + { + animals: new Set(['cat', 'dog']), + }, + { + numbers: new Set([1, 2, 3]), + }, + { mergeOptionalPartialValues: true }, + ); + expect(result).toEqual({ + animals: new Set(['cat', 'dog']), + numbers: new Set([1, 2, 3]), + }); + }); + + it('should merge Sets like arrays from additionalPartials', () => { + const result = mergePartial( + { + animals: new Set(['cat', 'dog']), + }, + {}, + {}, + [ + { + animals: new Set(['cat', 'dog', 'bird']), + }, + ], + ); + expect(result).toEqual({ + animals: new Set(['cat', 'dog', 'bird']), + }); + }); + }); + + describe('additionalPartials', () => { + test('should override string value in base with first partial value', () => { + const partial: PartialTestType = { string: 'test1' }; + const partials: PartialTestType[] = [{ string: 'test2' }, { string: 'test3' }]; + const newBase = mergePartial(base, partial, {}, partials); + expect(newBase).toEqual({ + ...newBase, + string: partial.string, + }); + }); + + test('should override string values in base with first and second partial value', () => { + const partial: PartialTestType = { number: 4 }; + const partials: PartialTestType[] = [{ string: 'test2' }]; + const newBase = mergePartial(base, partial, {}, partials); + expect(newBase).toEqual({ + ...newBase, + number: partial.number, + string: partials[0].string, + }); + }); + + test('should override string values in base with first, second and thrid partial value', () => { + const partial: PartialTestType = { number: 4 }; + const partials: PartialTestType[] = [ + { number: 10, string: 'test2' }, + { number: 20, string: 'nope', boolean: true }, + ]; + const newBase = mergePartial(base, partial, {}, partials); + expect(newBase).toEqual({ + ...newBase, + number: partial.number, + string: partials[0].string, + boolean: partials[1].boolean, + }); + }); + + test('should override complex array value in base', () => { + const partial: PartialTestType = { array1: [{ string: 'test1' }] }; + const partials: PartialTestType[] = [{ array1: [{ string: 'test2' }] }]; + const newBase = mergePartial(base, partial, {}, partials); + expect(newBase).toEqual({ + ...newBase, + array1: partial.array1, + }); + }); + + test('should override complex array value in base second partial', () => { + const partial: PartialTestType = {}; + const partials: PartialTestType[] = [{}, { array1: [{ string: 'test2' }] }]; + const newBase = mergePartial(base, partial, {}, partials); + expect(newBase).toEqual({ + ...newBase, + array1: partials[1].array1, + }); + }); + + test('should override simple array value in base', () => { + const partial: PartialTestType = { array2: [4, 5, 6] }; + const partials: PartialTestType[] = [{ array2: [7, 8, 9] }]; + const newBase = mergePartial(base, partial, {}, partials); + expect(newBase).toEqual({ + ...newBase, + array2: partial.array2, + }); + }); + + test('should override simple array value in base with partial', () => { + const partial: PartialTestType = {}; + const partials: PartialTestType[] = [{ array2: [7, 8, 9] }]; + const newBase = mergePartial(base, partial, {}, partials); + expect(newBase).toEqual({ + ...newBase, + array2: partials[0].array2, + }); + }); + + test('should override simple array value in base with second partial', () => { + const partial: PartialTestType = {}; + const partials: PartialTestType[] = [{}, { array2: [7, 8, 9] }]; + const newBase = mergePartial(base, partial, {}, partials); + expect(newBase).toEqual({ + ...newBase, + array2: partials[1].array2, + }); + }); + + test('should override nested values in base', () => { + const partial: PartialTestType = { nested: { number: 5 } }; + const partials: PartialTestType[] = [{ nested: { number: 10 } }]; + const newBase = mergePartial(base, partial, {}, partials); + expect(newBase).toEqual({ + ...newBase, + nested: { + ...newBase.nested, + number: partial.nested!.number, + }, + }); + }); + + test('should override nested values from partial', () => { + const partial: PartialTestType = {}; + const partials: PartialTestType[] = [{ nested: { number: 10 } }]; + const newBase = mergePartial(base, partial, {}, partials); + expect(newBase).toEqual({ + ...newBase, + nested: { + ...newBase.nested, + number: partials[0].nested!.number, + }, + }); + }); + }); + + describe('MergeOptions', () => { + describe('mergeOptionalPartialValues', () => { + interface OptionalTestType { + value1: string; + value2?: number; + value3: string; + value4?: OptionalTestType; + } + const defaultBase: OptionalTestType = { + value1: 'foo', + value3: 'bar', + value4: { + value1: 'foo', + value3: 'bar', + }, + }; + const partial1: RecursivePartial = { value1: 'baz', value2: 10 }; + const partial2: RecursivePartial = { value1: 'baz', value4: { value2: 10 } }; + + describe('mergeOptionalPartialValues is true', () => { + test('should merge optional parameters', () => { + const merged = mergePartial(defaultBase, partial1, { mergeOptionalPartialValues: true }); + expect(merged).toEqual({ + value1: 'baz', + value2: 10, + value3: 'bar', + value4: { + value1: 'foo', + value3: 'bar', + }, + }); + }); + + test('should merge nested optional parameters', () => { + const merged = mergePartial(defaultBase, partial2, { mergeOptionalPartialValues: true }); + expect(merged).toEqual({ + value1: 'baz', + value3: 'bar', + value4: { + value1: 'foo', + value2: 10, + value3: 'bar', + }, + }); + }); + + test('should merge optional params from partials', () => { + type PartialTestTypeOverride = PartialTestType & any; + const partial: PartialTestTypeOverride = { nick: 'test', number: 6 }; + const partials: PartialTestTypeOverride[] = [{ string: 'test', foo: 'bar' }, { array3: [3, 3, 3] }]; + const newBase = mergePartial(base, partial, { mergeOptionalPartialValues: true }, partials); + expect(newBase).toEqual({ + ...newBase, + ...partial, + ...partials[0], + ...partials[1], + }); + }); + }); + + describe('mergeOptionalPartialValues is false', () => { + test('should NOT merge optional parameters', () => { + const merged = mergePartial(defaultBase, partial1, { mergeOptionalPartialValues: false }); + expect(merged).toEqual({ + value1: 'baz', + value3: 'bar', + value4: { + value1: 'foo', + value3: 'bar', + }, + }); + }); + + test('should NOT merge nested optional parameters', () => { + const merged = mergePartial(defaultBase, partial2, { mergeOptionalPartialValues: false }); + expect(merged).toEqual({ + value1: 'baz', + value3: 'bar', + value4: { + value1: 'foo', + value3: 'bar', + }, + }); + }); + }); + }); + }); + }); + + describe('#getColorFromVariant', () => { + const seriesColor = '#626825'; + it('should return seriesColor if color is undefined', () => { + expect(getColorFromVariant(seriesColor)).toBe(seriesColor); + }); + + it('should return seriesColor color if ColorVariant is Series', () => { + expect(getColorFromVariant(seriesColor, ColorVariant.Series)).toBe(seriesColor); + }); + + it('should return transparent if ColorVariant is None', () => { + expect(getColorFromVariant(seriesColor, ColorVariant.None)).toBe('transparent'); + }); + + it('should return color if Color is passed', () => { + const color = '#f61c48'; + expect(getColorFromVariant(seriesColor, color)).toBe(color); + }); + }); +}); + +describe('#isUniqueArray', () => { + it('should return true for simple unique values', () => { + expect(isUniqueArray([1, 2])).toBe(true); + }); + + it('should return true for complex unique values', () => { + expect(isUniqueArray([{ n: 1 }, { n: 2 }], ({ n }) => n)).toBe(true); + }); + + it('should return false for simple duplicated values', () => { + expect(isUniqueArray([1, 1, 2])).toBe(false); + }); + + it('should return false for complex duplicated values', () => { + expect(isUniqueArray([{ n: 1 }, { n: 1 }, { n: 2 }], ({ n }) => n)).toBe(false); + }); +}); + +describe('#isDefined', () => { + it('should return false for null', () => { + expect(isDefined(null)).toBe(false); + }); + + it('should return false for undefined', () => { + expect(isDefined()).toBe(false); + }); + + it('should return true for zero', () => { + expect(isDefined(0)).toBe(true); + }); + + it('should return true for empty string', () => { + expect(isDefined('')).toBe(true); + }); + + it('should return true for empty false', () => { + expect(isDefined(false)).toBe(true); + }); + + it('should filter out null and undefined values', () => { + const values: (number | null | undefined)[] = [1, 2, null, 4, 5, undefined]; + const result: number[] = values.filter(isDefined); + expect(result).toEqual([1, 2, 4, 5]); + }); +}); + +describe('#isDefinedFrom', () => { + interface Test { + a?: number | string | boolean | null; + } + it('should filter out undefined values from complex types', () => { + const values: Partial[] = [ + { + a: 1, + }, + { + a: 'string', + }, + { + a: false, + }, + {}, + ]; + const result: NonNullable[] = values.filter(isDefinedFrom(({ a }) => a !== undefined)); + expect(result).toEqual(values.slice(0, -1)); + }); + + it('should filter out null values from complex types', () => { + const values: Test[] = [ + { + a: 1, + }, + { + a: 'string', + }, + { + a: false, + }, + { + a: null, + }, + ]; + const result: NonNullable[] = values.filter(isDefinedFrom(({ a }) => a !== null)); + expect(result).toEqual(values.slice(0, -1)); + }); + + it('should filter out null values from complex nested types', () => { + type NestedTest = { + aa: Test; + }; + const values: NestedTest[] = [ + { + aa: { a: 1 }, + }, + { + aa: { a: 'string' }, + }, + { + aa: { a: false }, + }, + { + aa: { a: null }, + }, + { + aa: {}, + }, + { + aa: { a: undefined }, + }, + ]; + const result: NonNullable[] = values.filter( + isDefinedFrom(({ aa }) => aa?.a !== undefined && aa?.a !== null), + ); + expect(result).toEqual(values.slice(0, 3)); + }); +}); diff --git a/packages/osd-charts/src/utils/common.tsx b/packages/osd-charts/src/utils/common.tsx new file mode 100644 index 000000000000..b67573dc14fb --- /dev/null +++ b/packages/osd-charts/src/utils/common.tsx @@ -0,0 +1,656 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { ComponentType, isValidElement, ReactNode } from 'react'; +import { $Values, isPrimitive } from 'utility-types'; +import { v1 as uuidV1 } from 'uuid'; + +import { PrimitiveValue } from '../chart_types/partition_chart/layout/utils/group_by_rollup'; +import { AdditiveNumber } from './accessor'; +import { Point } from './point'; + +/** @public */ +export const Position = Object.freeze({ + Top: 'top' as const, + Bottom: 'bottom' as const, + Left: 'left' as const, + Right: 'right' as const, +}); +/** @public */ +export type Position = $Values; + +/** @public */ +export const LayoutDirection = Object.freeze({ + Horizontal: 'horizontal' as const, + Vertical: 'vertical' as const, +}); +/** @public */ +export type LayoutDirection = $Values; + +/** + * Color variants that are unique to `@elastic/charts`. These go beyond the standard + * static color allocations. + * @public + */ +export const ColorVariant = Object.freeze({ + /** + * Uses series color. Rather than setting a static color, this will use the + * default series color for a given series. + */ + Series: '__use__series__color__' as const, + /** + * Uses empty color, similar to transparent. + */ + None: '__use__empty__color__' as const, +}); +/** @public */ +export type ColorVariant = $Values; + +/** @public */ +export const HorizontalAlignment = Object.freeze({ + Center: 'center' as const, + Right: Position.Right, + Left: Position.Left, + /** + * Aligns to near side of axis depending on position + * + * Examples: + * - Left Axis, `Near` will push the label to the `Right`, _near_ the axis + * - Right Axis, `Near` will push the axis labels to the `Left` + * - Top/Bottom Axes, `Near` will default to `Center` + */ + Near: 'near' as const, + /** + * Aligns to far side of axis depending on position + * + * Examples: + * - Left Axis, `Far` will push the label to the `Left`, _far_ from the axis + * - Right Axis, `Far` will push the axis labels to the `Right` + * - Top/Bottom Axes, `Far` will default to `Center` + */ + Far: 'far' as const, +}); + +/** + * Horizontal text alignment + * @public + */ +export type HorizontalAlignment = $Values; + +/** @public */ +export const VerticalAlignment = Object.freeze({ + Middle: 'middle' as const, + Top: Position.Top, + Bottom: Position.Bottom, + /** + * Aligns to near side of axis depending on position + * + * Examples: + * - Top Axis, `Near` will push the label to the `Right`, _near_ the axis + * - Bottom Axis, `Near` will push the axis labels to the `Left` + * - Left/Right Axes, `Near` will default to `Middle` + */ + Near: 'near' as const, + /** + * Aligns to far side of axis depending on position + * + * Examples: + * - Top Axis, `Far` will push the label to the `Top`, _far_ from the axis + * - Bottom Axis, `Far` will push the axis labels to the `Bottom` + * - Left/Right Axes, `Far` will default to `Middle` + */ + Far: 'far' as const, +}); + +/** + * Vertical text alignment + * @public + */ +export type VerticalAlignment = $Values; + +/** @public */ +export type Datum = any; // unknown; +/** @public */ +export type Rotation = 0 | 90 | -90 | 180; +/** @public */ +export type Rendering = 'canvas' | 'svg'; +/** @public */ +export type Color = string; // todo static/runtime type it this for proper color string content; several places in the code, and ultimate use, dictate it not be an empty string +/** @public */ +export type StrokeStyle = Color; // now narrower than string | CanvasGradient | CanvasPattern + +/** @internal */ +export function identity(value: T): T { + return value; +} + +/** @internal */ +export function compareByValueAsc(a: number | string, b: number | string): number { + return a > b ? 1 : -1; +} + +/** @internal */ +export function clamp(value: number, lowerBound: number, upperBound: number) { + return minValueWithLowerLimit(value, upperBound, lowerBound); +} + +/** + * Return the minimum value between val1 and val2. The value is bounded from below by lowerLimit + * @param val1 a numeric value + * @param val2 a numeric value + * @param lowerLimit the lower limit + * @internal + */ +export function minValueWithLowerLimit(val1: number, val2: number, lowerLimit: number) { + return Math.max(Math.min(val1, val2), lowerLimit); +} + +/** + * Return the maximum value between val1 and val2. The value is bounded from above by upperLimit + * @param val1 a numeric value + * @param val2 a numeric value + * @param upperLimit the upper limit + * @internal + */ +export function maxValueWithUpperLimit(val1: number, val2: number, upperLimit: number) { + return Math.min(Math.max(val1, val2), upperLimit); +} + +/** + * Returns color given any color variant + * + * @internal + */ +export function getColorFromVariant(seriesColor: Color, color?: Color | ColorVariant): Color { + if (color === ColorVariant.Series) { + return seriesColor; + } + + if (color === ColorVariant.None) { + return 'transparent'; + } + + return color || seriesColor; +} + +/** + * Converts degree to radians + * @param angle - in degrees + * @public + */ +export const getRadians = (angle: number) => (angle * Math.PI) / 180; + +/** + * This function returns a function to generate ids. + * This can be used to generate unique, but predictable ids to pair labels + * with their inputs. It takes an optional prefix as a parameter. If you don't + * specify it, it generates a random id prefix. If you specify a custom prefix + * it should begin with an letter to be HTML4 compliant. + * @internal + */ +export function htmlIdGenerator(idPrefix?: string) { + const prefix = idPrefix || `i${uuidV1()}`; + return (suffix?: string) => `${prefix}_${suffix || uuidV1()}`; +} + +/** + * Replaces all properties on any type as optional, includes nested types + * + * example: + * ```ts + * interface Person { + * name: string; + * age?: number; + * spouse: Person; + * children: Person[]; + * } + * type PartialPerson = RecursivePartial; + * // results in + * interface PartialPerson { + * name?: string; + * age?: number; + * spouse?: RecursivePartial; + * children?: RecursivePartial[] + * } + * ``` + * @public + */ +export type RecursivePartial = { + [P in keyof T]?: T[P] extends NonAny[] // checks for nested any[] + ? T[P] + : T[P] extends ReadonlyArray // checks for nested ReadonlyArray + ? T[P] + : T[P] extends (infer U)[] + ? RecursivePartial[] + : T[P] extends ReadonlyArray // eslint-disable-line @typescript-eslint/array-type + ? ReadonlyArray> // eslint-disable-line @typescript-eslint/array-type + : T[P] extends Set // checks for Sets + ? Set> + : T[P] extends Map // checks for Maps + ? Map> + : T[P] extends NonAny // checks for primitive values + ? T[P] + : IsUnknown extends 1 + ? T[P] + : RecursivePartial; // recurse for all non-array and non-primitive values +}; + +/** + * return True if T is `any`, otherwise return False + * @public + */ +export type IsAny = True | False extends (T extends never ? True : False) ? True : False; + +/** + * return True if T is `unknown`, otherwise return False + * @public + */ +export type IsUnknown = unknown extends T ? IsAny : False; + +/** @public */ +export type NonAny = number | boolean | string | symbol | null; + +/** @public */ +export interface MergeOptions { + /** + * Includes all available keys of every provided partial at a given level. + * This is opposite to normal behavior, which only uses keys from the base + * object to merge values. + * + * @defaultValue false + */ + mergeOptionalPartialValues?: boolean; + /** + * Merges Maps same as objects. By default this is disabled and Maps are replaced on the base + * with a defined Map on any partial. + * + * @defaultValue false + */ + mergeMaps?: boolean; +} + +/** @internal */ +export function getPartialValue(base: T, partial?: RecursivePartial, partials: RecursivePartial[] = []): T { + const partialWithValue = partial !== undefined ? partial : partials.find((v) => v !== undefined); + return partialWithValue !== undefined ? (partialWithValue as T) : base; +} + +/** + * Returns all top-level keys from one or more objects + * @param object - first object to get keys + * @param objects + * @internal + */ +export function getAllKeys(object: any, objects: any[] = []): string[] { + const initialKeys = object instanceof Map ? [...object.keys()] : Object.keys(object); + + return objects.reduce((keys: any[], obj) => { + if (obj && typeof obj === 'object') { + const newKeys = obj instanceof Map ? obj.keys() : Object.keys(obj); + keys.push(...newKeys); + } + + return keys; + }, initialKeys); +} + +/** @internal */ +export function isArrayOrSet(value: any): value is Array | Set { + return Array.isArray(value) || value instanceof Set; +} + +/** @internal */ +export function isNil(value: any): value is null | undefined { + return value === null || value === undefined; +} + +/** @internal */ +export function hasPartialObjectToMerge( + base: T, + partial?: RecursivePartial, + additionalPartials: RecursivePartial[] = [], +): boolean { + if (isArrayOrSet(base)) { + return false; + } + + if (typeof base === 'object' && base !== null) { + if (typeof partial === 'object' && !isArrayOrSet(partial) && partial !== null) { + return true; + } + + return additionalPartials.some((p) => typeof p === 'object' && !Array.isArray(p)); + } + + return false; +} + +/** @internal */ +export function shallowClone(value: any) { + if (Array.isArray(value)) { + return [...value]; + } + + if (value instanceof Set) { + return new Set([...value]); + } + + if (typeof value === 'object' && value !== null) { + if (value instanceof Map) { + return new Map(value.entries()); + } + + return { ...value }; + } + + return value; +} + +function isReactNode(el: any): el is ReactNode { + return isNil(el) || isPrimitive(el) || isValidElement(el); +} + +function isReactComponent

    >(el: any): el is ComponentType

    { + return !isReactNode(el); +} + +/** + * Renders simple react node or react component with props + * @internal + */ +export function renderWithProps

    >(El: ReactNode | ComponentType

    , props: P): ReactNode { + return isReactComponent

    (El) ? : El; +} + +/** + * Merges values of a partial structure with a base structure. + * + * @note No nested array merging + * + * @param base structure to be duplicated, must have all props of `partial` + * @param partial structure to override values from base + * + * @returns new base structure with updated partial values + * @internal + */ +export function mergePartial( + base: T, + partial?: RecursivePartial, + options: MergeOptions = {}, + additionalPartials: RecursivePartial[] = [], +): T { + const baseClone = shallowClone(base); + + if (hasPartialObjectToMerge(base, partial, additionalPartials)) { + const mapCondition = !(baseClone instanceof Map) || options.mergeMaps; + if (partial !== undefined && options.mergeOptionalPartialValues && mapCondition) { + getAllKeys(partial, additionalPartials).forEach((key) => { + if (baseClone instanceof Map) { + if (!baseClone.has(key)) { + baseClone.set( + key, + (partial as any).get(key) !== undefined + ? (partial as any).get(key) + : additionalPartials.find((v: any) => v.get(key) !== undefined) || new Map().get(key), + ); + } + } else if (!(key in baseClone)) { + baseClone[key] = + (partial as any)[key] !== undefined + ? (partial as any)[key] + : (additionalPartials.find((v: any) => v[key] !== undefined) || ({} as any))[key]; + } + }); + } + + if (baseClone instanceof Map) { + if (options.mergeMaps) { + return [...baseClone.keys()].reduce((newBase: Map, key) => { + const partialValue = partial && (partial as any).get(key); + const partialValues = additionalPartials.map((v) => + typeof v === 'object' && v instanceof Map ? v.get(key) : undefined, + ); + const baseValue = (base as any).get(key); + + newBase.set(key, mergePartial(baseValue, partialValue, options, partialValues)); + + return newBase; + }, baseClone as any); + } + + if (partial !== undefined) { + return partial as any; + } + + const additional = additionalPartials.find((p: any) => p !== undefined); + if (additional) { + return additional as any; + } + + return baseClone as any; + } + + return Object.keys(base).reduce((newBase, key) => { + const partialValue = partial && (partial as any)[key]; + const partialValues = additionalPartials.map((v) => (typeof v === 'object' ? (v as any)[key] : undefined)); + const baseValue = (base as any)[key]; + + newBase[key] = mergePartial(baseValue, partialValue, options, partialValues); + + return newBase; + }, baseClone); + } + + return getPartialValue(baseClone, partial, additionalPartials); +} + +/** @internal */ +export function getUniqueValues(fullArray: T[], uniqueProperty: keyof T, filterConsecutives = false): T[] { + return fullArray.reduce<{ + filtered: T[]; + uniqueValues: Set; + }>( + (acc, currentValue) => { + const uniqueValue = currentValue[uniqueProperty]; + if (acc.uniqueValues.has(uniqueValue)) { + return acc; + } + if (filterConsecutives) { + acc.uniqueValues.clear(); + acc.uniqueValues.add(uniqueValue); + } else { + acc.uniqueValues.add(uniqueValue); + } + acc.filtered.push(currentValue); + return acc; + }, + { + filtered: [], + uniqueValues: new Set(), + }, + ).filtered; +} + +/** @public */ +export type ValueFormatter = (value: number) => string; +/** @public */ +export type ValueAccessor = (d: Datum) => AdditiveNumber; +/** @public */ +export type LabelAccessor = (value: PrimitiveValue) => string; +/** @public */ +export type ShowAccessor = (value: PrimitiveValue) => boolean; + +/** + * Returns planar distance bewtween two points + * @internal + */ +export function getDistance(a: Point, b: Point): number { + return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2)); +} + +/** @internal */ +export function stringifyNullsUndefined(value?: PrimitiveValue): string | number { + if (value === undefined) { + return 'undefined'; + } + + if (value === null) { + return 'null'; + } + + return value; +} + +/** + * Determines if an array has all unique values + * + * examples: + * ```ts + * isUniqueArray([1, 2]) // => true + * isUniqueArray([1, 1, 2]) // => false + * isUniqueArray([{ n: 1 }, { n: 1 }, { n: 2 }], ({ n }) => n) // => false + * ``` + * + * @internal + * @param {B[]} arr + * @param {(d:B)=>T} extractor? extract the value from B + */ +export function isUniqueArray(arr: B[], extractor?: (value: B) => T) { + const values = new Set(); + + return (function isUniqueArrayFn() { + return arr.every((v) => { + const value = extractor ? extractor(v) : v; + + if (values.has(value)) { + return false; + } + + values.add(value); + return true; + }); + })(); +} + +/** + * Returns defined value type if not null nor undefined + * + * @internal + */ +export function isDefined(value?: T): value is NonNullable { + return value !== null && value !== undefined; +} + +/** + * Returns defined value type if value from getter function is not null nor undefined + * + * **IMPORTANT**: You must provide an accurate typeCheck function that will filter out _EVERY_ + * item in the array that is not of type `T`. If not, the type check will override the + * type as `T` which may be incorrect. + * + * @internal + */ +export function isDefinedFrom(typeCheck: (value: RecursivePartial) => boolean) { + return (value?: RecursivePartial): value is NonNullable => { + if (value === undefined) { + return false; + } + + try { + return typeCheck(value); + } catch { + return false; + } + }; +} + +/** + * Returns rounded number to given decimals + * + * @internal + */ +export const round = (value: number, fractionDigits = 0): number => { + const precision = Math.pow(10, Math.max(fractionDigits, 0)); + const scaledValue = Math.floor(value * precision); + + return scaledValue / precision; +}; + +/** + * Get number/percentage value from string + * + * i.e. `'90%'` with relative value of `100` returns `90` + * @internal + */ +export function getPercentageValue(ratio: string | number, relativeValue: number, defaultValue: T): number | T { + if (typeof ratio === 'number') { + return Math.abs(ratio); + } + + const ratioStr = ratio.trim(); + + if (/\d+%$/.test(ratioStr)) { + const percentage = Math.abs(Number.parseInt(ratioStr.slice(0, -1), 10)); + return relativeValue * (percentage / 100); + } + const num = Number.parseFloat(ratioStr); + + return num && !isNaN(num) ? Math.abs(num) : defaultValue; +} + +/** + * Predicate function, eg. to be called with [].filter, to keep distinct values + * @example [1, 2, 4, 2, 4, 0, 3, 2].filter(keepDistinct) ==> [1, 2, 4, 0, 3] + * @internal + */ +export function keepDistinct(d: T, i: number, a: T[]): boolean { + return a.indexOf(d) === i; +} + +/** + * Return an object which keys are values of an object and the value is the + * static one provided + * @public + */ +export function toEntries, S>( + array: T[], + accessor: keyof T, + staticValue: S, +): Record { + return array.reduce>((acc, curr) => { + acc[curr[accessor]] = staticValue; + return acc; + }, {}); +} + +/** + * Safely format values with error handling + * @internal + */ +export function safeFormat(value: V, formatter?: (value: V) => string): string { + if (formatter) { + try { + return formatter(value); + } catch { + // fallthrough + } + } + + return `${value}`; +} diff --git a/packages/osd-charts/src/utils/curves.test.ts b/packages/osd-charts/src/utils/curves.test.ts new file mode 100644 index 000000000000..57621b347c72 --- /dev/null +++ b/packages/osd-charts/src/utils/curves.test.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + curveBasis, + curveCardinal, + curveCatmullRom, + curveLinear, + curveMonotoneX, + curveMonotoneY, + curveNatural, + curveStep, + curveStepAfter, + curveStepBefore, +} from 'd3-shape'; + +import { CurveType, getCurveFactory } from './curves'; + +describe('Curve utils', () => { + test('factory', () => { + expect(getCurveFactory(CurveType.CURVE_CARDINAL)).toBe(curveCardinal); + expect(getCurveFactory(CurveType.CURVE_BASIS)).toBe(curveBasis); + expect(getCurveFactory(CurveType.CURVE_CATMULL_ROM)).toBe(curveCatmullRom); + expect(getCurveFactory(CurveType.CURVE_MONOTONE_X)).toBe(curveMonotoneX); + expect(getCurveFactory(CurveType.CURVE_MONOTONE_Y)).toBe(curveMonotoneY); + expect(getCurveFactory(CurveType.CURVE_NATURAL)).toBe(curveNatural); + expect(getCurveFactory(CurveType.CURVE_STEP)).toBe(curveStep); + expect(getCurveFactory(CurveType.CURVE_STEP_AFTER)).toBe(curveStepAfter); + expect(getCurveFactory(CurveType.CURVE_STEP_BEFORE)).toBe(curveStepBefore); + expect(getCurveFactory(CurveType.LINEAR)).toBe(curveLinear); + expect(getCurveFactory()).toBe(curveLinear); + }); +}); diff --git a/packages/osd-charts/src/utils/curves.ts b/packages/osd-charts/src/utils/curves.ts new file mode 100644 index 000000000000..c2f23223fe14 --- /dev/null +++ b/packages/osd-charts/src/utils/curves.ts @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + curveBasis, + curveCardinal, + curveCatmullRom, + curveLinear, + curveMonotoneX, + curveMonotoneY, + curveNatural, + curveStep, + curveStepAfter, + curveStepBefore, +} from 'd3-shape'; +import { $Values } from 'utility-types'; + +/** @public */ +export const CurveType = Object.freeze({ + CURVE_CARDINAL: 0 as const, + CURVE_NATURAL: 1 as const, + CURVE_MONOTONE_X: 2 as const, + CURVE_MONOTONE_Y: 3 as const, + CURVE_BASIS: 4 as const, + CURVE_CATMULL_ROM: 5 as const, + CURVE_STEP: 6 as const, + CURVE_STEP_AFTER: 7 as const, + CURVE_STEP_BEFORE: 8 as const, + LINEAR: 9 as const, +}); + +/** @public */ +export type CurveType = $Values; + +/** @internal */ +export function getCurveFactory(curveType: CurveType = CurveType.LINEAR) { + switch (curveType) { + case CurveType.CURVE_CARDINAL: + return curveCardinal; + case CurveType.CURVE_NATURAL: + return curveNatural; + case CurveType.CURVE_MONOTONE_X: + return curveMonotoneX; + case CurveType.CURVE_MONOTONE_Y: + return curveMonotoneY; + case CurveType.CURVE_BASIS: + return curveBasis; + case CurveType.CURVE_CATMULL_ROM: + return curveCatmullRom; + case CurveType.CURVE_STEP: + return curveStep; + case CurveType.CURVE_STEP_AFTER: + return curveStepAfter; + case CurveType.CURVE_STEP_BEFORE: + return curveStepBefore; + case CurveType.LINEAR: + default: + return curveLinear; + } +} diff --git a/packages/osd-charts/src/utils/d3-delaunay/index.ts b/packages/osd-charts/src/utils/d3-delaunay/index.ts new file mode 100644 index 000000000000..eae4d0d23c11 --- /dev/null +++ b/packages/osd-charts/src/utils/d3-delaunay/index.ts @@ -0,0 +1,1543 @@ +/** + * @notice + * This product includes code that is adapted d3-delaunay@5.2.1, + * which is available under a "ISC" license. + * + * Copyright 2018 Observable, Inc. + * + * Permission to use, copy, modify, and/or distribute this software for any purpose + * with or without fee is hereby granted, provided that the above copyright notice + * and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER + * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + * THIS SOFTWARE. + */ + +// @ts-nocheck + +/** + * Delaunay triangulation + */ +interface DelaunayI

    { + /** + * The coordinates of the points as an array [x0, y0, x1, y1, ...]. + * Typically, this is a Float64Array, however you can use any array-like type in the constructor. + */ + points: ArrayLike; + + /** + * The halfedge indices as an Int32Array [j0, j1, ...]. + * For each index 0 <= i < halfedges.length, there is a halfedge from triangle vertex j = halfedges[i] to triangle vertex i. + */ + halfedges: Int32Array; + + /** + * An arbitrary node on the convex hull. + * The convex hull is represented as a circular doubly-linked list of nodes. + */ + hull: Node; + + /** + * The triangle vertex indices as an Uint32Array [i0, j0, k0, i1, j1, k1, ...]. + * Each contiguous triplet of indices i, j, k forms a counterclockwise triangle. + * The coordinates of the triangle's points can be found by going through 'points'. + */ + triangles: Uint32Array; + + /** + * The incoming halfedge indexes as a Int32Array [e0, e1, e2, ...]. + * For each point i, inedges[i] is the halfedge index e of an incoming halfedge. + * For coincident points, the halfedge index is -1; for points on the convex hull, the incoming halfedge is on the convex hull; for other points, the choice of incoming halfedge is arbitrary. + */ + inedges: Int32Array; + + /** + * The outgoing halfedge indexes as a Int32Array [e0, e1, e2, ...]. + * For each point i on the convex hull, outedges[i] is the halfedge index e of the corresponding outgoing halfedge; for other points, the halfedge index is -1. + */ + outedges: Int32Array; + + /** + * Returns the index of the input point that is closest to the specified point ⟨x, y⟩. + * The search is started at the specified point i. If i is not specified, it defaults to zero. + */ + find(x: number, y: number, i?: number): number; + + /** + * Returns an iterable over the indexes of the neighboring points to the specified point i. + * The iterable is empty if i is a coincident point. + */ + neighbors(i: number): IterableIterator; + + /** + * Returns the closed polygon [[x0, y0], [x1, y1], ..., [x0, y0]] representing the convex hull. + */ + hullPolygon(): Polygon; + + /** + * Returns the closed polygon [[x0, y0], [x1, y1], [x2, y2], [x0, y0]] representing the triangle i. + */ + trianglePolygon(i: number): Triangle; + + /** + * Returns an iterable over the polygons for each triangle, in order. + */ + trianglePolygons(): IterableIterator; + + /** + * Returns the Voronoi diagram for the associated points. + * When rendering, the diagram will be clipped to the specified bounds = [xmin, ymin, xmax, ymax]. + * If bounds is not specified, it defaults to [0, 0, 960, 500]. + * See To Infinity and Back Again for an interactive explanation of Voronoi cell clipping. + */ + voronoi(bounds?: Bounds): Voronoi

    ; +} + +/** + * A point represented as an array tuple [x, y]. + */ +type Point = number[]; + +/** + * A closed polygon [[x0, y0], [x1, y1], [x2, y2], [x0, y0]] representing a triangle. + */ +type Triangle = Point[]; + +/** + * A closed polygon [[x0, y0], [x1, y1], ..., [x0, y0]]. + */ +type PolygonI = Point[]; + +/** + * A rectangular area [x, y, width, height]. + */ +export type Bounds = number[]; + +/** + * A function to extract a x- or y-coordinate from the specified point. + */ +type GetCoordinate = (point: P, i: number, points: PS) => number; + +/** + * A point node on a convex hull (represented as a circular linked list). + */ +interface Node { + /** + * The index of the associated point. + */ + i: number; + + /** + * The x-coordinate of the associated point. + */ + x: number; + + /** + * The y-coordinate of the associated point. + */ + y: number; + + /** + * The index of the (incoming or outgoing?) associated halfedge. + */ + t: number; + + /** + * The previous node on the hull. + */ + prev: Node; + + /** + * The next node on the hull. + */ + next: Node; + + /** + * Whether the node has been removed from the linked list. + */ + removed: boolean; +} + +/** + * An interface for the rect() method of the CanvasPathMethods API. + */ +interface RectContext { + /** + * rect() method of the CanvasPathMethods API. + */ + rect(x: number, y: number, width: number, height: number): void; +} + +/** + * An interface for the moveTo() method of the CanvasPathMethods API. + */ +interface MoveContext { + /** + * moveTo() method of the CanvasPathMethods API. + */ + moveTo(x: number, y: number): void; +} + +/** + * An interface for the lineTo() method of the CanvasPathMethods API. + */ +interface LineContext { + /** + * lineTo() method of the CanvasPathMethods API. + */ + lineTo(x: number, y: number): void; +} + +/** + * An interface for the arc() method of the CanvasPathMethods API. + */ +interface ArcContext { + /** + * arc() method of the CanvasPathMethods API. + */ + arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void; +} + +/** + * An interface for the closePath() method of the CanvasPathMethods API. + */ +interface ClosableContext { + /** + * closePath() method of the CanvasPathMethods API. + */ + closePath(): void; +} + +/** + * Voronoi regions + */ +interface VoronoiI

    { + /** + * The Voronoi diagram’s associated Delaunay triangulation. + */ + delaunay: DelaunayI

    ; + + /** + * The circumcenters of the Delaunay triangles [cx0, cy0, cx1, cy1, ...]. + * Each contiguous pair of coordinates cx, cy is the circumcenter for the corresponding triangle. + * These circumcenters form the coordinates of the Voronoi cell polygons. + */ + circumcenters: Float64Array; + + /** + * An array [vx0, vy0, wx0, wy0, ...] where each non-zero quadruple describes an open (infinite) cell + * on the outer hull, giving the directions of two open half-lines. + */ + vectors: Float64Array; + + /** + * The bounds of the viewport [xmin, ymin, xmax, ymax] for rendering the Voronoi diagram. + * These values only affect the rendering methods (voronoi.render, voronoi.renderBounds, cell.render). + */ + xmin: number; + ymin: number; + xmax: number; + ymax: number; + + /** + * Returns true if the cell with the specified index i contains the specified point ⟨x, y⟩. + * (This method is not affected by the associated Voronoi diagram’s viewport bounds.) + */ + contains(i: number, x: number, y: number): boolean; + + /** + * Returns the convex, closed polygon [[x0, y0], [x1, y1], ..., [x0, y0]] representing the cell for the specified point i. + */ + cellPolygon(i: number): PolygonI; + + /** + * Returns an iterable over the polygons for each cell, in order. + */ + cellPolygons(): IterableIterator; +} + +// https://github.com/d3/d3-delaunay v5.2.1 Copyright 2020 Mike Bostock +// https://github.com/mapbox/delaunator v4.0.1. Copyright 2019 Mapbox, Inc. + +// Type definitions for d3-delaunay 4.1 +// Project: https://github.com/d3/d3-delaunay +// Definitions by: Bradley Odell +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +const EPSILON = Math.pow(2, -52); +const EDGE_STACK = new Uint32Array(512); + +class Delaunator { + static from(points, getX = defaultGetX, getY = defaultGetY) { + const n = points.length; + const coords = new Float64Array(n * 2); + + for (let i = 0; i < n; i++) { + const p = points[i]; + coords[2 * i] = getX(p); + coords[2 * i + 1] = getY(p); + } + + return new Delaunator(coords); + } + + constructor(coords) { + const n = coords.length >> 1; + if (n > 0 && typeof coords[0] !== 'number') throw new Error('Expected coords to contain numbers.'); + + this.coords = coords; + + // arrays that will store the triangulation graph + const maxTriangles = Math.max(2 * n - 5, 0); + this._triangles = new Uint32Array(maxTriangles * 3); + this._halfedges = new Int32Array(maxTriangles * 3); + + // temporary arrays for tracking the edges of the advancing convex hull + this._hashSize = Math.ceil(Math.sqrt(n)); + this._hullPrev = new Uint32Array(n); // edge to prev edge + this._hullNext = new Uint32Array(n); // edge to next edge + this._hullTri = new Uint32Array(n); // edge to adjacent triangle + this._hullHash = new Int32Array(this._hashSize).fill(-1); // angular edge hash + + // temporary arrays for sorting points + this._ids = new Uint32Array(n); + this._dists = new Float64Array(n); + + this.update(); + } + + update() { + const { coords, _hullPrev: hullPrev, _hullNext: hullNext, _hullTri: hullTri, _hullHash: hullHash } = this; + const n = coords.length >> 1; + + // populate an array of point indices; calculate input data bbox + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + + for (let i = 0; i < n; i++) { + const x = coords[2 * i]; + const y = coords[2 * i + 1]; + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + this._ids[i] = i; + } + const cx = (minX + maxX) / 2; + const cy = (minY + maxY) / 2; + + let minDist = Infinity; + let i0, i1, i2; + + // pick a seed point close to the center + for (let i = 0; i < n; i++) { + const d = dist(cx, cy, coords[2 * i], coords[2 * i + 1]); + if (d < minDist) { + i0 = i; + minDist = d; + } + } + const i0x = coords[2 * i0]; + const i0y = coords[2 * i0 + 1]; + + minDist = Infinity; + + // find the point closest to the seed + for (let i = 0; i < n; i++) { + if (i === i0) continue; + const d = dist(i0x, i0y, coords[2 * i], coords[2 * i + 1]); + if (d < minDist && d > 0) { + i1 = i; + minDist = d; + } + } + let i1x = coords[2 * i1]; + let i1y = coords[2 * i1 + 1]; + + let minRadius = Infinity; + + // find the third point which forms the smallest circumcircle with the first two + for (let i = 0; i < n; i++) { + if (i === i0 || i === i1) continue; + const r = circumradius(i0x, i0y, i1x, i1y, coords[2 * i], coords[2 * i + 1]); + if (r < minRadius) { + i2 = i; + minRadius = r; + } + } + let i2x = coords[2 * i2]; + let i2y = coords[2 * i2 + 1]; + + if (minRadius === Infinity) { + // order collinear points by dx (or dy if all x are identical) + // and return the list as a hull + for (let i = 0; i < n; i++) { + this._dists[i] = coords[2 * i] - coords[0] || coords[2 * i + 1] - coords[1]; + } + quicksort(this._ids, this._dists, 0, n - 1); + const hull = new Uint32Array(n); + let j = 0; + for (let i = 0, d0 = -Infinity; i < n; i++) { + const id = this._ids[i]; + if (this._dists[id] > d0) { + hull[j++] = id; + d0 = this._dists[id]; + } + } + this.hull = hull.subarray(0, j); + this.triangles = new Uint32Array(0); + this.halfedges = new Uint32Array(0); + return; + } + + // swap the order of the seed points for counter-clockwise orientation + if (orient(i0x, i0y, i1x, i1y, i2x, i2y)) { + const i = i1; + const x = i1x; + const y = i1y; + i1 = i2; + i1x = i2x; + i1y = i2y; + i2 = i; + i2x = x; + i2y = y; + } + + const center = circumcenter(i0x, i0y, i1x, i1y, i2x, i2y); + this._cx = center.x; + this._cy = center.y; + + for (let i = 0; i < n; i++) { + this._dists[i] = dist(coords[2 * i], coords[2 * i + 1], center.x, center.y); + } + + // sort the points by distance from the seed triangle circumcenter + quicksort(this._ids, this._dists, 0, n - 1); + + // set up the seed triangle as the starting hull + this._hullStart = i0; + let hullSize = 3; + + hullNext[i0] = hullPrev[i2] = i1; + hullNext[i1] = hullPrev[i0] = i2; + hullNext[i2] = hullPrev[i1] = i0; + + hullTri[i0] = 0; + hullTri[i1] = 1; + hullTri[i2] = 2; + + hullHash.fill(-1); + hullHash[this._hashKey(i0x, i0y)] = i0; + hullHash[this._hashKey(i1x, i1y)] = i1; + hullHash[this._hashKey(i2x, i2y)] = i2; + + this.trianglesLen = 0; + this._addTriangle(i0, i1, i2, -1, -1, -1); + + for (let k = 0, xp, yp; k < this._ids.length; k++) { + const i = this._ids[k]; + const x = coords[2 * i]; + const y = coords[2 * i + 1]; + + // skip near-duplicate points + if (k > 0 && Math.abs(x - xp) <= EPSILON && Math.abs(y - yp) <= EPSILON) continue; + xp = x; + yp = y; + + // skip seed triangle points + if (i === i0 || i === i1 || i === i2) continue; + + // find a visible edge on the convex hull using edge hash + let start = 0; + for (let j = 0, key = this._hashKey(x, y); j < this._hashSize; j++) { + start = hullHash[(key + j) % this._hashSize]; + if (start !== -1 && start !== hullNext[start]) break; + } + + start = hullPrev[start]; + let e = start, + q; + while (((q = hullNext[e]), !orient(x, y, coords[2 * e], coords[2 * e + 1], coords[2 * q], coords[2 * q + 1]))) { + e = q; + if (e === start) { + e = -1; + break; + } + } + if (e === -1) continue; // likely a near-duplicate point; skip it + + // add the first triangle from the point + let t = this._addTriangle(e, i, hullNext[e], -1, -1, hullTri[e]); + + // recursively flip triangles from the point until they satisfy the Delaunay condition + hullTri[i] = this._legalize(t + 2); + hullTri[e] = t; // keep track of boundary triangles on the hull + hullSize++; + + // walk forward through the hull, adding more triangles and flipping recursively + let n = hullNext[e]; + while (((q = hullNext[n]), orient(x, y, coords[2 * n], coords[2 * n + 1], coords[2 * q], coords[2 * q + 1]))) { + t = this._addTriangle(n, i, q, hullTri[i], -1, hullTri[n]); + hullTri[i] = this._legalize(t + 2); + hullNext[n] = n; // mark as removed + hullSize--; + n = q; + } + + // walk backward from the other side, adding more triangles and flipping + if (e === start) { + while (((q = hullPrev[e]), orient(x, y, coords[2 * q], coords[2 * q + 1], coords[2 * e], coords[2 * e + 1]))) { + t = this._addTriangle(q, i, e, -1, hullTri[e], hullTri[q]); + this._legalize(t + 2); + hullTri[q] = t; + hullNext[e] = e; // mark as removed + hullSize--; + e = q; + } + } + + // update the hull indices + this._hullStart = hullPrev[i] = e; + hullNext[e] = hullPrev[n] = i; + hullNext[i] = n; + + // save the two new edges in the hash table + hullHash[this._hashKey(x, y)] = i; + hullHash[this._hashKey(coords[2 * e], coords[2 * e + 1])] = e; + } + + this.hull = new Uint32Array(hullSize); + for (let i = 0, e = this._hullStart; i < hullSize; i++) { + this.hull[i] = e; + e = hullNext[e]; + } + + // trim typed triangle mesh arrays + this.triangles = this._triangles.subarray(0, this.trianglesLen); + this.halfedges = this._halfedges.subarray(0, this.trianglesLen); + } + + _hashKey(x, y) { + return Math.floor(pseudoAngle(x - this._cx, y - this._cy) * this._hashSize) % this._hashSize; + } + + _legalize(a) { + const { _triangles: triangles, _halfedges: halfedges, coords } = this; + + let i = 0; + let ar = 0; + + // recursion eliminated with a fixed-size stack + while (true) { + const b = halfedges[a]; + + /* if the pair of triangles doesn't satisfy the Delaunay condition + * (p1 is inside the circumcircle of [p0, pl, pr]), flip them, + * then do the same check/flip recursively for the new pair of triangles + * + * pl pl + * /||\ / \ + * al/ || \bl al/ \a + * / || \ / \ + * / a||b \ flip /___ar___\ + * p0\ || /p1 => p0\---bl---/p1 + * \ || / \ / + * ar\ || /br b\ /br + * \||/ \ / + * pr pr + */ + const a0 = a - (a % 3); + ar = a0 + ((a + 2) % 3); + + if (b === -1) { + // convex hull edge + if (i === 0) break; + a = EDGE_STACK[--i]; + continue; + } + + const b0 = b - (b % 3); + const al = a0 + ((a + 1) % 3); + const bl = b0 + ((b + 2) % 3); + + const p0 = triangles[ar]; + const pr = triangles[a]; + const pl = triangles[al]; + const p1 = triangles[bl]; + + const illegal = inCircle( + coords[2 * p0], + coords[2 * p0 + 1], + coords[2 * pr], + coords[2 * pr + 1], + coords[2 * pl], + coords[2 * pl + 1], + coords[2 * p1], + coords[2 * p1 + 1], + ); + + if (illegal) { + triangles[a] = p1; + triangles[b] = p0; + + const hbl = halfedges[bl]; + + // edge swapped on the other side of the hull (rare); fix the halfedge reference + if (hbl === -1) { + let e = this._hullStart; + do { + if (this._hullTri[e] === bl) { + this._hullTri[e] = a; + break; + } + e = this._hullPrev[e]; + } while (e !== this._hullStart); + } + this._link(a, hbl); + this._link(b, halfedges[ar]); + this._link(ar, bl); + + const br = b0 + ((b + 1) % 3); + + // don't worry about hitting the cap: it can only happen on extremely degenerate input + if (i < EDGE_STACK.length) { + EDGE_STACK[i++] = br; + } + } else { + if (i === 0) break; + a = EDGE_STACK[--i]; + } + } + + return ar; + } + + _link(a, b) { + this._halfedges[a] = b; + if (b !== -1) this._halfedges[b] = a; + } + + // add a new triangle given vertex indices and adjacent half-edge ids + _addTriangle(i0, i1, i2, a, b, c) { + const t = this.trianglesLen; + + this._triangles[t] = i0; + this._triangles[t + 1] = i1; + this._triangles[t + 2] = i2; + + this._link(t, a); + this._link(t + 1, b); + this._link(t + 2, c); + + this.trianglesLen += 3; + + return t; + } +} + +// monotonically increases with real angle, but doesn't need expensive trigonometry +function pseudoAngle(dx, dy) { + const p = dx / (Math.abs(dx) + Math.abs(dy)); + return (dy > 0 ? 3 - p : 1 + p) / 4; // [0..1] +} + +function dist(ax, ay, bx, by) { + const dx = ax - bx; + const dy = ay - by; + return dx * dx + dy * dy; +} + +// return 2d orientation sign if we're confident in it through J. Shewchuk's error bound check +function orientIfSure(px, py, rx, ry, qx, qy) { + const l = (ry - py) * (qx - px); + const r = (rx - px) * (qy - py); + return Math.abs(l - r) >= 3.3306690738754716e-16 * Math.abs(l + r) ? l - r : 0; +} + +// a more robust orientation test that's stable in a given triangle (to fix robustness issues) +function orient(rx, ry, qx, qy, px, py) { + const sign = + orientIfSure(px, py, rx, ry, qx, qy) || + orientIfSure(rx, ry, qx, qy, px, py) || + orientIfSure(qx, qy, px, py, rx, ry); + return sign < 0; +} + +function inCircle(ax, ay, bx, by, cx, cy, px, py) { + const dx = ax - px; + const dy = ay - py; + const ex = bx - px; + const ey = by - py; + const fx = cx - px; + const fy = cy - py; + + const ap = dx * dx + dy * dy; + const bp = ex * ex + ey * ey; + const cp = fx * fx + fy * fy; + + return dx * (ey * cp - bp * fy) - dy * (ex * cp - bp * fx) + ap * (ex * fy - ey * fx) < 0; +} + +function circumradius(ax, ay, bx, by, cx, cy) { + const dx = bx - ax; + const dy = by - ay; + const ex = cx - ax; + const ey = cy - ay; + + const bl = dx * dx + dy * dy; + const cl = ex * ex + ey * ey; + const d = 0.5 / (dx * ey - dy * ex); + + const x = (ey * bl - dy * cl) * d; + const y = (dx * cl - ex * bl) * d; + + return x * x + y * y; +} + +function circumcenter(ax, ay, bx, by, cx, cy) { + const dx = bx - ax; + const dy = by - ay; + const ex = cx - ax; + const ey = cy - ay; + + const bl = dx * dx + dy * dy; + const cl = ex * ex + ey * ey; + const d = 0.5 / (dx * ey - dy * ex); + + const x = ax + (ey * bl - dy * cl) * d; + const y = ay + (dx * cl - ex * bl) * d; + + return { x, y }; +} + +function quicksort(ids, dists, left, right) { + if (right - left <= 20) { + for (let i = left + 1; i <= right; i++) { + const temp = ids[i]; + const tempDist = dists[temp]; + let j = i - 1; + while (j >= left && dists[ids[j]] > tempDist) ids[j + 1] = ids[j--]; + ids[j + 1] = temp; + } + } else { + const median = (left + right) >> 1; + let i = left + 1; + let j = right; + swap(ids, median, i); + if (dists[ids[left]] > dists[ids[right]]) swap(ids, left, right); + if (dists[ids[i]] > dists[ids[right]]) swap(ids, i, right); + if (dists[ids[left]] > dists[ids[i]]) swap(ids, left, i); + + const temp = ids[i]; + const tempDist = dists[temp]; + while (true) { + do i++; + while (dists[ids[i]] < tempDist); + do j--; + while (dists[ids[j]] > tempDist); + if (j < i) break; + swap(ids, i, j); + } + ids[left + 1] = ids[j]; + ids[j] = temp; + + if (right - i + 1 >= j - left) { + quicksort(ids, dists, i, right); + quicksort(ids, dists, left, j - 1); + } else { + quicksort(ids, dists, left, j - 1); + quicksort(ids, dists, i, right); + } + } +} + +function swap(arr, i, j) { + const tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; +} + +function defaultGetX(p) { + return p[0]; +} +function defaultGetY(p) { + return p[1]; +} + +const epsilon = 1e-6; + +class Path { + constructor() { + this._x0 = this._y0 = this._x1 = this._y1 = null; // start of current subpath // end of current subpath + this._ = ''; + } + moveTo(x, y) { + this._ += `M${(this._x0 = this._x1 = +x)},${(this._y0 = this._y1 = +y)}`; + } + closePath() { + if (this._x1 !== null) { + (this._x1 = this._x0), (this._y1 = this._y0); + this._ += 'Z'; + } + } + lineTo(x, y) { + this._ += `L${(this._x1 = +x)},${(this._y1 = +y)}`; + } + arc(x, y, r) { + (x = +x), (y = +y), (r = +r); + const x0 = x + r; + const y0 = y; + if (r < 0) throw new Error('negative radius'); + if (this._x1 === null) this._ += `M${x0},${y0}`; + else if (Math.abs(this._x1 - x0) > epsilon || Math.abs(this._y1 - y0) > epsilon) this._ += 'L' + x0 + ',' + y0; + if (!r) return; + this._ += `A${r},${r},0,1,1,${x - r},${y}A${r},${r},0,1,1,${(this._x1 = x0)},${(this._y1 = y0)}`; + } + rect(x, y, w, h) { + this._ += `M${(this._x0 = this._x1 = +x)},${(this._y0 = this._y1 = +y)}h${+w}v${+h}h${-w}Z`; + } + value() { + return this._ || null; + } +} + +class Polygon { + constructor() { + this._ = []; + } + moveTo(x, y) { + this._.push([x, y]); + } + closePath() { + this._.push(this._[0].slice()); + } + lineTo(x, y) { + this._.push([x, y]); + } + value() { + return this._.length ? this._ : null; + } +} + +export class Voronoi

    implements VoronoiI

    { + xmin: number; + ymin: number; + xmax: number; + ymax: number; + /** + * The Voronoi diagram’s associated Delaunay triangulation. + */ + delaunay: DelaunayI

    ; + + /** + * The circumcenters of the Delaunay triangles [cx0, cy0, cx1, cy1, ...]. + * Each contiguous pair of coordinates cx, cy is the circumcenter for the corresponding triangle. + * These circumcenters form the coordinates of the Voronoi cell polygons. + */ + circumcenters: Float64Array; + + /** + * An array [vx0, vy0, wx0, wy0, ...] where each non-zero quadruple describes an open (infinite) cell + * on the outer hull, giving the directions of two open half-lines. + */ + vectors: Float64Array; + + constructor(delaunay: DelaunayI

    , [xmin, ymin, xmax, ymax]: Bounds = [0, 0, 960, 500]) { + if (!((xmax = +xmax) >= (xmin = +xmin)) || !((ymax = +ymax) >= (ymin = +ymin))) throw new Error('invalid bounds'); + this.delaunay = delaunay; + this._circumcenters = new Float64Array(delaunay.points.length * 2); + this.vectors = new Float64Array(delaunay.points.length * 2); + (this.xmax = xmax), (this.xmin = xmin); + (this.ymax = ymax), (this.ymin = ymin); + this._init(); + } + update() { + this.delaunay.update(); + this._init(); + return this; + } + _init() { + const { + delaunay: { points, hull, triangles }, + vectors, + } = this; + + // Compute circumcenters. + const circumcenters = (this.circumcenters = this._circumcenters.subarray(0, (triangles.length / 3) * 2)); + for (let i = 0, j = 0, n = triangles.length, x, y; i < n; i += 3, j += 2) { + const t1 = triangles[i] * 2; + const t2 = triangles[i + 1] * 2; + const t3 = triangles[i + 2] * 2; + const x1 = points[t1]; + const y1 = points[t1 + 1]; + const x2 = points[t2]; + const y2 = points[t2 + 1]; + const x3 = points[t3]; + const y3 = points[t3 + 1]; + + const dx = x2 - x1; + const dy = y2 - y1; + const ex = x3 - x1; + const ey = y3 - y1; + const bl = dx * dx + dy * dy; + const cl = ex * ex + ey * ey; + const ab = (dx * ey - dy * ex) * 2; + + if (!ab) { + // degenerate case (collinear diagram) + x = (x1 + x3) / 2 - 1e8 * ey; + y = (y1 + y3) / 2 + 1e8 * ex; + } else if (Math.abs(ab) < 1e-8) { + // almost equal points (degenerate triangle) + x = (x1 + x3) / 2; + y = (y1 + y3) / 2; + } else { + const d = 1 / ab; + x = x1 + (ey * bl - dy * cl) * d; + y = y1 + (dx * cl - ex * bl) * d; + } + circumcenters[j] = x; + circumcenters[j + 1] = y; + } + + // Compute exterior cell rays. + let h = hull[hull.length - 1]; + let p0, + p1 = h * 4; + let x0, + x1 = points[2 * h]; + let y0, + y1 = points[2 * h + 1]; + vectors.fill(0); + for (let i = 0; i < hull.length; ++i) { + h = hull[i]; + (p0 = p1), (x0 = x1), (y0 = y1); + (p1 = h * 4), (x1 = points[2 * h]), (y1 = points[2 * h + 1]); + vectors[p0 + 2] = vectors[p1] = y0 - y1; + vectors[p0 + 3] = vectors[p1 + 1] = x1 - x0; + } + } + /** + * Renders the mesh of Voronoi cells to the specified context. + * The specified context must implement the context.moveTo and context.lineTo methods from the CanvasPathMethods API. + */ + render(context: MoveContext & LineContext): void { + const buffer = context == null ? (context = new Path()) : undefined; + const { + delaunay: { halfedges, inedges, hull }, + circumcenters, + vectors, + } = this; + if (hull.length <= 1) return null; + for (let i = 0, n = halfedges.length; i < n; ++i) { + const j = halfedges[i]; + if (j < i) continue; + const ti = Math.floor(i / 3) * 2; + const tj = Math.floor(j / 3) * 2; + const xi = circumcenters[ti]; + const yi = circumcenters[ti + 1]; + const xj = circumcenters[tj]; + const yj = circumcenters[tj + 1]; + this._renderSegment(xi, yi, xj, yj, context); + } + let h0, + h1 = hull[hull.length - 1]; + for (let i = 0; i < hull.length; ++i) { + (h0 = h1), (h1 = hull[i]); + const t = Math.floor(inedges[h1] / 3) * 2; + const x = circumcenters[t]; + const y = circumcenters[t + 1]; + const v = h0 * 4; + const p = this._project(x, y, vectors[v + 2], vectors[v + 3]); + if (p) this._renderSegment(x, y, p[0], p[1], context); + } + return buffer && buffer.value(); + } + /** + * Renders the viewport extent to the specified context. + * The specified context must implement the context.rect method from the CanvasPathMethods API. + * Equivalent to context.rect(voronoi.xmin, voronoi.ymin, voronoi.xmax - voronoi.xmin, voronoi.ymax - voronoi.ymin). + */ + renderBounds(context: RectContext): void { + const buffer = context == null ? (context = new Path()) : undefined; + context.rect(this.xmin, this.ymin, this.xmax - this.xmin, this.ymax - this.ymin); + return buffer && buffer.value(); + } + + /** + * Renders the cell with the specified index i to the specified context. + * The specified context must implement the context.moveTo, context.lineTo, and context.closePath methods from the CanvasPathMethods API. + */ + renderCell(i: number, context: MoveContext & LineContext & ClosableContext): void { + const buffer = context == null ? (context = new Path()) : undefined; + const points = this._clip(i); + if (points === null) return; + context.moveTo(points[0], points[1]); + let n = points.length; + while (points[0] === points[n - 2] && points[1] === points[n - 1] && n > 1) n -= 2; + for (let i = 2; i < n; i += 2) { + if (points[i] !== points[i - 2] || points[i + 1] !== points[i - 1]) context.lineTo(points[i], points[i + 1]); + } + context.closePath(); + return buffer && buffer.value(); + } + *cellPolygons() { + const { + delaunay: { points }, + } = this; + for (let i = 0, n = points.length / 2; i < n; ++i) { + const cell = this.cellPolygon(i); + if (cell) yield cell; + } + } + cellPolygon(i) { + const polygon = new Polygon(); + this.renderCell(i, polygon); + return polygon.value(); + } + _renderSegment(x0, y0, x1, y1, context) { + let S; + const c0 = this._regioncode(x0, y0); + const c1 = this._regioncode(x1, y1); + if (c0 === 0 && c1 === 0) { + context.moveTo(x0, y0); + context.lineTo(x1, y1); + } else if ((S = this._clipSegment(x0, y0, x1, y1, c0, c1))) { + context.moveTo(S[0], S[1]); + context.lineTo(S[2], S[3]); + } + } + contains(i, x, y) { + if (((x = +x), x !== x) || ((y = +y), y !== y)) return false; + return this.delaunay._step(i, x, y) === i; + } + *neighbors(i) { + const ci = this._clip(i); + if (ci) + for (const j of this.delaunay.neighbors(i)) { + const cj = this._clip(j); + // find the common edge + if (cj) + loop: for (let ai = 0, li = ci.length; ai < li; ai += 2) { + for (let aj = 0, lj = cj.length; aj < lj; aj += 2) { + if ( + ci[ai] == cj[aj] && + ci[ai + 1] == cj[aj + 1] && + ci[(ai + 2) % li] == cj[(aj + lj - 2) % lj] && + ci[(ai + 3) % li] == cj[(aj + lj - 1) % lj] + ) { + yield j; + break loop; + } + } + } + } + } + _cell(i) { + const { + circumcenters, + delaunay: { inedges, halfedges, triangles }, + } = this; + const e0 = inedges[i]; + if (e0 === -1) return null; // coincident point + const points = []; + let e = e0; + do { + const t = Math.floor(e / 3); + points.push(circumcenters[t * 2], circumcenters[t * 2 + 1]); + e = e % 3 === 2 ? e - 2 : e + 1; + if (triangles[e] !== i) break; // bad triangulation + e = halfedges[e]; + } while (e !== e0 && e !== -1); + return points; + } + _clip(i) { + // degenerate case (1 valid point: return the box) + if (i === 0 && this.delaunay.hull.length === 1) { + return [this.xmax, this.ymin, this.xmax, this.ymax, this.xmin, this.ymax, this.xmin, this.ymin]; + } + const points = this._cell(i); + if (points === null) return null; + const { vectors: V } = this; + const v = i * 4; + return V[v] || V[v + 1] + ? this._clipInfinite(i, points, V[v], V[v + 1], V[v + 2], V[v + 3]) + : this._clipFinite(i, points); + } + _clipFinite(i, points) { + const n = points.length; + let P = null; + let x0, + y0, + x1 = points[n - 2], + y1 = points[n - 1]; + let c0, + c1 = this._regioncode(x1, y1); + let e0, e1; + for (let j = 0; j < n; j += 2) { + (x0 = x1), (y0 = y1), (x1 = points[j]), (y1 = points[j + 1]); + (c0 = c1), (c1 = this._regioncode(x1, y1)); + if (c0 === 0 && c1 === 0) { + (e0 = e1), (e1 = 0); + if (P) P.push(x1, y1); + else P = [x1, y1]; + } else { + let S, sx0, sy0, sx1, sy1; + if (c0 === 0) { + if ((S = this._clipSegment(x0, y0, x1, y1, c0, c1)) === null) continue; + [sx0, sy0, sx1, sy1] = S; + } else { + if ((S = this._clipSegment(x1, y1, x0, y0, c1, c0)) === null) continue; + [sx1, sy1, sx0, sy0] = S; + (e0 = e1), (e1 = this._edgecode(sx0, sy0)); + if (e0 && e1) this._edge(i, e0, e1, P, P.length); + if (P) P.push(sx0, sy0); + else P = [sx0, sy0]; + } + (e0 = e1), (e1 = this._edgecode(sx1, sy1)); + if (e0 && e1) this._edge(i, e0, e1, P, P.length); + if (P) P.push(sx1, sy1); + else P = [sx1, sy1]; + } + } + if (P) { + (e0 = e1), (e1 = this._edgecode(P[0], P[1])); + if (e0 && e1) this._edge(i, e0, e1, P, P.length); + } else if (this.contains(i, (this.xmin + this.xmax) / 2, (this.ymin + this.ymax) / 2)) { + return [this.xmax, this.ymin, this.xmax, this.ymax, this.xmin, this.ymax, this.xmin, this.ymin]; + } + return P; + } + _clipSegment(x0, y0, x1, y1, c0, c1) { + while (true) { + if (c0 === 0 && c1 === 0) return [x0, y0, x1, y1]; + if (c0 & c1) return null; + let x, + y, + c = c0 || c1; + if (c & 0b1000) (x = x0 + ((x1 - x0) * (this.ymax - y0)) / (y1 - y0)), (y = this.ymax); + else if (c & 0b0100) (x = x0 + ((x1 - x0) * (this.ymin - y0)) / (y1 - y0)), (y = this.ymin); + else if (c & 0b0010) (y = y0 + ((y1 - y0) * (this.xmax - x0)) / (x1 - x0)), (x = this.xmax); + else (y = y0 + ((y1 - y0) * (this.xmin - x0)) / (x1 - x0)), (x = this.xmin); + if (c0) (x0 = x), (y0 = y), (c0 = this._regioncode(x0, y0)); + else (x1 = x), (y1 = y), (c1 = this._regioncode(x1, y1)); + } + } + _clipInfinite(i, points, vx0, vy0, vxn, vyn) { + let P = Array.from(points), + p; + if ((p = this._project(P[0], P[1], vx0, vy0))) P.unshift(p[0], p[1]); + if ((p = this._project(P[P.length - 2], P[P.length - 1], vxn, vyn))) P.push(p[0], p[1]); + if ((P = this._clipFinite(i, P))) { + for (let j = 0, n = P.length, c0, c1 = this._edgecode(P[n - 2], P[n - 1]); j < n; j += 2) { + (c0 = c1), (c1 = this._edgecode(P[j], P[j + 1])); + if (c0 && c1) (j = this._edge(i, c0, c1, P, j)), (n = P.length); + } + } else if (this.contains(i, (this.xmin + this.xmax) / 2, (this.ymin + this.ymax) / 2)) { + P = [this.xmin, this.ymin, this.xmax, this.ymin, this.xmax, this.ymax, this.xmin, this.ymax]; + } + return P; + } + _edge(i, e0, e1, P, j) { + while (e0 !== e1) { + let x, y; + switch (e0) { + case 0b0101: + e0 = 0b0100; + continue; // top-left + case 0b0100: + (e0 = 0b0110), (x = this.xmax), (y = this.ymin); + break; // top + case 0b0110: + e0 = 0b0010; + continue; // top-right + case 0b0010: + (e0 = 0b1010), (x = this.xmax), (y = this.ymax); + break; // right + case 0b1010: + e0 = 0b1000; + continue; // bottom-right + case 0b1000: + (e0 = 0b1001), (x = this.xmin), (y = this.ymax); + break; // bottom + case 0b1001: + e0 = 0b0001; + continue; // bottom-left + case 0b0001: + (e0 = 0b0101), (x = this.xmin), (y = this.ymin); + break; // left + } + if ((P[j] !== x || P[j + 1] !== y) && this.contains(i, x, y)) { + P.splice(j, 0, x, y), (j += 2); + } + } + if (P.length > 4) { + for (let i = 0; i < P.length; i += 2) { + const j = (i + 2) % P.length, + k = (i + 4) % P.length; + if ((P[i] === P[j] && P[j] === P[k]) || (P[i + 1] === P[j + 1] && P[j + 1] === P[k + 1])) + P.splice(j, 2), (i -= 2); + } + } + return j; + } + _project(x0, y0, vx, vy) { + let t = Infinity, + c, + x, + y; + if (vy < 0) { + // top + if (y0 <= this.ymin) return null; + if ((c = (this.ymin - y0) / vy) < t) (y = this.ymin), (x = x0 + (t = c) * vx); + } else if (vy > 0) { + // bottom + if (y0 >= this.ymax) return null; + if ((c = (this.ymax - y0) / vy) < t) (y = this.ymax), (x = x0 + (t = c) * vx); + } + if (vx > 0) { + // right + if (x0 >= this.xmax) return null; + if ((c = (this.xmax - x0) / vx) < t) (x = this.xmax), (y = y0 + (t = c) * vy); + } else if (vx < 0) { + // left + if (x0 <= this.xmin) return null; + if ((c = (this.xmin - x0) / vx) < t) (x = this.xmin), (y = y0 + (t = c) * vy); + } + return [x, y]; + } + _edgecode(x, y) { + return ( + (x === this.xmin ? 0b0001 : x === this.xmax ? 0b0010 : 0b0000) | + (y === this.ymin ? 0b0100 : y === this.ymax ? 0b1000 : 0b0000) + ); + } + _regioncode(x, y) { + return ( + (x < this.xmin ? 0b0001 : x > this.xmax ? 0b0010 : 0b0000) | + (y < this.ymin ? 0b0100 : y > this.ymax ? 0b1000 : 0b0000) + ); + } +} + +const tau = 2 * Math.PI; + +function pointX(p) { + return p[0]; +} + +function pointY(p) { + return p[1]; +} + +// A triangulation is collinear if all its triangles have a non-null area +function collinear(d) { + const { triangles, coords } = d; + for (let i = 0; i < triangles.length; i += 3) { + const a = 2 * triangles[i], + b = 2 * triangles[i + 1], + c = 2 * triangles[i + 2], + cross = + (coords[c] - coords[a]) * (coords[b + 1] - coords[a + 1]) - + (coords[b] - coords[a]) * (coords[c + 1] - coords[a + 1]); + if (cross > 1e-10) return false; + } + return true; +} + +function jitter(x, y, r) { + return [x + Math.sin(x + y) * r, y + Math.cos(x - y) * r]; +} + +export class Delaunay

    implements DelaunayI

    { + /** + * The coordinates of the points as an array [x0, y0, x1, y1, ...]. + * Typically, this is a Float64Array, however you can use any array-like type in the constructor. + */ + points: ArrayLike; + + /** + * The halfedge indices as an Int32Array [j0, j1, ...]. + * For each index 0 <= i < halfedges.length, there is a halfedge from triangle vertex j = halfedges[i] to triangle vertex i. + */ + halfedges: Int32Array; + + /** + * An arbitrary node on the convex hull. + * The convex hull is represented as a circular doubly-linked list of nodes. + */ + hull: Node; + + /** + * The triangle vertex indices as an Uint32Array [i0, j0, k0, i1, j1, k1, ...]. + * Each contiguous triplet of indices i, j, k forms a counterclockwise triangle. + * The coordinates of the triangle's points can be found by going through 'points'. + */ + triangles: Uint32Array; + + /** + * The incoming halfedge indexes as a Int32Array [e0, e1, e2, ...]. + * For each point i, inedges[i] is the halfedge index e of an incoming halfedge. + * For coincident points, the halfedge index is -1; for points on the convex hull, the incoming halfedge is on the convex hull; for other points, the choice of incoming halfedge is arbitrary. + */ + inedges: Int32Array; + + /** + * The outgoing halfedge indexes as a Int32Array [e0, e1, e2, ...]. + * For each point i on the convex hull, outedges[i] is the halfedge index e of the corresponding outgoing halfedge; for other points, the halfedge index is -1. + */ + outedges: Int32Array; + /** + * Returns the Delaunay triangulation for the given array or iterable of points. + * Otherwise, the getX and getY functions are invoked for each point in order, and must return the respective x- and y-coordinate for each point. + * If that is specified, the functions getX and getY are invoked with that as this. + * (See Array.from for reference.) + */ + static from

    ( + points: ArrayLike

    | Iterable

    , + fx: GetCoordinate | Iterable

    > = pointX, + fy: GetCoordinate | Iterable

    > = pointY, + that?: any, + ): Delaunay

    { + return new Delaunay( + 'length' in points ? flatArray(points, fx, fy, that) : Float64Array.from(flatIterable(points, fx, fy, that)), + ); + } + /** + * Returns the Delaunay triangulation for the given flat array [x0, y0, x1, y1, …] of points. + */ + constructor(points: ArrayLike) { + this._delaunator = new Delaunator(points); + this.inedges = new Int32Array(points.length / 2); + this._hullIndex = new Int32Array(points.length / 2); + this.points = this._delaunator.coords; + this._init(); + } + update() { + this._delaunator.update(); + this._init(); + return this; + } + _init() { + const d = this._delaunator, + points = this.points; + + // check for collinear + if (d.hull && d.hull.length > 2 && collinear(d)) { + this.collinear = Int32Array.from({ length: points.length / 2 }, (_, i) => i).sort( + (i, j) => points[2 * i] - points[2 * j] || points[2 * i + 1] - points[2 * j + 1], + ); // for exact neighbors + const e = this.collinear[0], + f = this.collinear[this.collinear.length - 1], + bounds = [points[2 * e], points[2 * e + 1], points[2 * f], points[2 * f + 1]], + r = 1e-8 * Math.sqrt((bounds[3] - bounds[1]) ** 2 + (bounds[2] - bounds[0]) ** 2); + for (let i = 0, n = points.length / 2; i < n; ++i) { + const p = jitter(points[2 * i], points[2 * i + 1], r); + points[2 * i] = p[0]; + points[2 * i + 1] = p[1]; + } + this._delaunator = new Delaunator(points); + } else { + delete this.collinear; + } + + const halfedges = (this.halfedges = this._delaunator.halfedges); + const hull = (this.hull = this._delaunator.hull); + const triangles = (this.triangles = this._delaunator.triangles); + const inedges = this.inedges.fill(-1); + const hullIndex = this._hullIndex.fill(-1); + + // Compute an index from each point to an (arbitrary) incoming halfedge + // Used to give the first neighbor of each point; for this reason, + // on the hull we give priority to exterior halfedges + for (let e = 0, n = halfedges.length; e < n; ++e) { + const p = triangles[e % 3 === 2 ? e - 2 : e + 1]; + if (halfedges[e] === -1 || inedges[p] === -1) inedges[p] = e; + } + for (let i = 0, n = hull.length; i < n; ++i) { + hullIndex[hull[i]] = i; + } + + // degenerate case: 1 or 2 (distinct) points + if (hull.length <= 2 && hull.length > 0) { + this.triangles = new Int32Array(3).fill(-1); + this.halfedges = new Int32Array(3).fill(-1); + this.triangles[0] = hull[0]; + this.triangles[1] = hull[1]; + this.triangles[2] = hull[1]; + inedges[hull[0]] = 1; + if (hull.length === 2) inedges[hull[1]] = 0; + } + } + voronoi(bounds) { + return new Voronoi(this, bounds); + } + *neighbors(i) { + const { inedges, hull, _hullIndex, halfedges, triangles, collinear } = this; + + // degenerate case with several collinear points + if (collinear) { + const l = collinear.indexOf(i); + if (l > 0) yield collinear[l - 1]; + if (l < collinear.length - 1) yield collinear[l + 1]; + return; + } + + const e0 = inedges[i]; + if (e0 === -1) return; // coincident point + let e = e0, + p0 = -1; + do { + yield (p0 = triangles[e]); + e = e % 3 === 2 ? e - 2 : e + 1; + if (triangles[e] !== i) return; // bad triangulation + e = halfedges[e]; + if (e === -1) { + const p = hull[(_hullIndex[i] + 1) % hull.length]; + if (p !== p0) yield p; + return; + } + } while (e !== e0); + } + find(x, y, i = 0) { + if (((x = +x), x !== x) || ((y = +y), y !== y)) return -1; + const i0 = i; + let c; + while ((c = this._step(i, x, y)) >= 0 && c !== i && c !== i0) i = c; + return c; + } + _step(i, x, y) { + const { inedges, hull, _hullIndex, halfedges, triangles, points } = this; + if (inedges[i] === -1 || !points.length) return (i + 1) % (points.length >> 1); + let c = i; + let dc = (x - points[i * 2]) ** 2 + (y - points[i * 2 + 1]) ** 2; + const e0 = inedges[i]; + let e = e0; + do { + let t = triangles[e]; + const dt = (x - points[t * 2]) ** 2 + (y - points[t * 2 + 1]) ** 2; + if (dt < dc) (dc = dt), (c = t); + e = e % 3 === 2 ? e - 2 : e + 1; + if (triangles[e] !== i) break; // bad triangulation + e = halfedges[e]; + if (e === -1) { + e = hull[(_hullIndex[i] + 1) % hull.length]; + if (e !== t) { + if ((x - points[e * 2]) ** 2 + (y - points[e * 2 + 1]) ** 2 < dc) return e; + } + break; + } + } while (e !== e0); + return c; + } + /** + * Renders the edges of the Delaunay triangulation to the specified context. + * The specified context must implement the context.moveTo and context.lineTo methods from the CanvasPathMethods API. + */ + render(context: MoveContext & LineContext): void { + const buffer = context == null ? (context = new Path()) : undefined; + const { points, halfedges, triangles } = this; + for (let i = 0, n = halfedges.length; i < n; ++i) { + const j = halfedges[i]; + if (j < i) continue; + const ti = triangles[i] * 2; + const tj = triangles[j] * 2; + context.moveTo(points[ti], points[ti + 1]); + context.lineTo(points[tj], points[tj + 1]); + } + this.renderHull(context); + return buffer && buffer.value(); + } + /** + * Renders the input points of the Delaunay triangulation to the specified context as circles with the specified radius. + * If radius is not specified, it defaults to 2. + * The specified context must implement the context.moveTo and context.arc methods from the CanvasPathMethods API. + */ + renderPoints(context: MoveContext & ArcContext, r?: number): void { + const buffer = context == null ? (context = new Path()) : undefined; + const { points } = this; + for (let i = 0, n = points.length; i < n; i += 2) { + const x = points[i], + y = points[i + 1]; + context.moveTo(x + r, y); + context.arc(x, y, r, 0, tau); + } + return buffer && buffer.value(); + } + /** + * Renders the convex hull of the Delaunay triangulation to the specified context. + * The specified context must implement the context.moveTo and context.lineTo methods from the CanvasPathMethods API. + */ + renderHull(context: MoveContext & LineContext): void { + const buffer = context == null ? (context = new Path()) : undefined; + const { hull, points } = this; + const h = hull[0] * 2, + n = hull.length; + context.moveTo(points[h], points[h + 1]); + for (let i = 1; i < n; ++i) { + const h = 2 * hull[i]; + context.lineTo(points[h], points[h + 1]); + } + context.closePath(); + return buffer && buffer.value(); + } + hullPolygon() { + const polygon = new Polygon(); + this.renderHull(polygon); + return polygon.value(); + } + /** + * Renders triangle i of the Delaunay triangulation to the specified context. + * The specified context must implement the context.moveTo, context.lineTo and context.closePath methods from the CanvasPathMethods API. + */ + renderTriangle(i: number, context: MoveContext & LineContext & ClosableContext): void { + const buffer = context == null ? (context = new Path()) : undefined; + const { points, triangles } = this; + const t0 = triangles[(i *= 3)] * 2; + const t1 = triangles[i + 1] * 2; + const t2 = triangles[i + 2] * 2; + context.moveTo(points[t0], points[t0 + 1]); + context.lineTo(points[t1], points[t1 + 1]); + context.lineTo(points[t2], points[t2 + 1]); + context.closePath(); + return buffer && buffer.value(); + } + *trianglePolygons() { + const { triangles } = this; + for (let i = 0, n = triangles.length / 3; i < n; ++i) { + yield this.trianglePolygon(i); + } + } + trianglePolygon(i) { + const polygon = new Polygon(); + this.renderTriangle(i, polygon); + return polygon.value(); + } +} + +function flatArray(points, fx, fy, that) { + const n = points.length; + const array = new Float64Array(n * 2); + for (let i = 0; i < n; ++i) { + const p = points[i]; + array[i * 2] = fx.call(that, p, i, points); + array[i * 2 + 1] = fy.call(that, p, i, points); + } + return array; +} + +function* flatIterable(points, fx, fy, that) { + let i = 0; + for (const p of points) { + yield fx.call(that, p, i, points); + yield fy.call(that, p, i, points); + ++i; + } +} diff --git a/packages/osd-charts/src/utils/data/date_time.ts b/packages/osd-charts/src/utils/data/date_time.ts new file mode 100644 index 000000000000..f126095c51c6 --- /dev/null +++ b/packages/osd-charts/src/utils/data/date_time.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment-timezone'; + +/** @internal */ +export function getMomentWithTz(date: number | Date, timeZone?: string) { + if (timeZone === 'local' || !timeZone) { + return moment(date); + } + if (timeZone.toLowerCase().startsWith('utc+') || timeZone.toLowerCase().startsWith('utc-')) { + return moment(date).utcOffset(Number(timeZone.slice(3))); + } + return moment.tz(date, timeZone); +} diff --git a/packages/osd-charts/src/utils/data/formatters.ts b/packages/osd-charts/src/utils/data/formatters.ts new file mode 100644 index 000000000000..be360a9f441a --- /dev/null +++ b/packages/osd-charts/src/utils/data/formatters.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment-timezone'; + +import { TickFormatter, TickFormatterOptions } from '../../chart_types/xy_chart/utils/specs'; +import { getMomentWithTz } from './date_time'; + +/** @public */ +export function timeFormatter(format: string): TickFormatter { + return (value: number, options?: TickFormatterOptions): string => + getMomentWithTz(value, options && options.timeZone).format(format); +} + +/** @public */ +export function niceTimeFormatter(domain: [number, number]): TickFormatter { + const minDate = moment(domain[0]); + const maxDate = moment(domain[1]); + const diff = maxDate.diff(minDate, 'days'); + const format = niceTimeFormatByDay(diff); + return timeFormatter(format); +} + +/** @public */ +export function niceTimeFormatByDay(days: number) { + if (days > 30) { + return 'YYYY-MM-DD'; + } + if (days > 7 && days <= 30) { + return 'MMMM DD'; + } + if (days > 1 && days <= 7) { + return 'MM-DD HH:mm'; + } + return 'HH:mm:ss'; +} diff --git a/packages/osd-charts/src/utils/data/formatters.tz.test.ts b/packages/osd-charts/src/utils/data/formatters.tz.test.ts new file mode 100644 index 000000000000..c6c4361074e9 --- /dev/null +++ b/packages/osd-charts/src/utils/data/formatters.tz.test.ts @@ -0,0 +1,139 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { niceTimeFormatter } from './formatters'; + +const anHour = 1000 * 60 * 60; +const aDay = anHour * 24; +const END_DAY = 1546387199000; // 2019-01-01T23:59:99.000Z +const START_DAY = 1546300800000; // 2019-01-01T00:00:00.000Z +const TIMEZONE_UTC = { timeZone: 'UTC' }; +const TIMEZONE_NY = { timeZone: 'America/New_York' }; +const TIMEZONE_TOKYO = { timeZone: 'Asia/Tokyo' }; + +describe('Date Formatter', () => { + describe('format nicely an interval of more than 30 days', () => { + const formatter = niceTimeFormatter([START_DAY, START_DAY + aDay * 31]); + test('UTC', () => { + expect(formatter(START_DAY, TIMEZONE_UTC)).toBe('2019-01-01'); + expect(formatter(START_DAY + aDay, TIMEZONE_UTC)).toBe('2019-01-02'); + expect(formatter(START_DAY + aDay * 2, TIMEZONE_UTC)).toBe('2019-01-03'); + expect(formatter(END_DAY, TIMEZONE_UTC)).toBe('2019-01-01'); + expect(formatter(END_DAY + aDay, TIMEZONE_UTC)).toBe('2019-01-02'); + expect(formatter(END_DAY + aDay * 2, TIMEZONE_UTC)).toBe('2019-01-03'); + }); + test('America/New_York', () => { + expect(formatter(START_DAY, TIMEZONE_NY)).toBe('2018-12-31'); + expect(formatter(START_DAY + aDay, TIMEZONE_NY)).toBe('2019-01-01'); + expect(formatter(START_DAY + aDay * 2, TIMEZONE_NY)).toBe('2019-01-02'); + expect(formatter(END_DAY, TIMEZONE_NY)).toBe('2019-01-01'); + expect(formatter(END_DAY + aDay, TIMEZONE_NY)).toBe('2019-01-02'); + expect(formatter(END_DAY + aDay * 2, TIMEZONE_NY)).toBe('2019-01-03'); + }); + test('Asia/Tokyo', () => { + expect(formatter(START_DAY, TIMEZONE_TOKYO)).toBe('2019-01-01'); + expect(formatter(START_DAY + aDay, TIMEZONE_TOKYO)).toBe('2019-01-02'); + expect(formatter(START_DAY + aDay * 2, TIMEZONE_TOKYO)).toBe('2019-01-03'); + expect(formatter(END_DAY, TIMEZONE_TOKYO)).toBe('2019-01-02'); + expect(formatter(END_DAY + aDay, TIMEZONE_TOKYO)).toBe('2019-01-03'); + expect(formatter(END_DAY + aDay * 2, TIMEZONE_TOKYO)).toBe('2019-01-04'); + }); + }); + describe('format nicely an interval of between 7 and 30 days', () => { + const formatter = niceTimeFormatter([START_DAY, START_DAY + aDay * 10]); + test('UTC', () => { + expect(formatter(START_DAY, TIMEZONE_UTC)).toBe('January 01'); + expect(formatter(START_DAY + aDay, TIMEZONE_UTC)).toBe('January 02'); + expect(formatter(START_DAY + aDay * 2, TIMEZONE_UTC)).toBe('January 03'); + expect(formatter(END_DAY, TIMEZONE_UTC)).toBe('January 01'); + expect(formatter(END_DAY + aDay, TIMEZONE_UTC)).toBe('January 02'); + expect(formatter(END_DAY + aDay * 2, TIMEZONE_UTC)).toBe('January 03'); + }); + test('America/New_York', () => { + expect(formatter(START_DAY, TIMEZONE_NY)).toBe('December 31'); + expect(formatter(START_DAY + aDay, TIMEZONE_NY)).toBe('January 01'); + expect(formatter(START_DAY + aDay * 2, TIMEZONE_NY)).toBe('January 02'); + expect(formatter(END_DAY, TIMEZONE_NY)).toBe('January 01'); + expect(formatter(END_DAY + aDay, TIMEZONE_NY)).toBe('January 02'); + expect(formatter(END_DAY + aDay * 2, TIMEZONE_NY)).toBe('January 03'); + }); + test('Asia/Tokyo', () => { + expect(formatter(START_DAY, TIMEZONE_TOKYO)).toBe('January 01'); + expect(formatter(START_DAY + aDay, TIMEZONE_TOKYO)).toBe('January 02'); + expect(formatter(START_DAY + aDay * 2, TIMEZONE_TOKYO)).toBe('January 03'); + expect(formatter(END_DAY, TIMEZONE_TOKYO)).toBe('January 02'); + expect(formatter(END_DAY + aDay, TIMEZONE_TOKYO)).toBe('January 03'); + expect(formatter(END_DAY + aDay * 2, TIMEZONE_TOKYO)).toBe('January 04'); + }); + }); + describe('format nicely an interval of between 1 and 7 days', () => { + const formatter = niceTimeFormatter([START_DAY, START_DAY + aDay * 6]); + test('UTC', () => { + expect(formatter(START_DAY, TIMEZONE_UTC)).toBe('01-01 00:00'); + expect(formatter(START_DAY + aDay, TIMEZONE_UTC)).toBe('01-02 00:00'); + expect(formatter(START_DAY + aDay * 2, TIMEZONE_UTC)).toBe('01-03 00:00'); + expect(formatter(END_DAY, TIMEZONE_UTC)).toBe('01-01 23:59'); + expect(formatter(END_DAY + aDay, TIMEZONE_UTC)).toBe('01-02 23:59'); + expect(formatter(END_DAY + aDay * 2, TIMEZONE_UTC)).toBe('01-03 23:59'); + }); + test('America/New_York', () => { + expect(formatter(START_DAY, TIMEZONE_NY)).toBe('12-31 19:00'); + expect(formatter(START_DAY + aDay, TIMEZONE_NY)).toBe('01-01 19:00'); + expect(formatter(START_DAY + aDay * 2, TIMEZONE_NY)).toBe('01-02 19:00'); + expect(formatter(END_DAY, TIMEZONE_NY)).toBe('01-01 18:59'); + expect(formatter(END_DAY + aDay, TIMEZONE_NY)).toBe('01-02 18:59'); + expect(formatter(END_DAY + aDay * 2, TIMEZONE_NY)).toBe('01-03 18:59'); + }); + test('Asia/Tokyo', () => { + expect(formatter(START_DAY, TIMEZONE_TOKYO)).toBe('01-01 09:00'); + expect(formatter(START_DAY + aDay, TIMEZONE_TOKYO)).toBe('01-02 09:00'); + expect(formatter(START_DAY + aDay * 2, TIMEZONE_TOKYO)).toBe('01-03 09:00'); + expect(formatter(END_DAY, TIMEZONE_TOKYO)).toBe('01-02 08:59'); + expect(formatter(END_DAY + aDay, TIMEZONE_TOKYO)).toBe('01-03 08:59'); + expect(formatter(END_DAY + aDay * 2, TIMEZONE_TOKYO)).toBe('01-04 08:59'); + }); + }); + describe('format nicely an interval LTE than 1 day', () => { + const formatter = niceTimeFormatter([START_DAY, START_DAY + aDay - 1]); + test('UTC', () => { + expect(formatter(START_DAY, TIMEZONE_UTC)).toBe('00:00:00'); + expect(formatter(START_DAY + anHour * 2.5, TIMEZONE_UTC)).toBe('02:30:00'); + expect(formatter(START_DAY + anHour * 10.5 + 1000, TIMEZONE_UTC)).toBe('10:30:01'); + expect(formatter(END_DAY, TIMEZONE_UTC)).toBe('23:59:59'); + expect(formatter(END_DAY + anHour * 2.5, TIMEZONE_UTC)).toBe('02:29:59'); + expect(formatter(END_DAY + anHour * 10.5 + 1000, TIMEZONE_UTC)).toBe('10:30:00'); + }); + test('America/New_York', () => { + expect(formatter(START_DAY, TIMEZONE_NY)).toBe('19:00:00'); + expect(formatter(START_DAY + anHour * 2.5, TIMEZONE_NY)).toBe('21:30:00'); + expect(formatter(START_DAY + anHour * 10.5 + 1000, TIMEZONE_NY)).toBe('05:30:01'); + expect(formatter(END_DAY, TIMEZONE_NY)).toBe('18:59:59'); + expect(formatter(END_DAY + anHour * 2.5, TIMEZONE_NY)).toBe('21:29:59'); + expect(formatter(END_DAY + anHour * 10.5 + 1000, TIMEZONE_NY)).toBe('05:30:00'); + }); + test('Asia/Tokyo', () => { + expect(formatter(START_DAY, TIMEZONE_TOKYO)).toBe('09:00:00'); + expect(formatter(START_DAY + anHour * 2.5, TIMEZONE_TOKYO)).toBe('11:30:00'); + expect(formatter(START_DAY + anHour * 10.5 + 1000, TIMEZONE_TOKYO)).toBe('19:30:01'); + expect(formatter(END_DAY, TIMEZONE_TOKYO)).toBe('08:59:59'); + expect(formatter(END_DAY + anHour * 2.5, TIMEZONE_TOKYO)).toBe('11:29:59'); + expect(formatter(END_DAY + anHour * 10.5 + 1000, TIMEZONE_TOKYO)).toBe('19:30:00'); + }); + }); +}); diff --git a/packages/osd-charts/src/utils/data/formatters.tz.test.utc.ts b/packages/osd-charts/src/utils/data/formatters.tz.test.utc.ts new file mode 100644 index 000000000000..cc33e0192fde --- /dev/null +++ b/packages/osd-charts/src/utils/data/formatters.tz.test.utc.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { niceTimeFormatter } from './formatters'; + +const anHour = 1000 * 60 * 60; +const aDay = anHour * 24; +const END_DAY = 1546387199000; // 2019-01-01T23:59:99.000Z +const START_DAY = 1546300800000; // 2019-01-01T00:00:00.000Z + +describe('Date Formatter, local CI time in UTC', () => { + test('format nicely an interval of more than 30 days', () => { + const formatter = niceTimeFormatter([START_DAY, START_DAY + aDay * 31]); + + expect(formatter(START_DAY)).toBe('2019-01-01'); + expect(formatter(START_DAY + aDay)).toBe('2019-01-02'); + expect(formatter(START_DAY + aDay * 2)).toBe('2019-01-03'); + expect(formatter(END_DAY)).toBe('2019-01-01'); + expect(formatter(END_DAY + aDay)).toBe('2019-01-02'); + expect(formatter(END_DAY + aDay * 2)).toBe('2019-01-03'); + }); + + describe('format nicely an interval of between 7 and 30 days', () => { + const formatter = niceTimeFormatter([START_DAY, START_DAY + aDay * 10]); + + expect(formatter(START_DAY)).toBe('January 01'); + expect(formatter(START_DAY + aDay)).toBe('January 02'); + expect(formatter(START_DAY + aDay * 2)).toBe('January 03'); + expect(formatter(END_DAY)).toBe('January 01'); + expect(formatter(END_DAY + aDay)).toBe('January 02'); + expect(formatter(END_DAY + aDay * 2)).toBe('January 03'); + }); + + describe('format nicely an interval of between 1 and 7 days', () => { + const formatter = niceTimeFormatter([START_DAY, START_DAY + aDay * 6]); + + expect(formatter(START_DAY)).toBe('01-01 00:00'); + expect(formatter(START_DAY + aDay)).toBe('01-02 00:00'); + expect(formatter(START_DAY + aDay * 2)).toBe('01-03 00:00'); + expect(formatter(END_DAY)).toBe('01-01 23:59'); + expect(formatter(END_DAY + aDay)).toBe('01-02 23:59'); + expect(formatter(END_DAY + aDay * 2)).toBe('01-03 23:59'); + }); + + describe('format nicely an interval LTE than 1 day', () => { + const formatter = niceTimeFormatter([START_DAY, START_DAY + aDay - 1]); + + expect(formatter(START_DAY)).toBe('00:00:00'); + expect(formatter(START_DAY + anHour * 2.5)).toBe('02:30:00'); + expect(formatter(START_DAY + anHour * 10.5 + 1000)).toBe('10:30:01'); + expect(formatter(END_DAY)).toBe('23:59:59'); + expect(formatter(END_DAY + anHour * 2.5)).toBe('02:29:59'); + expect(formatter(END_DAY + anHour * 10.5 + 1000)).toBe('10:30:00'); + }); +}); diff --git a/packages/osd-charts/src/utils/data_generators/data_generator.ts b/packages/osd-charts/src/utils/data_generators/data_generator.ts new file mode 100644 index 000000000000..a252c16228ef --- /dev/null +++ b/packages/osd-charts/src/utils/data_generators/data_generator.ts @@ -0,0 +1,96 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Simple1DNoise } from './simple_noise'; + +/** @public */ +export type RandomNumberGenerator = ( + min?: number, + max?: number, + fractionDigits?: number, + inclusive?: boolean, +) => number; + +function defaultRNG(min = 0, max = 1, fractionDigits = 0, inclusive = true) { + const precision = Math.pow(10, Math.max(fractionDigits, 0)); + const scaledMax = max * precision; + const scaledMin = min * precision; + const offset = inclusive ? 1 : 0; + const num = Math.floor(Math.random() * (scaledMax - scaledMin + offset)) + scaledMin; + + return num / precision; +} + +/** @public */ +export class DataGenerator { + private randomNumberGenerator: RandomNumberGenerator; + + private generator: Simple1DNoise; + + private frequency: number; + + constructor(frequency = 500, randomNumberGenerator: RandomNumberGenerator = defaultRNG) { + this.randomNumberGenerator = randomNumberGenerator; + this.generator = new Simple1DNoise(this.randomNumberGenerator); + this.frequency = frequency; + } + + generateBasicSeries(totalPoints = 50, offset = 0, amplitude = 1) { + const dataPoints = new Array(totalPoints).fill(0).map((_, i) => ({ + x: i, + y: (this.generator.getValue(i) + offset) * amplitude, + })); + return dataPoints; + } + + generateSimpleSeries(totalPoints = 50, groupIndex = 1, groupPrefix = '') { + const group = String.fromCharCode(97 + groupIndex); + const dataPoints = new Array(totalPoints).fill(0).map((_, i) => ({ + x: i, + y: 3 + Math.sin(i / this.frequency) + this.generator.getValue(i), + g: `${groupPrefix}${group}`, + })); + return dataPoints; + } + + generateGroupedSeries(totalPoints = 50, totalGroups = 2, groupPrefix = '') { + const groups = new Array(totalGroups) + .fill(0) + .map((group, i) => this.generateSimpleSeries(totalPoints, i, groupPrefix)); + return groups.reduce((acc, curr) => [...acc, ...curr]); + } + + generateRandomSeries(totalPoints = 50, groupIndex = 1, groupPrefix = '') { + const group = String.fromCharCode(97 + groupIndex); + const dataPoints = new Array(totalPoints).fill(0).map(() => ({ + x: this.randomNumberGenerator(0, 100), + y: this.randomNumberGenerator(0, 100), + z: this.randomNumberGenerator(0, 100), + g: `${groupPrefix}${group}`, + })); + return dataPoints; + } + + generateRandomGroupedSeries(totalPoints = 50, totalGroups = 2, groupPrefix = '') { + const groups = new Array(totalGroups) + .fill(0) + .map((group, i) => this.generateRandomSeries(totalPoints, i, groupPrefix)); + return groups.reduce((acc, curr) => [...acc, ...curr]); + } +} diff --git a/packages/osd-charts/src/utils/data_generators/simple_noise.ts b/packages/osd-charts/src/utils/data_generators/simple_noise.ts new file mode 100644 index 000000000000..139bb231c46b --- /dev/null +++ b/packages/osd-charts/src/utils/data_generators/simple_noise.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RandomNumberGenerator } from './data_generator'; + +/** @internal */ +export class Simple1DNoise { + private maxVertices: number; + + private maxVerticesMask: number; + + private amplitude: number; + + private scale: number; + + private getRandomNumber: RandomNumberGenerator; + + constructor(randomNumberGenerator: RandomNumberGenerator, maxVertices = 256, amplitude = 5.1, scale = 0.6) { + this.getRandomNumber = randomNumberGenerator; + this.maxVerticesMask = maxVertices - 1; + this.amplitude = amplitude; + this.scale = scale; + this.maxVertices = maxVertices; + } + + getValue(x: number) { + const r = new Array(this.maxVertices).fill(0).map(() => this.getRandomNumber(0, 1, 5, true)); + + const scaledX = x * this.scale; + const xFloor = Math.floor(scaledX); + const t = scaledX - xFloor; + const tRemapSmoothstep = t * t * (3 - 2 * t); + + // tslint:disable-next-line:no-bitwise + const xMin = xFloor & this.maxVerticesMask; + // tslint:disable-next-line:no-bitwise + const xMax = (xMin + 1) & this.maxVerticesMask; + + const y = this.lerp(r[xMin], r[xMax], tRemapSmoothstep); + + return y * this.amplitude; + } + + private lerp(a: number, b: number, t: number) { + return a * (1 - t) + b * t; + } +} diff --git a/packages/osd-charts/src/utils/data_samples/4_time_series.json b/packages/osd-charts/src/utils/data_samples/4_time_series.json new file mode 100644 index 000000000000..130e3f3b0db2 --- /dev/null +++ b/packages/osd-charts/src/utils/data_samples/4_time_series.json @@ -0,0 +1,4694 @@ +{ + "1 passenger ": { + "doc_count": 975811, + "buckets": [ + { + "key_as_string": "2020-03-01T23:00:00.000+01:00", + "key": 1583100000000, + "doc_count": 129, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1771 + } + }, + { + "key_as_string": "2020-03-02T00:00:00.000+01:00", + "key": 1583103600000, + "doc_count": 2809, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 47088.53000118211 + } + }, + { + "key_as_string": "2020-03-02T01:00:00.000+01:00", + "key": 1583107200000, + "doc_count": 1709, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 30240.020005226135 + } + }, + { + "key_as_string": "2020-03-02T02:00:00.000+01:00", + "key": 1583110800000, + "doc_count": 1014, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 15785.70000076294 + } + }, + { + "key_as_string": "2020-03-02T03:00:00.000+01:00", + "key": 1583114400000, + "doc_count": 599, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8388.909999618307 + } + }, + { + "key_as_string": "2020-03-02T04:00:00.000+01:00", + "key": 1583118000000, + "doc_count": 486, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7206.700000762939 + } + }, + { + "key_as_string": "2020-03-02T05:00:00.000+01:00", + "key": 1583121600000, + "doc_count": 586, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9796.900007247925 + } + }, + { + "key_as_string": "2020-03-02T06:00:00.000+01:00", + "key": 1583125200000, + "doc_count": 1515, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 24497.300002098083 + } + }, + { + "key_as_string": "2020-03-02T07:00:00.000+01:00", + "key": 1583128800000, + "doc_count": 4083, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 48543.41000652313 + } + }, + { + "key_as_string": "2020-03-02T08:00:00.000+01:00", + "key": 1583132400000, + "doc_count": 7796, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 89826.27002288774 + } + }, + { + "key_as_string": "2020-03-02T09:00:00.000+01:00", + "key": 1583136000000, + "doc_count": 9097, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 111743.74000453949 + } + }, + { + "key_as_string": "2020-03-02T10:00:00.000+01:00", + "key": 1583139600000, + "doc_count": 7923, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 95509.30001331307 + } + }, + { + "key_as_string": "2020-03-02T11:00:00.000+01:00", + "key": 1583143200000, + "doc_count": 6939, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 82218.57000637054 + } + }, + { + "key_as_string": "2020-03-02T12:00:00.000+01:00", + "key": 1583146800000, + "doc_count": 6633, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 81761.61001564004 + } + }, + { + "key_as_string": "2020-03-02T13:00:00.000+01:00", + "key": 1583150400000, + "doc_count": 7130, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 86977.67001724243 + } + }, + { + "key_as_string": "2020-03-02T14:00:00.000+01:00", + "key": 1583154000000, + "doc_count": 7287, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 91398.25000953674 + } + }, + { + "key_as_string": "2020-03-02T15:00:00.000+01:00", + "key": 1583157600000, + "doc_count": 7794, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 97141.68001651764 + } + }, + { + "key_as_string": "2020-03-02T16:00:00.000+01:00", + "key": 1583161200000, + "doc_count": 8231, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 106335.33002235368 + } + }, + { + "key_as_string": "2020-03-02T17:00:00.000+01:00", + "key": 1583164800000, + "doc_count": 7869, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 104612.67002773285 + } + }, + { + "key_as_string": "2020-03-02T18:00:00.000+01:00", + "key": 1583168400000, + "doc_count": 9837, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 121494.3800199125 + } + }, + { + "key_as_string": "2020-03-02T19:00:00.000+01:00", + "key": 1583172000000, + "doc_count": 11244, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 132524.1400082782 + } + }, + { + "key_as_string": "2020-03-02T20:00:00.000+01:00", + "key": 1583175600000, + "doc_count": 9296, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 109899.5400094986 + } + }, + { + "key_as_string": "2020-03-02T21:00:00.000+01:00", + "key": 1583179200000, + "doc_count": 8218, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 103322.06001234055 + } + }, + { + "key_as_string": "2020-03-02T22:00:00.000+01:00", + "key": 1583182800000, + "doc_count": 7628, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 101830.63999919966 + } + }, + { + "key_as_string": "2020-03-02T23:00:00.000+01:00", + "key": 1583186400000, + "doc_count": 6142, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 86926.66999343783 + } + }, + { + "key_as_string": "2020-03-03T00:00:00.000+01:00", + "key": 1583190000000, + "doc_count": 3721, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 54890.55999828316 + } + }, + { + "key_as_string": "2020-03-03T01:00:00.000+01:00", + "key": 1583193600000, + "doc_count": 2249, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 33725.28999996185 + } + }, + { + "key_as_string": "2020-03-03T02:00:00.000+01:00", + "key": 1583197200000, + "doc_count": 1153, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 17439.840002059937 + } + }, + { + "key_as_string": "2020-03-03T03:00:00.000+01:00", + "key": 1583200800000, + "doc_count": 699, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8905.300000190735 + } + }, + { + "key_as_string": "2020-03-03T04:00:00.000+01:00", + "key": 1583204400000, + "doc_count": 494, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7134.200000762939 + } + }, + { + "key_as_string": "2020-03-03T05:00:00.000+01:00", + "key": 1583208000000, + "doc_count": 562, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9029.71000171639 + } + }, + { + "key_as_string": "2020-03-03T06:00:00.000+01:00", + "key": 1583211600000, + "doc_count": 1426, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 20177.1800031662 + } + }, + { + "key_as_string": "2020-03-03T07:00:00.000+01:00", + "key": 1583215200000, + "doc_count": 4017, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 47288.01001358032 + } + }, + { + "key_as_string": "2020-03-03T08:00:00.000+01:00", + "key": 1583218800000, + "doc_count": 7736, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 86566.28001213074 + } + }, + { + "key_as_string": "2020-03-03T09:00:00.000+01:00", + "key": 1583222400000, + "doc_count": 9451, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 112218.13001918793 + } + }, + { + "key_as_string": "2020-03-03T10:00:00.000+01:00", + "key": 1583226000000, + "doc_count": 8525, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 104280.4200103376 + } + }, + { + "key_as_string": "2020-03-03T11:00:00.000+01:00", + "key": 1583229600000, + "doc_count": 7916, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 97309.6400089357 + } + }, + { + "key_as_string": "2020-03-03T12:00:00.000+01:00", + "key": 1583233200000, + "doc_count": 7718, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 94622.20000934601 + } + }, + { + "key_as_string": "2020-03-03T13:00:00.000+01:00", + "key": 1583236800000, + "doc_count": 8112, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 96547.28001117706 + } + }, + { + "key_as_string": "2020-03-03T14:00:00.000+01:00", + "key": 1583240400000, + "doc_count": 8384, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 102762.84001232125 + } + }, + { + "key_as_string": "2020-03-03T15:00:00.000+01:00", + "key": 1583244000000, + "doc_count": 10808, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 128503.81002473831 + } + }, + { + "key_as_string": "2020-03-03T16:00:00.000+01:00", + "key": 1583247600000, + "doc_count": 11682, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 144847.76002647355 + } + }, + { + "key_as_string": "2020-03-03T17:00:00.000+01:00", + "key": 1583251200000, + "doc_count": 9105, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 115458.98002601601 + } + }, + { + "key_as_string": "2020-03-03T18:00:00.000+01:00", + "key": 1583254800000, + "doc_count": 10361, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 130197.97003318742 + } + }, + { + "key_as_string": "2020-03-03T19:00:00.000+01:00", + "key": 1583258400000, + "doc_count": 11658, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 143175.74001384713 + } + }, + { + "key_as_string": "2020-03-03T20:00:00.000+01:00", + "key": 1583262000000, + "doc_count": 10027, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 118539.54001617432 + } + }, + { + "key_as_string": "2020-03-03T21:00:00.000+01:00", + "key": 1583265600000, + "doc_count": 9121, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 112776.94001247361 + } + }, + { + "key_as_string": "2020-03-03T22:00:00.000+01:00", + "key": 1583269200000, + "doc_count": 10088, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 124547.52998919226 + } + }, + { + "key_as_string": "2020-03-03T23:00:00.000+01:00", + "key": 1583272800000, + "doc_count": 9143, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 115423.28999496438 + } + }, + { + "key_as_string": "2020-03-04T00:00:00.000+01:00", + "key": 1583276400000, + "doc_count": 4676, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 62504.76999282837 + } + }, + { + "key_as_string": "2020-03-04T01:00:00.000+01:00", + "key": 1583280000000, + "doc_count": 2638, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 38664.17000057176 + } + }, + { + "key_as_string": "2020-03-04T02:00:00.000+01:00", + "key": 1583283600000, + "doc_count": 1450, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 19699.559999236837 + } + }, + { + "key_as_string": "2020-03-04T03:00:00.000+01:00", + "key": 1583287200000, + "doc_count": 859, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10806.70000076294 + } + }, + { + "key_as_string": "2020-03-04T04:00:00.000+01:00", + "key": 1583290800000, + "doc_count": 542, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8037.610002288595 + } + }, + { + "key_as_string": "2020-03-04T05:00:00.000+01:00", + "key": 1583294400000, + "doc_count": 632, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10281.700005054474 + } + }, + { + "key_as_string": "2020-03-04T06:00:00.000+01:00", + "key": 1583298000000, + "doc_count": 1582, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 24081.99000263214 + } + }, + { + "key_as_string": "2020-03-04T07:00:00.000+01:00", + "key": 1583301600000, + "doc_count": 4251, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 48204.280005931854 + } + }, + { + "key_as_string": "2020-03-04T08:00:00.000+01:00", + "key": 1583305200000, + "doc_count": 8115, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 91682.07000923157 + } + }, + { + "key_as_string": "2020-03-04T09:00:00.000+01:00", + "key": 1583308800000, + "doc_count": 9954, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 117978.45001125336 + } + }, + { + "key_as_string": "2020-03-04T10:00:00.000+01:00", + "key": 1583312400000, + "doc_count": 9017, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 110283.72000789642 + } + }, + { + "key_as_string": "2020-03-04T11:00:00.000+01:00", + "key": 1583316000000, + "doc_count": 8172, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 98942.98000526428 + } + }, + { + "key_as_string": "2020-03-04T12:00:00.000+01:00", + "key": 1583319600000, + "doc_count": 8246, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 100501.95000839233 + } + }, + { + "key_as_string": "2020-03-04T13:00:00.000+01:00", + "key": 1583323200000, + "doc_count": 8596, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 104862.18001295067 + } + }, + { + "key_as_string": "2020-03-04T14:00:00.000+01:00", + "key": 1583326800000, + "doc_count": 8427, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 109543.76001335122 + } + }, + { + "key_as_string": "2020-03-04T15:00:00.000+01:00", + "key": 1583330400000, + "doc_count": 9400, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 120479.87001037598 + } + }, + { + "key_as_string": "2020-03-04T16:00:00.000+01:00", + "key": 1583334000000, + "doc_count": 9442, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 123415.75003578141 + } + }, + { + "key_as_string": "2020-03-04T17:00:00.000+01:00", + "key": 1583337600000, + "doc_count": 8921, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 114802.4000339508 + } + }, + { + "key_as_string": "2020-03-04T18:00:00.000+01:00", + "key": 1583341200000, + "doc_count": 10756, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 133211.13003234938 + } + }, + { + "key_as_string": "2020-03-04T19:00:00.000+01:00", + "key": 1583344800000, + "doc_count": 12330, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 148727.90001707152 + } + }, + { + "key_as_string": "2020-03-04T20:00:00.000+01:00", + "key": 1583348400000, + "doc_count": 11447, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 135058.34001159668 + } + }, + { + "key_as_string": "2020-03-04T21:00:00.000+01:00", + "key": 1583352000000, + "doc_count": 11287, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 135062.45001769997 + } + }, + { + "key_as_string": "2020-03-04T22:00:00.000+01:00", + "key": 1583355600000, + "doc_count": 10551, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 133476.87001800537 + } + }, + { + "key_as_string": "2020-03-04T23:00:00.000+01:00", + "key": 1583359200000, + "doc_count": 8494, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 110113.49000526406 + } + }, + { + "key_as_string": "2020-03-05T00:00:00.000+01:00", + "key": 1583362800000, + "doc_count": 5435, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 70513.32000400499 + } + }, + { + "key_as_string": "2020-03-05T01:00:00.000+01:00", + "key": 1583366400000, + "doc_count": 3017, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 40945.82999897003 + } + }, + { + "key_as_string": "2020-03-05T02:00:00.000+01:00", + "key": 1583370000000, + "doc_count": 1705, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 21772.659994125366 + } + }, + { + "key_as_string": "2020-03-05T03:00:00.000+01:00", + "key": 1583373600000, + "doc_count": 1041, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 12203.709998130798 + } + }, + { + "key_as_string": "2020-03-05T04:00:00.000+01:00", + "key": 1583377200000, + "doc_count": 690, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9182.659998893738 + } + }, + { + "key_as_string": "2020-03-05T05:00:00.000+01:00", + "key": 1583380800000, + "doc_count": 741, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 12935.400001525879 + } + }, + { + "key_as_string": "2020-03-05T06:00:00.000+01:00", + "key": 1583384400000, + "doc_count": 1487, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 22234.23000122048 + } + }, + { + "key_as_string": "2020-03-05T07:00:00.000+01:00", + "key": 1583388000000, + "doc_count": 4213, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 50418.240016937256 + } + }, + { + "key_as_string": "2020-03-05T08:00:00.000+01:00", + "key": 1583391600000, + "doc_count": 8314, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 93121.56001377106 + } + }, + { + "key_as_string": "2020-03-05T09:00:00.000+01:00", + "key": 1583395200000, + "doc_count": 10461, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 123802.39001464844 + } + }, + { + "key_as_string": "2020-03-05T10:00:00.000+01:00", + "key": 1583398800000, + "doc_count": 9730, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 117206.7500024531 + } + }, + { + "key_as_string": "2020-03-05T11:00:00.000+01:00", + "key": 1583402400000, + "doc_count": 10551, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 137863.32000732422 + } + }, + { + "key_as_string": "2020-03-05T12:00:00.000+01:00", + "key": 1583406000000, + "doc_count": 8859, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 109684.16000887193 + } + }, + { + "key_as_string": "2020-03-05T13:00:00.000+01:00", + "key": 1583409600000, + "doc_count": 8866, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 111860.52001877129 + } + }, + { + "key_as_string": "2020-03-05T14:00:00.000+01:00", + "key": 1583413200000, + "doc_count": 8800, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 114759.15002632141 + } + }, + { + "key_as_string": "2020-03-05T15:00:00.000+01:00", + "key": 1583416800000, + "doc_count": 9592, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 127636.9400239177 + } + }, + { + "key_as_string": "2020-03-05T16:00:00.000+01:00", + "key": 1583420400000, + "doc_count": 9528, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 125207.06003215723 + } + }, + { + "key_as_string": "2020-03-05T17:00:00.000+01:00", + "key": 1583424000000, + "doc_count": 9315, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 121494.91002918221 + } + }, + { + "key_as_string": "2020-03-05T18:00:00.000+01:00", + "key": 1583427600000, + "doc_count": 11463, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 141068.28002502397 + } + }, + { + "key_as_string": "2020-03-05T19:00:00.000+01:00", + "key": 1583431200000, + "doc_count": 13082, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 158059.0200209804 + } + }, + { + "key_as_string": "2020-03-05T20:00:00.000+01:00", + "key": 1583434800000, + "doc_count": 11446, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 139933.2700176239 + } + }, + { + "key_as_string": "2020-03-05T21:00:00.000+01:00", + "key": 1583438400000, + "doc_count": 10281, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 128757.53000999428 + } + }, + { + "key_as_string": "2020-03-05T22:00:00.000+01:00", + "key": 1583442000000, + "doc_count": 10463, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 133505.56999855116 + } + }, + { + "key_as_string": "2020-03-05T23:00:00.000+01:00", + "key": 1583445600000, + "doc_count": 9982, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 130831.41000877507 + } + }, + { + "key_as_string": "2020-03-06T00:00:00.000+01:00", + "key": 1583449200000, + "doc_count": 6559, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 84433.7899940107 + } + }, + { + "key_as_string": "2020-03-06T01:00:00.000+01:00", + "key": 1583452800000, + "doc_count": 4113, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 52986.37000083923 + } + }, + { + "key_as_string": "2020-03-06T02:00:00.000+01:00", + "key": 1583456400000, + "doc_count": 2488, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 29301.019996909425 + } + }, + { + "key_as_string": "2020-03-06T03:00:00.000+01:00", + "key": 1583460000000, + "doc_count": 1455, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 17465.22000169754 + } + }, + { + "key_as_string": "2020-03-06T04:00:00.000+01:00", + "key": 1583463600000, + "doc_count": 1004, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 13216.94999885559 + } + }, + { + "key_as_string": "2020-03-06T05:00:00.000+01:00", + "key": 1583467200000, + "doc_count": 999, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 15785.530002364889 + } + }, + { + "key_as_string": "2020-03-06T06:00:00.000+01:00", + "key": 1583470800000, + "doc_count": 1547, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 24655.700001716614 + } + }, + { + "key_as_string": "2020-03-06T07:00:00.000+01:00", + "key": 1583474400000, + "doc_count": 4059, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 47983.43000411987 + } + }, + { + "key_as_string": "2020-03-06T08:00:00.000+01:00", + "key": 1583478000000, + "doc_count": 7526, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 84826.00001907349 + } + }, + { + "key_as_string": "2020-03-06T09:00:00.000+01:00", + "key": 1583481600000, + "doc_count": 9545, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 111400.7300233841 + } + }, + { + "key_as_string": "2020-03-06T10:00:00.000+01:00", + "key": 1583485200000, + "doc_count": 8708, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 104632.14000606537 + } + }, + { + "key_as_string": "2020-03-06T11:00:00.000+01:00", + "key": 1583488800000, + "doc_count": 7841, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 95115.67000484467 + } + }, + { + "key_as_string": "2020-03-06T12:00:00.000+01:00", + "key": 1583492400000, + "doc_count": 8491, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 101223.19000816345 + } + }, + { + "key_as_string": "2020-03-06T13:00:00.000+01:00", + "key": 1583496000000, + "doc_count": 8881, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 104602.86001182534 + } + }, + { + "key_as_string": "2020-03-06T14:00:00.000+01:00", + "key": 1583499600000, + "doc_count": 9779, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 118404.24000812508 + } + }, + { + "key_as_string": "2020-03-06T15:00:00.000+01:00", + "key": 1583503200000, + "doc_count": 11337, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 142803.61001731828 + } + }, + { + "key_as_string": "2020-03-06T16:00:00.000+01:00", + "key": 1583506800000, + "doc_count": 10380, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 136764.43001556396 + } + }, + { + "key_as_string": "2020-03-06T17:00:00.000+01:00", + "key": 1583510400000, + "doc_count": 9039, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 120485.72002365068 + } + }, + { + "key_as_string": "2020-03-06T18:00:00.000+01:00", + "key": 1583514000000, + "doc_count": 11619, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 147596.4600083921 + } + }, + { + "key_as_string": "2020-03-06T19:00:00.000+01:00", + "key": 1583517600000, + "doc_count": 12680, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 155689.000014076 + } + }, + { + "key_as_string": "2020-03-06T20:00:00.000+01:00", + "key": 1583521200000, + "doc_count": 11402, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 140214.910003433 + } + }, + { + "key_as_string": "2020-03-06T21:00:00.000+01:00", + "key": 1583524800000, + "doc_count": 8243, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 103935.08999538422 + } + }, + { + "key_as_string": "2020-03-06T22:00:00.000+01:00", + "key": 1583528400000, + "doc_count": 7860, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 97568.04000663757 + } + }, + { + "key_as_string": "2020-03-06T23:00:00.000+01:00", + "key": 1583532000000, + "doc_count": 8754, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 109642.79000717215 + } + }, + { + "key_as_string": "2020-03-07T00:00:00.000+01:00", + "key": 1583535600000, + "doc_count": 7589, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 96109.74000072479 + } + }, + { + "key_as_string": "2020-03-07T01:00:00.000+01:00", + "key": 1583539200000, + "doc_count": 5904, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 73824.64999916032 + } + }, + { + "key_as_string": "2020-03-07T02:00:00.000+01:00", + "key": 1583542800000, + "doc_count": 4714, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 57449.39999772981 + } + }, + { + "key_as_string": "2020-03-07T03:00:00.000+01:00", + "key": 1583546400000, + "doc_count": 3777, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 43647.46999908425 + } + }, + { + "key_as_string": "2020-03-07T04:00:00.000+01:00", + "key": 1583550000000, + "doc_count": 2456, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 29974.10000038147 + } + }, + { + "key_as_string": "2020-03-07T05:00:00.000+01:00", + "key": 1583553600000, + "doc_count": 1666, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 22199.89999961853 + } + }, + { + "key_as_string": "2020-03-07T06:00:00.000+01:00", + "key": 1583557200000, + "doc_count": 929, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 15604.500001907349 + } + }, + { + "key_as_string": "2020-03-07T07:00:00.000+01:00", + "key": 1583560800000, + "doc_count": 1516, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 21308.819999694824 + } + }, + { + "key_as_string": "2020-03-07T08:00:00.000+01:00", + "key": 1583564400000, + "doc_count": 2297, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 29054.55000591278 + } + }, + { + "key_as_string": "2020-03-07T09:00:00.000+01:00", + "key": 1583568000000, + "doc_count": 3789, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 42230.590003967285 + } + }, + { + "key_as_string": "2020-03-07T10:00:00.000+01:00", + "key": 1583571600000, + "doc_count": 5446, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 56812.540002822876 + } + }, + { + "key_as_string": "2020-03-07T11:00:00.000+01:00", + "key": 1583575200000, + "doc_count": 6473, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 67401.60000228882 + } + }, + { + "key_as_string": "2020-03-07T12:00:00.000+01:00", + "key": 1583578800000, + "doc_count": 7215, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 76582.73000312783 + } + }, + { + "key_as_string": "2020-03-07T13:00:00.000+01:00", + "key": 1583582400000, + "doc_count": 7747, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 84855.8000164032 + } + }, + { + "key_as_string": "2020-03-07T14:00:00.000+01:00", + "key": 1583586000000, + "doc_count": 7808, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 89765.05000568368 + } + }, + { + "key_as_string": "2020-03-07T15:00:00.000+01:00", + "key": 1583589600000, + "doc_count": 7481, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 89854.66000939347 + } + }, + { + "key_as_string": "2020-03-07T16:00:00.000+01:00", + "key": 1583593200000, + "doc_count": 7728, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 95179.0799994655 + } + }, + { + "key_as_string": "2020-03-07T17:00:00.000+01:00", + "key": 1583596800000, + "doc_count": 7707, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 92442.28000236489 + } + }, + { + "key_as_string": "2020-03-07T18:00:00.000+01:00", + "key": 1583600400000, + "doc_count": 8152, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 96965.98999359086 + } + }, + { + "key_as_string": "2020-03-07T19:00:00.000+01:00", + "key": 1583604000000, + "doc_count": 9076, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 107920.21000171639 + } + }, + { + "key_as_string": "2020-03-07T20:00:00.000+01:00", + "key": 1583607600000, + "doc_count": 8755, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 103143.00000953674 + } + }, + { + "key_as_string": "2020-03-07T21:00:00.000+01:00", + "key": 1583611200000, + "doc_count": 6647, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 80349.27000236511 + } + }, + { + "key_as_string": "2020-03-07T22:00:00.000+01:00", + "key": 1583614800000, + "doc_count": 6853, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 80808.96000480652 + } + }, + { + "key_as_string": "2020-03-07T23:00:00.000+01:00", + "key": 1583618400000, + "doc_count": 7900, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 95062.01000871137 + } + }, + { + "key_as_string": "2020-03-08T00:00:00.000+01:00", + "key": 1583622000000, + "doc_count": 6880, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 81881.05000305176 + } + } + ] + }, + "2 passengers": { + "doc_count": 186705, + "buckets": [ + { + "key_as_string": "2020-03-01T23:00:00.000+01:00", + "key": 1583100000000, + "doc_count": 28, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 405 + } + }, + { + "key_as_string": "2020-03-02T00:00:00.000+01:00", + "key": 1583103600000, + "doc_count": 610, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10258.91999912262 + } + }, + { + "key_as_string": "2020-03-02T01:00:00.000+01:00", + "key": 1583107200000, + "doc_count": 341, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5637.419998168945 + } + }, + { + "key_as_string": "2020-03-02T02:00:00.000+01:00", + "key": 1583110800000, + "doc_count": 196, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3007.6199989318848 + } + }, + { + "key_as_string": "2020-03-02T03:00:00.000+01:00", + "key": 1583114400000, + "doc_count": 138, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2126.5 + } + }, + { + "key_as_string": "2020-03-02T04:00:00.000+01:00", + "key": 1583118000000, + "doc_count": 85, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1475 + } + }, + { + "key_as_string": "2020-03-02T05:00:00.000+01:00", + "key": 1583121600000, + "doc_count": 93, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1620.5 + } + }, + { + "key_as_string": "2020-03-02T06:00:00.000+01:00", + "key": 1583125200000, + "doc_count": 186, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4497 + } + }, + { + "key_as_string": "2020-03-02T07:00:00.000+01:00", + "key": 1583128800000, + "doc_count": 453, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7304.300003051758 + } + }, + { + "key_as_string": "2020-03-02T08:00:00.000+01:00", + "key": 1583132400000, + "doc_count": 1008, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 12837.119998931885 + } + }, + { + "key_as_string": "2020-03-02T09:00:00.000+01:00", + "key": 1583136000000, + "doc_count": 1271, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 16647 + } + }, + { + "key_as_string": "2020-03-02T10:00:00.000+01:00", + "key": 1583139600000, + "doc_count": 1113, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 14823.800003051758 + } + }, + { + "key_as_string": "2020-03-02T11:00:00.000+01:00", + "key": 1583143200000, + "doc_count": 1100, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 14961.619998931885 + } + }, + { + "key_as_string": "2020-03-02T12:00:00.000+01:00", + "key": 1583146800000, + "doc_count": 1154, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 15359.5 + } + }, + { + "key_as_string": "2020-03-02T13:00:00.000+01:00", + "key": 1583150400000, + "doc_count": 1191, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 16084.5 + } + }, + { + "key_as_string": "2020-03-02T14:00:00.000+01:00", + "key": 1583154000000, + "doc_count": 1260, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 18832.58999633789 + } + }, + { + "key_as_string": "2020-03-02T15:00:00.000+01:00", + "key": 1583157600000, + "doc_count": 1486, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 21229 + } + }, + { + "key_as_string": "2020-03-02T16:00:00.000+01:00", + "key": 1583161200000, + "doc_count": 1641, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 22983.240005493164 + } + }, + { + "key_as_string": "2020-03-02T17:00:00.000+01:00", + "key": 1583164800000, + "doc_count": 1549, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 22569.230003356934 + } + }, + { + "key_as_string": "2020-03-02T18:00:00.000+01:00", + "key": 1583168400000, + "doc_count": 1803, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 25230.759998321533 + } + }, + { + "key_as_string": "2020-03-02T19:00:00.000+01:00", + "key": 1583172000000, + "doc_count": 1936, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 24030.800003051758 + } + }, + { + "key_as_string": "2020-03-02T20:00:00.000+01:00", + "key": 1583175600000, + "doc_count": 1670, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 20322.419998168945 + } + }, + { + "key_as_string": "2020-03-02T21:00:00.000+01:00", + "key": 1583179200000, + "doc_count": 1494, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 20300.920001983643 + } + }, + { + "key_as_string": "2020-03-02T22:00:00.000+01:00", + "key": 1583182800000, + "doc_count": 1539, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 20441.720001220703 + } + }, + { + "key_as_string": "2020-03-02T23:00:00.000+01:00", + "key": 1583186400000, + "doc_count": 1367, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 18494 + } + }, + { + "key_as_string": "2020-03-03T00:00:00.000+01:00", + "key": 1583190000000, + "doc_count": 728, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9971.919998168945 + } + }, + { + "key_as_string": "2020-03-03T01:00:00.000+01:00", + "key": 1583193600000, + "doc_count": 461, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6804.5 + } + }, + { + "key_as_string": "2020-03-03T02:00:00.000+01:00", + "key": 1583197200000, + "doc_count": 214, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3568.5 + } + }, + { + "key_as_string": "2020-03-03T03:00:00.000+01:00", + "key": 1583200800000, + "doc_count": 109, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1410.5 + } + }, + { + "key_as_string": "2020-03-03T04:00:00.000+01:00", + "key": 1583204400000, + "doc_count": 80, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1134.5 + } + }, + { + "key_as_string": "2020-03-03T05:00:00.000+01:00", + "key": 1583208000000, + "doc_count": 89, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1629.7999992370605 + } + }, + { + "key_as_string": "2020-03-03T06:00:00.000+01:00", + "key": 1583211600000, + "doc_count": 168, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3389.7999992370605 + } + }, + { + "key_as_string": "2020-03-03T07:00:00.000+01:00", + "key": 1583215200000, + "doc_count": 441, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6873.919998168945 + } + }, + { + "key_as_string": "2020-03-03T08:00:00.000+01:00", + "key": 1583218800000, + "doc_count": 1067, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 12824 + } + }, + { + "key_as_string": "2020-03-03T09:00:00.000+01:00", + "key": 1583222400000, + "doc_count": 1326, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 16921.10000228882 + } + }, + { + "key_as_string": "2020-03-03T10:00:00.000+01:00", + "key": 1583226000000, + "doc_count": 1192, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 16609.800003051758 + } + }, + { + "key_as_string": "2020-03-03T11:00:00.000+01:00", + "key": 1583229600000, + "doc_count": 1242, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 17136 + } + }, + { + "key_as_string": "2020-03-03T12:00:00.000+01:00", + "key": 1583233200000, + "doc_count": 1272, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 17326.5 + } + }, + { + "key_as_string": "2020-03-03T13:00:00.000+01:00", + "key": 1583236800000, + "doc_count": 1342, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 17940 + } + }, + { + "key_as_string": "2020-03-03T14:00:00.000+01:00", + "key": 1583240400000, + "doc_count": 1472, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 21097 + } + }, + { + "key_as_string": "2020-03-03T15:00:00.000+01:00", + "key": 1583244000000, + "doc_count": 1984, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 25924.919998168945 + } + }, + { + "key_as_string": "2020-03-03T16:00:00.000+01:00", + "key": 1583247600000, + "doc_count": 2311, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 30512.369992980734 + } + }, + { + "key_as_string": "2020-03-03T17:00:00.000+01:00", + "key": 1583251200000, + "doc_count": 1670, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 23779.670002937317 + } + }, + { + "key_as_string": "2020-03-03T18:00:00.000+01:00", + "key": 1583254800000, + "doc_count": 1825, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 22873.300000190735 + } + }, + { + "key_as_string": "2020-03-03T19:00:00.000+01:00", + "key": 1583258400000, + "doc_count": 2022, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 26291.260003089905 + } + }, + { + "key_as_string": "2020-03-03T20:00:00.000+01:00", + "key": 1583262000000, + "doc_count": 1751, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 21683.83999633789 + } + }, + { + "key_as_string": "2020-03-03T21:00:00.000+01:00", + "key": 1583265600000, + "doc_count": 1640, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 20601.120002746582 + } + }, + { + "key_as_string": "2020-03-03T22:00:00.000+01:00", + "key": 1583269200000, + "doc_count": 2013, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 25952.41999912262 + } + }, + { + "key_as_string": "2020-03-03T23:00:00.000+01:00", + "key": 1583272800000, + "doc_count": 1914, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 23768.420002937317 + } + }, + { + "key_as_string": "2020-03-04T00:00:00.000+01:00", + "key": 1583276400000, + "doc_count": 920, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11899.459999084473 + } + }, + { + "key_as_string": "2020-03-04T01:00:00.000+01:00", + "key": 1583280000000, + "doc_count": 468, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7298 + } + }, + { + "key_as_string": "2020-03-04T02:00:00.000+01:00", + "key": 1583283600000, + "doc_count": 223, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3278.5 + } + }, + { + "key_as_string": "2020-03-04T03:00:00.000+01:00", + "key": 1583287200000, + "doc_count": 150, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2136.199999809265 + } + }, + { + "key_as_string": "2020-03-04T04:00:00.000+01:00", + "key": 1583290800000, + "doc_count": 94, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1381 + } + }, + { + "key_as_string": "2020-03-04T05:00:00.000+01:00", + "key": 1583294400000, + "doc_count": 105, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1437.5 + } + }, + { + "key_as_string": "2020-03-04T06:00:00.000+01:00", + "key": 1583298000000, + "doc_count": 169, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2632.5 + } + }, + { + "key_as_string": "2020-03-04T07:00:00.000+01:00", + "key": 1583301600000, + "doc_count": 447, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6082.5 + } + }, + { + "key_as_string": "2020-03-04T08:00:00.000+01:00", + "key": 1583305200000, + "doc_count": 1090, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 12506 + } + }, + { + "key_as_string": "2020-03-04T09:00:00.000+01:00", + "key": 1583308800000, + "doc_count": 1370, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 17135 + } + }, + { + "key_as_string": "2020-03-04T10:00:00.000+01:00", + "key": 1583312400000, + "doc_count": 1258, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 16801.009999999776 + } + }, + { + "key_as_string": "2020-03-04T11:00:00.000+01:00", + "key": 1583316000000, + "doc_count": 1246, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 15826.5 + } + }, + { + "key_as_string": "2020-03-04T12:00:00.000+01:00", + "key": 1583319600000, + "doc_count": 1360, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 18243.319999236614 + } + }, + { + "key_as_string": "2020-03-04T13:00:00.000+01:00", + "key": 1583323200000, + "doc_count": 1440, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 20010.5 + } + }, + { + "key_as_string": "2020-03-04T14:00:00.000+01:00", + "key": 1583326800000, + "doc_count": 1463, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 22286.919998168945 + } + }, + { + "key_as_string": "2020-03-04T15:00:00.000+01:00", + "key": 1583330400000, + "doc_count": 1689, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 24144.879997253418 + } + }, + { + "key_as_string": "2020-03-04T16:00:00.000+01:00", + "key": 1583334000000, + "doc_count": 1712, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 23332.219997406006 + } + }, + { + "key_as_string": "2020-03-04T17:00:00.000+01:00", + "key": 1583337600000, + "doc_count": 1652, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 23256.160007476807 + } + }, + { + "key_as_string": "2020-03-04T18:00:00.000+01:00", + "key": 1583341200000, + "doc_count": 1853, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 23987.95000076294 + } + }, + { + "key_as_string": "2020-03-04T19:00:00.000+01:00", + "key": 1583344800000, + "doc_count": 2219, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 27491 + } + }, + { + "key_as_string": "2020-03-04T20:00:00.000+01:00", + "key": 1583348400000, + "doc_count": 2080, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 24858.5 + } + }, + { + "key_as_string": "2020-03-04T21:00:00.000+01:00", + "key": 1583352000000, + "doc_count": 2249, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 26847.5 + } + }, + { + "key_as_string": "2020-03-04T22:00:00.000+01:00", + "key": 1583355600000, + "doc_count": 2132, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 26175.82000350952 + } + }, + { + "key_as_string": "2020-03-04T23:00:00.000+01:00", + "key": 1583359200000, + "doc_count": 1838, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 23919.370002746582 + } + }, + { + "key_as_string": "2020-03-05T00:00:00.000+01:00", + "key": 1583362800000, + "doc_count": 1100, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 14639.419998168945 + } + }, + { + "key_as_string": "2020-03-05T01:00:00.000+01:00", + "key": 1583366400000, + "doc_count": 598, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7066.5 + } + }, + { + "key_as_string": "2020-03-05T02:00:00.000+01:00", + "key": 1583370000000, + "doc_count": 333, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4684.800000190735 + } + }, + { + "key_as_string": "2020-03-05T03:00:00.000+01:00", + "key": 1583373600000, + "doc_count": 187, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2297.300000190735 + } + }, + { + "key_as_string": "2020-03-05T04:00:00.000+01:00", + "key": 1583377200000, + "doc_count": 132, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1824.1199989318848 + } + }, + { + "key_as_string": "2020-03-05T05:00:00.000+01:00", + "key": 1583380800000, + "doc_count": 105, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1932.5 + } + }, + { + "key_as_string": "2020-03-05T06:00:00.000+01:00", + "key": 1583384400000, + "doc_count": 173, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2670.599998474121 + } + }, + { + "key_as_string": "2020-03-05T07:00:00.000+01:00", + "key": 1583388000000, + "doc_count": 435, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6626.5 + } + }, + { + "key_as_string": "2020-03-05T08:00:00.000+01:00", + "key": 1583391600000, + "doc_count": 1119, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 13355.5 + } + }, + { + "key_as_string": "2020-03-05T09:00:00.000+01:00", + "key": 1583395200000, + "doc_count": 1417, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 17453.050003051758 + } + }, + { + "key_as_string": "2020-03-05T10:00:00.000+01:00", + "key": 1583398800000, + "doc_count": 1337, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 18050.219997406006 + } + }, + { + "key_as_string": "2020-03-05T11:00:00.000+01:00", + "key": 1583402400000, + "doc_count": 1652, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 23409.5 + } + }, + { + "key_as_string": "2020-03-05T12:00:00.000+01:00", + "key": 1583406000000, + "doc_count": 1403, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 19611.83999633789 + } + }, + { + "key_as_string": "2020-03-05T13:00:00.000+01:00", + "key": 1583409600000, + "doc_count": 1530, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 21741.38999938965 + } + }, + { + "key_as_string": "2020-03-05T14:00:00.000+01:00", + "key": 1583413200000, + "doc_count": 1568, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 23559.5 + } + }, + { + "key_as_string": "2020-03-05T15:00:00.000+01:00", + "key": 1583416800000, + "doc_count": 1736, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 26078.240001678467 + } + }, + { + "key_as_string": "2020-03-05T16:00:00.000+01:00", + "key": 1583420400000, + "doc_count": 1769, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 25146.62000091374 + } + }, + { + "key_as_string": "2020-03-05T17:00:00.000+01:00", + "key": 1583424000000, + "doc_count": 1764, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 24361.220002174377 + } + }, + { + "key_as_string": "2020-03-05T18:00:00.000+01:00", + "key": 1583427600000, + "doc_count": 2202, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 29728.880001068115 + } + }, + { + "key_as_string": "2020-03-05T19:00:00.000+01:00", + "key": 1583431200000, + "doc_count": 2408, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 30161.499996185303 + } + }, + { + "key_as_string": "2020-03-05T20:00:00.000+01:00", + "key": 1583434800000, + "doc_count": 2248, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 27410.219997406006 + } + }, + { + "key_as_string": "2020-03-05T21:00:00.000+01:00", + "key": 1583438400000, + "doc_count": 2077, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 25875.16999984719 + } + }, + { + "key_as_string": "2020-03-05T22:00:00.000+01:00", + "key": 1583442000000, + "doc_count": 2386, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 29442.31999206543 + } + }, + { + "key_as_string": "2020-03-05T23:00:00.000+01:00", + "key": 1583445600000, + "doc_count": 2339, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 29801.969997406006 + } + }, + { + "key_as_string": "2020-03-06T00:00:00.000+01:00", + "key": 1583449200000, + "doc_count": 1467, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 17895.800000190735 + } + }, + { + "key_as_string": "2020-03-06T01:00:00.000+01:00", + "key": 1583452800000, + "doc_count": 869, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11359.5 + } + }, + { + "key_as_string": "2020-03-06T02:00:00.000+01:00", + "key": 1583456400000, + "doc_count": 488, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5876 + } + }, + { + "key_as_string": "2020-03-06T03:00:00.000+01:00", + "key": 1583460000000, + "doc_count": 341, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3898.7999992370605 + } + }, + { + "key_as_string": "2020-03-06T04:00:00.000+01:00", + "key": 1583463600000, + "doc_count": 198, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2669.8300018310547 + } + }, + { + "key_as_string": "2020-03-06T05:00:00.000+01:00", + "key": 1583467200000, + "doc_count": 171, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2639.5 + } + }, + { + "key_as_string": "2020-03-06T06:00:00.000+01:00", + "key": 1583470800000, + "doc_count": 192, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3586.7999992370605 + } + }, + { + "key_as_string": "2020-03-06T07:00:00.000+01:00", + "key": 1583474400000, + "doc_count": 430, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6126 + } + }, + { + "key_as_string": "2020-03-06T08:00:00.000+01:00", + "key": 1583478000000, + "doc_count": 1059, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 13004.699999809265 + } + }, + { + "key_as_string": "2020-03-06T09:00:00.000+01:00", + "key": 1583481600000, + "doc_count": 1334, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 16817.75 + } + }, + { + "key_as_string": "2020-03-06T10:00:00.000+01:00", + "key": 1583485200000, + "doc_count": 1243, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 16457.450001716614 + } + }, + { + "key_as_string": "2020-03-06T11:00:00.000+01:00", + "key": 1583488800000, + "doc_count": 1257, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 17359.5 + } + }, + { + "key_as_string": "2020-03-06T12:00:00.000+01:00", + "key": 1583492400000, + "doc_count": 1432, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 18841.38999557495 + } + }, + { + "key_as_string": "2020-03-06T13:00:00.000+01:00", + "key": 1583496000000, + "doc_count": 1493, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 19288.599996337667 + } + }, + { + "key_as_string": "2020-03-06T14:00:00.000+01:00", + "key": 1583499600000, + "doc_count": 1838, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 24128.300000190735 + } + }, + { + "key_as_string": "2020-03-06T15:00:00.000+01:00", + "key": 1583503200000, + "doc_count": 2167, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 29402.29999923706 + } + }, + { + "key_as_string": "2020-03-06T16:00:00.000+01:00", + "key": 1583506800000, + "doc_count": 2134, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 30264.83999633789 + } + }, + { + "key_as_string": "2020-03-06T17:00:00.000+01:00", + "key": 1583510400000, + "doc_count": 1694, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 24695.220001220703 + } + }, + { + "key_as_string": "2020-03-06T18:00:00.000+01:00", + "key": 1583514000000, + "doc_count": 2292, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 30800.500000953674 + } + }, + { + "key_as_string": "2020-03-06T19:00:00.000+01:00", + "key": 1583517600000, + "doc_count": 2650, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 34681.20000076294 + } + }, + { + "key_as_string": "2020-03-06T20:00:00.000+01:00", + "key": 1583521200000, + "doc_count": 2656, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 31831.60000038147 + } + }, + { + "key_as_string": "2020-03-06T21:00:00.000+01:00", + "key": 1583524800000, + "doc_count": 1991, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 23769.40000152588 + } + }, + { + "key_as_string": "2020-03-06T22:00:00.000+01:00", + "key": 1583528400000, + "doc_count": 2225, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 27154.20000076294 + } + }, + { + "key_as_string": "2020-03-06T23:00:00.000+01:00", + "key": 1583532000000, + "doc_count": 2584, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 31251 + } + }, + { + "key_as_string": "2020-03-07T00:00:00.000+01:00", + "key": 1583535600000, + "doc_count": 2102, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 26095.349999427795 + } + }, + { + "key_as_string": "2020-03-07T01:00:00.000+01:00", + "key": 1583539200000, + "doc_count": 1488, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 18438 + } + }, + { + "key_as_string": "2020-03-07T02:00:00.000+01:00", + "key": 1583542800000, + "doc_count": 1171, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 13542.620002746582 + } + }, + { + "key_as_string": "2020-03-07T03:00:00.000+01:00", + "key": 1583546400000, + "doc_count": 993, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11024.300000190735 + } + }, + { + "key_as_string": "2020-03-07T04:00:00.000+01:00", + "key": 1583550000000, + "doc_count": 612, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7452.909997710958 + } + }, + { + "key_as_string": "2020-03-07T05:00:00.000+01:00", + "key": 1583553600000, + "doc_count": 379, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5697.799999237061 + } + }, + { + "key_as_string": "2020-03-07T06:00:00.000+01:00", + "key": 1583557200000, + "doc_count": 141, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2177.5 + } + }, + { + "key_as_string": "2020-03-07T07:00:00.000+01:00", + "key": 1583560800000, + "doc_count": 200, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3945.5 + } + }, + { + "key_as_string": "2020-03-07T08:00:00.000+01:00", + "key": 1583564400000, + "doc_count": 326, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4841.419998168945 + } + }, + { + "key_as_string": "2020-03-07T09:00:00.000+01:00", + "key": 1583568000000, + "doc_count": 604, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7548.389999389648 + } + }, + { + "key_as_string": "2020-03-07T10:00:00.000+01:00", + "key": 1583571600000, + "doc_count": 987, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11085.5 + } + }, + { + "key_as_string": "2020-03-07T11:00:00.000+01:00", + "key": 1583575200000, + "doc_count": 1285, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 14574.149975776672 + } + }, + { + "key_as_string": "2020-03-07T12:00:00.000+01:00", + "key": 1583578800000, + "doc_count": 1638, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 18388 + } + }, + { + "key_as_string": "2020-03-07T13:00:00.000+01:00", + "key": 1583582400000, + "doc_count": 1748, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 20822.710000762716 + } + }, + { + "key_as_string": "2020-03-07T14:00:00.000+01:00", + "key": 1583586000000, + "doc_count": 1899, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 23774.5 + } + }, + { + "key_as_string": "2020-03-07T15:00:00.000+01:00", + "key": 1583589600000, + "doc_count": 1812, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 24348.41999912262 + } + }, + { + "key_as_string": "2020-03-07T16:00:00.000+01:00", + "key": 1583593200000, + "doc_count": 1920, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 24761.919998168945 + } + }, + { + "key_as_string": "2020-03-07T17:00:00.000+01:00", + "key": 1583596800000, + "doc_count": 2206, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 27332.79999923706 + } + }, + { + "key_as_string": "2020-03-07T18:00:00.000+01:00", + "key": 1583600400000, + "doc_count": 2267, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 27598 + } + }, + { + "key_as_string": "2020-03-07T19:00:00.000+01:00", + "key": 1583604000000, + "doc_count": 2690, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 32270.719997406006 + } + }, + { + "key_as_string": "2020-03-07T20:00:00.000+01:00", + "key": 1583607600000, + "doc_count": 2655, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 30426.56000137329 + } + }, + { + "key_as_string": "2020-03-07T21:00:00.000+01:00", + "key": 1583611200000, + "doc_count": 2022, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 25030.120002746582 + } + }, + { + "key_as_string": "2020-03-07T22:00:00.000+01:00", + "key": 1583614800000, + "doc_count": 2245, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 25905.28999710083 + } + }, + { + "key_as_string": "2020-03-07T23:00:00.000+01:00", + "key": 1583618400000, + "doc_count": 2606, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 29786.399997860193 + } + }, + { + "key_as_string": "2020-03-08T00:00:00.000+01:00", + "key": 1583622000000, + "doc_count": 2214, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 25452.619998931885 + } + } + ] + }, + "3 passengers": { + "doc_count": 49150, + "buckets": [ + { + "key_as_string": "2020-03-01T23:00:00.000+01:00", + "key": 1583100000000, + "doc_count": 7, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 196.5 + } + }, + { + "key_as_string": "2020-03-02T00:00:00.000+01:00", + "key": 1583103600000, + "doc_count": 139, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1901 + } + }, + { + "key_as_string": "2020-03-02T01:00:00.000+01:00", + "key": 1583107200000, + "doc_count": 82, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1211.5 + } + }, + { + "key_as_string": "2020-03-02T02:00:00.000+01:00", + "key": 1583110800000, + "doc_count": 58, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 821 + } + }, + { + "key_as_string": "2020-03-02T03:00:00.000+01:00", + "key": 1583114400000, + "doc_count": 36, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 606.5 + } + }, + { + "key_as_string": "2020-03-02T04:00:00.000+01:00", + "key": 1583118000000, + "doc_count": 28, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 373.5 + } + }, + { + "key_as_string": "2020-03-02T05:00:00.000+01:00", + "key": 1583121600000, + "doc_count": 20, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 330.79999923706055 + } + }, + { + "key_as_string": "2020-03-02T06:00:00.000+01:00", + "key": 1583125200000, + "doc_count": 53, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 971 + } + }, + { + "key_as_string": "2020-03-02T07:00:00.000+01:00", + "key": 1583128800000, + "doc_count": 115, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1644.5 + } + }, + { + "key_as_string": "2020-03-02T08:00:00.000+01:00", + "key": 1583132400000, + "doc_count": 313, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3725 + } + }, + { + "key_as_string": "2020-03-02T09:00:00.000+01:00", + "key": 1583136000000, + "doc_count": 373, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4711 + } + }, + { + "key_as_string": "2020-03-02T10:00:00.000+01:00", + "key": 1583139600000, + "doc_count": 288, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3770.5 + } + }, + { + "key_as_string": "2020-03-02T11:00:00.000+01:00", + "key": 1583143200000, + "doc_count": 280, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3648.9199981689453 + } + }, + { + "key_as_string": "2020-03-02T12:00:00.000+01:00", + "key": 1583146800000, + "doc_count": 311, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4071.7000007629395 + } + }, + { + "key_as_string": "2020-03-02T13:00:00.000+01:00", + "key": 1583150400000, + "doc_count": 302, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3854.5 + } + }, + { + "key_as_string": "2020-03-02T14:00:00.000+01:00", + "key": 1583154000000, + "doc_count": 336, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4352.5 + } + }, + { + "key_as_string": "2020-03-02T15:00:00.000+01:00", + "key": 1583157600000, + "doc_count": 400, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5458 + } + }, + { + "key_as_string": "2020-03-02T16:00:00.000+01:00", + "key": 1583161200000, + "doc_count": 486, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6371.5 + } + }, + { + "key_as_string": "2020-03-02T17:00:00.000+01:00", + "key": 1583164800000, + "doc_count": 434, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6055.5 + } + }, + { + "key_as_string": "2020-03-02T18:00:00.000+01:00", + "key": 1583168400000, + "doc_count": 511, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6961.75 + } + }, + { + "key_as_string": "2020-03-02T19:00:00.000+01:00", + "key": 1583172000000, + "doc_count": 544, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6632.5 + } + }, + { + "key_as_string": "2020-03-02T20:00:00.000+01:00", + "key": 1583175600000, + "doc_count": 470, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5724.119998931885 + } + }, + { + "key_as_string": "2020-03-02T21:00:00.000+01:00", + "key": 1583179200000, + "doc_count": 430, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5448 + } + }, + { + "key_as_string": "2020-03-02T22:00:00.000+01:00", + "key": 1583182800000, + "doc_count": 375, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4769.5 + } + }, + { + "key_as_string": "2020-03-02T23:00:00.000+01:00", + "key": 1583186400000, + "doc_count": 347, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4350 + } + }, + { + "key_as_string": "2020-03-03T00:00:00.000+01:00", + "key": 1583190000000, + "doc_count": 195, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2526.9199981689453 + } + }, + { + "key_as_string": "2020-03-03T01:00:00.000+01:00", + "key": 1583193600000, + "doc_count": 105, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1406.5 + } + }, + { + "key_as_string": "2020-03-03T02:00:00.000+01:00", + "key": 1583197200000, + "doc_count": 63, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 815.5 + } + }, + { + "key_as_string": "2020-03-03T03:00:00.000+01:00", + "key": 1583200800000, + "doc_count": 25, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 352.5 + } + }, + { + "key_as_string": "2020-03-03T04:00:00.000+01:00", + "key": 1583204400000, + "doc_count": 19, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 249 + } + }, + { + "key_as_string": "2020-03-03T05:00:00.000+01:00", + "key": 1583208000000, + "doc_count": 24, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 423 + } + }, + { + "key_as_string": "2020-03-03T06:00:00.000+01:00", + "key": 1583211600000, + "doc_count": 50, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 843.5 + } + }, + { + "key_as_string": "2020-03-03T07:00:00.000+01:00", + "key": 1583215200000, + "doc_count": 141, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1723.5 + } + }, + { + "key_as_string": "2020-03-03T08:00:00.000+01:00", + "key": 1583218800000, + "doc_count": 359, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3864.5 + } + }, + { + "key_as_string": "2020-03-03T09:00:00.000+01:00", + "key": 1583222400000, + "doc_count": 391, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4957.5 + } + }, + { + "key_as_string": "2020-03-03T10:00:00.000+01:00", + "key": 1583226000000, + "doc_count": 257, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3681 + } + }, + { + "key_as_string": "2020-03-03T11:00:00.000+01:00", + "key": 1583229600000, + "doc_count": 313, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4419 + } + }, + { + "key_as_string": "2020-03-03T12:00:00.000+01:00", + "key": 1583233200000, + "doc_count": 320, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3939 + } + }, + { + "key_as_string": "2020-03-03T13:00:00.000+01:00", + "key": 1583236800000, + "doc_count": 338, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4119 + } + }, + { + "key_as_string": "2020-03-03T14:00:00.000+01:00", + "key": 1583240400000, + "doc_count": 362, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4355.119998931885 + } + }, + { + "key_as_string": "2020-03-03T15:00:00.000+01:00", + "key": 1583244000000, + "doc_count": 539, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6393 + } + }, + { + "key_as_string": "2020-03-03T16:00:00.000+01:00", + "key": 1583247600000, + "doc_count": 612, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7310 + } + }, + { + "key_as_string": "2020-03-03T17:00:00.000+01:00", + "key": 1583251200000, + "doc_count": 435, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5740.5 + } + }, + { + "key_as_string": "2020-03-03T18:00:00.000+01:00", + "key": 1583254800000, + "doc_count": 507, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6286 + } + }, + { + "key_as_string": "2020-03-03T19:00:00.000+01:00", + "key": 1583258400000, + "doc_count": 515, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6393.120002746582 + } + }, + { + "key_as_string": "2020-03-03T20:00:00.000+01:00", + "key": 1583262000000, + "doc_count": 441, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5055.5 + } + }, + { + "key_as_string": "2020-03-03T21:00:00.000+01:00", + "key": 1583265600000, + "doc_count": 416, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5161 + } + }, + { + "key_as_string": "2020-03-03T22:00:00.000+01:00", + "key": 1583269200000, + "doc_count": 529, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6326 + } + }, + { + "key_as_string": "2020-03-03T23:00:00.000+01:00", + "key": 1583272800000, + "doc_count": 488, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5673 + } + }, + { + "key_as_string": "2020-03-04T00:00:00.000+01:00", + "key": 1583276400000, + "doc_count": 236, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2959.5 + } + }, + { + "key_as_string": "2020-03-04T01:00:00.000+01:00", + "key": 1583280000000, + "doc_count": 133, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2016.5 + } + }, + { + "key_as_string": "2020-03-04T02:00:00.000+01:00", + "key": 1583283600000, + "doc_count": 66, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 771 + } + }, + { + "key_as_string": "2020-03-04T03:00:00.000+01:00", + "key": 1583287200000, + "doc_count": 47, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 572.5 + } + }, + { + "key_as_string": "2020-03-04T04:00:00.000+01:00", + "key": 1583290800000, + "doc_count": 22, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 377 + } + }, + { + "key_as_string": "2020-03-04T05:00:00.000+01:00", + "key": 1583294400000, + "doc_count": 20, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 620 + } + }, + { + "key_as_string": "2020-03-04T06:00:00.000+01:00", + "key": 1583298000000, + "doc_count": 45, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 949 + } + }, + { + "key_as_string": "2020-03-04T07:00:00.000+01:00", + "key": 1583301600000, + "doc_count": 140, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1879 + } + }, + { + "key_as_string": "2020-03-04T08:00:00.000+01:00", + "key": 1583305200000, + "doc_count": 371, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4462 + } + }, + { + "key_as_string": "2020-03-04T09:00:00.000+01:00", + "key": 1583308800000, + "doc_count": 413, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5009 + } + }, + { + "key_as_string": "2020-03-04T10:00:00.000+01:00", + "key": 1583312400000, + "doc_count": 330, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4107.5 + } + }, + { + "key_as_string": "2020-03-04T11:00:00.000+01:00", + "key": 1583316000000, + "doc_count": 298, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3848.5 + } + }, + { + "key_as_string": "2020-03-04T12:00:00.000+01:00", + "key": 1583319600000, + "doc_count": 320, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4338.5 + } + }, + { + "key_as_string": "2020-03-04T13:00:00.000+01:00", + "key": 1583323200000, + "doc_count": 366, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5141.5099999997765 + } + }, + { + "key_as_string": "2020-03-04T14:00:00.000+01:00", + "key": 1583326800000, + "doc_count": 337, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4499.5 + } + }, + { + "key_as_string": "2020-03-04T15:00:00.000+01:00", + "key": 1583330400000, + "doc_count": 408, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5833.169998168945 + } + }, + { + "key_as_string": "2020-03-04T16:00:00.000+01:00", + "key": 1583334000000, + "doc_count": 439, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5644.5 + } + }, + { + "key_as_string": "2020-03-04T17:00:00.000+01:00", + "key": 1583337600000, + "doc_count": 448, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5573.300000190735 + } + }, + { + "key_as_string": "2020-03-04T18:00:00.000+01:00", + "key": 1583341200000, + "doc_count": 504, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 677270.125 + } + }, + { + "key_as_string": "2020-03-04T19:00:00.000+01:00", + "key": 1583344800000, + "doc_count": 542, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6407.5 + } + }, + { + "key_as_string": "2020-03-04T20:00:00.000+01:00", + "key": 1583348400000, + "doc_count": 465, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5632.5 + } + }, + { + "key_as_string": "2020-03-04T21:00:00.000+01:00", + "key": 1583352000000, + "doc_count": 562, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6483 + } + }, + { + "key_as_string": "2020-03-04T22:00:00.000+01:00", + "key": 1583355600000, + "doc_count": 510, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5914.5 + } + }, + { + "key_as_string": "2020-03-04T23:00:00.000+01:00", + "key": 1583359200000, + "doc_count": 423, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5279.5 + } + }, + { + "key_as_string": "2020-03-05T00:00:00.000+01:00", + "key": 1583362800000, + "doc_count": 262, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3039 + } + }, + { + "key_as_string": "2020-03-05T01:00:00.000+01:00", + "key": 1583366400000, + "doc_count": 133, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1649.5 + } + }, + { + "key_as_string": "2020-03-05T02:00:00.000+01:00", + "key": 1583370000000, + "doc_count": 68, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 813.5 + } + }, + { + "key_as_string": "2020-03-05T03:00:00.000+01:00", + "key": 1583373600000, + "doc_count": 50, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 581.5 + } + }, + { + "key_as_string": "2020-03-05T04:00:00.000+01:00", + "key": 1583377200000, + "doc_count": 35, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 349 + } + }, + { + "key_as_string": "2020-03-05T05:00:00.000+01:00", + "key": 1583380800000, + "doc_count": 36, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 409.5 + } + }, + { + "key_as_string": "2020-03-05T06:00:00.000+01:00", + "key": 1583384400000, + "doc_count": 43, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 739 + } + }, + { + "key_as_string": "2020-03-05T07:00:00.000+01:00", + "key": 1583388000000, + "doc_count": 130, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1656 + } + }, + { + "key_as_string": "2020-03-05T08:00:00.000+01:00", + "key": 1583391600000, + "doc_count": 348, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4051 + } + }, + { + "key_as_string": "2020-03-05T09:00:00.000+01:00", + "key": 1583395200000, + "doc_count": 433, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5041.5 + } + }, + { + "key_as_string": "2020-03-05T10:00:00.000+01:00", + "key": 1583398800000, + "doc_count": 344, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4465.5 + } + }, + { + "key_as_string": "2020-03-05T11:00:00.000+01:00", + "key": 1583402400000, + "doc_count": 417, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5765.5 + } + }, + { + "key_as_string": "2020-03-05T12:00:00.000+01:00", + "key": 1583406000000, + "doc_count": 366, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4946.5 + } + }, + { + "key_as_string": "2020-03-05T13:00:00.000+01:00", + "key": 1583409600000, + "doc_count": 364, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4919.920001983643 + } + }, + { + "key_as_string": "2020-03-05T14:00:00.000+01:00", + "key": 1583413200000, + "doc_count": 393, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5267.800000190735 + } + }, + { + "key_as_string": "2020-03-05T15:00:00.000+01:00", + "key": 1583416800000, + "doc_count": 449, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6294.5 + } + }, + { + "key_as_string": "2020-03-05T16:00:00.000+01:00", + "key": 1583420400000, + "doc_count": 483, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7091 + } + }, + { + "key_as_string": "2020-03-05T17:00:00.000+01:00", + "key": 1583424000000, + "doc_count": 463, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6043.5 + } + }, + { + "key_as_string": "2020-03-05T18:00:00.000+01:00", + "key": 1583427600000, + "doc_count": 527, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6298.110000610352 + } + }, + { + "key_as_string": "2020-03-05T19:00:00.000+01:00", + "key": 1583431200000, + "doc_count": 646, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7928 + } + }, + { + "key_as_string": "2020-03-05T20:00:00.000+01:00", + "key": 1583434800000, + "doc_count": 597, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7060 + } + }, + { + "key_as_string": "2020-03-05T21:00:00.000+01:00", + "key": 1583438400000, + "doc_count": 507, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6181.300003051758 + } + }, + { + "key_as_string": "2020-03-05T22:00:00.000+01:00", + "key": 1583442000000, + "doc_count": 543, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6751 + } + }, + { + "key_as_string": "2020-03-05T23:00:00.000+01:00", + "key": 1583445600000, + "doc_count": 518, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6538.300000190735 + } + }, + { + "key_as_string": "2020-03-06T00:00:00.000+01:00", + "key": 1583449200000, + "doc_count": 393, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4783 + } + }, + { + "key_as_string": "2020-03-06T01:00:00.000+01:00", + "key": 1583452800000, + "doc_count": 224, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2659.5 + } + }, + { + "key_as_string": "2020-03-06T02:00:00.000+01:00", + "key": 1583456400000, + "doc_count": 105, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1282 + } + }, + { + "key_as_string": "2020-03-06T03:00:00.000+01:00", + "key": 1583460000000, + "doc_count": 67, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 805 + } + }, + { + "key_as_string": "2020-03-06T04:00:00.000+01:00", + "key": 1583463600000, + "doc_count": 44, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 530.5 + } + }, + { + "key_as_string": "2020-03-06T05:00:00.000+01:00", + "key": 1583467200000, + "doc_count": 47, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 820.5 + } + }, + { + "key_as_string": "2020-03-06T06:00:00.000+01:00", + "key": 1583470800000, + "doc_count": 39, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 635 + } + }, + { + "key_as_string": "2020-03-06T07:00:00.000+01:00", + "key": 1583474400000, + "doc_count": 126, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1468 + } + }, + { + "key_as_string": "2020-03-06T08:00:00.000+01:00", + "key": 1583478000000, + "doc_count": 322, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3638.5 + } + }, + { + "key_as_string": "2020-03-06T09:00:00.000+01:00", + "key": 1583481600000, + "doc_count": 410, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5011 + } + }, + { + "key_as_string": "2020-03-06T10:00:00.000+01:00", + "key": 1583485200000, + "doc_count": 344, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4492.5 + } + }, + { + "key_as_string": "2020-03-06T11:00:00.000+01:00", + "key": 1583488800000, + "doc_count": 352, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4839.220001220703 + } + }, + { + "key_as_string": "2020-03-06T12:00:00.000+01:00", + "key": 1583492400000, + "doc_count": 389, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5064 + } + }, + { + "key_as_string": "2020-03-06T13:00:00.000+01:00", + "key": 1583496000000, + "doc_count": 382, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4906.5 + } + }, + { + "key_as_string": "2020-03-06T14:00:00.000+01:00", + "key": 1583499600000, + "doc_count": 500, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6624 + } + }, + { + "key_as_string": "2020-03-06T15:00:00.000+01:00", + "key": 1583503200000, + "doc_count": 631, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7864.5 + } + }, + { + "key_as_string": "2020-03-06T16:00:00.000+01:00", + "key": 1583506800000, + "doc_count": 614, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8318.419998168945 + } + }, + { + "key_as_string": "2020-03-06T17:00:00.000+01:00", + "key": 1583510400000, + "doc_count": 449, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6287 + } + }, + { + "key_as_string": "2020-03-06T18:00:00.000+01:00", + "key": 1583514000000, + "doc_count": 582, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7393.919998168945 + } + }, + { + "key_as_string": "2020-03-06T19:00:00.000+01:00", + "key": 1583517600000, + "doc_count": 656, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8463.08999633789 + } + }, + { + "key_as_string": "2020-03-06T20:00:00.000+01:00", + "key": 1583521200000, + "doc_count": 679, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7894.5 + } + }, + { + "key_as_string": "2020-03-06T21:00:00.000+01:00", + "key": 1583524800000, + "doc_count": 485, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5681 + } + }, + { + "key_as_string": "2020-03-06T22:00:00.000+01:00", + "key": 1583528400000, + "doc_count": 567, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7026.5 + } + }, + { + "key_as_string": "2020-03-06T23:00:00.000+01:00", + "key": 1583532000000, + "doc_count": 645, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7641.5 + } + }, + { + "key_as_string": "2020-03-07T00:00:00.000+01:00", + "key": 1583535600000, + "doc_count": 506, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5881.919998168945 + } + }, + { + "key_as_string": "2020-03-07T01:00:00.000+01:00", + "key": 1583539200000, + "doc_count": 400, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4550.5 + } + }, + { + "key_as_string": "2020-03-07T02:00:00.000+01:00", + "key": 1583542800000, + "doc_count": 375, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4003.5 + } + }, + { + "key_as_string": "2020-03-07T03:00:00.000+01:00", + "key": 1583546400000, + "doc_count": 277, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3105.5 + } + }, + { + "key_as_string": "2020-03-07T04:00:00.000+01:00", + "key": 1583550000000, + "doc_count": 159, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2426.2999992370605 + } + }, + { + "key_as_string": "2020-03-07T05:00:00.000+01:00", + "key": 1583553600000, + "doc_count": 90, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1088 + } + }, + { + "key_as_string": "2020-03-07T06:00:00.000+01:00", + "key": 1583557200000, + "doc_count": 34, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 670.5 + } + }, + { + "key_as_string": "2020-03-07T07:00:00.000+01:00", + "key": 1583560800000, + "doc_count": 47, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 875.5 + } + }, + { + "key_as_string": "2020-03-07T08:00:00.000+01:00", + "key": 1583564400000, + "doc_count": 101, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1864.5 + } + }, + { + "key_as_string": "2020-03-07T09:00:00.000+01:00", + "key": 1583568000000, + "doc_count": 168, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2360 + } + }, + { + "key_as_string": "2020-03-07T10:00:00.000+01:00", + "key": 1583571600000, + "doc_count": 256, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2770 + } + }, + { + "key_as_string": "2020-03-07T11:00:00.000+01:00", + "key": 1583575200000, + "doc_count": 404, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4132 + } + }, + { + "key_as_string": "2020-03-07T12:00:00.000+01:00", + "key": 1583578800000, + "doc_count": 420, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4599.25 + } + }, + { + "key_as_string": "2020-03-07T13:00:00.000+01:00", + "key": 1583582400000, + "doc_count": 473, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5015.5 + } + }, + { + "key_as_string": "2020-03-07T14:00:00.000+01:00", + "key": 1583586000000, + "doc_count": 515, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6450 + } + }, + { + "key_as_string": "2020-03-07T15:00:00.000+01:00", + "key": 1583589600000, + "doc_count": 514, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6220 + } + }, + { + "key_as_string": "2020-03-07T16:00:00.000+01:00", + "key": 1583593200000, + "doc_count": 524, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6845.000003814697 + } + }, + { + "key_as_string": "2020-03-07T17:00:00.000+01:00", + "key": 1583596800000, + "doc_count": 610, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7734.75 + } + }, + { + "key_as_string": "2020-03-07T18:00:00.000+01:00", + "key": 1583600400000, + "doc_count": 634, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7442.25 + } + }, + { + "key_as_string": "2020-03-07T19:00:00.000+01:00", + "key": 1583604000000, + "doc_count": 750, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8845.5 + } + }, + { + "key_as_string": "2020-03-07T20:00:00.000+01:00", + "key": 1583607600000, + "doc_count": 697, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7857 + } + }, + { + "key_as_string": "2020-03-07T21:00:00.000+01:00", + "key": 1583611200000, + "doc_count": 559, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6364.449999988079 + } + }, + { + "key_as_string": "2020-03-07T22:00:00.000+01:00", + "key": 1583614800000, + "doc_count": 615, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6751.5 + } + }, + { + "key_as_string": "2020-03-07T23:00:00.000+01:00", + "key": 1583618400000, + "doc_count": 628, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6885.0099999997765 + } + }, + { + "key_as_string": "2020-03-08T00:00:00.000+01:00", + "key": 1583622000000, + "doc_count": 560, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6540.800000190735 + } + } + ] + }, + "4+ passengers": { + "doc_count": 94064, + "buckets": [ + { + "key_as_string": "2020-03-01T23:00:00.000+01:00", + "key": 1583100000000, + "doc_count": 11, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 155.5 + } + }, + { + "key_as_string": "2020-03-02T00:00:00.000+01:00", + "key": 1583103600000, + "doc_count": 246, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4258.5 + } + }, + { + "key_as_string": "2020-03-02T01:00:00.000+01:00", + "key": 1583107200000, + "doc_count": 152, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2580.5 + } + }, + { + "key_as_string": "2020-03-02T02:00:00.000+01:00", + "key": 1583110800000, + "doc_count": 109, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1580 + } + }, + { + "key_as_string": "2020-03-02T03:00:00.000+01:00", + "key": 1583114400000, + "doc_count": 62, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 867.5 + } + }, + { + "key_as_string": "2020-03-02T04:00:00.000+01:00", + "key": 1583118000000, + "doc_count": 63, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 943 + } + }, + { + "key_as_string": "2020-03-02T05:00:00.000+01:00", + "key": 1583121600000, + "doc_count": 59, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 755 + } + }, + { + "key_as_string": "2020-03-02T06:00:00.000+01:00", + "key": 1583125200000, + "doc_count": 149, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2289 + } + }, + { + "key_as_string": "2020-03-02T07:00:00.000+01:00", + "key": 1583128800000, + "doc_count": 322, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4772.590000152588 + } + }, + { + "key_as_string": "2020-03-02T08:00:00.000+01:00", + "key": 1583132400000, + "doc_count": 624, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7278.300000190735 + } + }, + { + "key_as_string": "2020-03-02T09:00:00.000+01:00", + "key": 1583136000000, + "doc_count": 780, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10134 + } + }, + { + "key_as_string": "2020-03-02T10:00:00.000+01:00", + "key": 1583139600000, + "doc_count": 676, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8257.619998931885 + } + }, + { + "key_as_string": "2020-03-02T11:00:00.000+01:00", + "key": 1583143200000, + "doc_count": 637, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8238 + } + }, + { + "key_as_string": "2020-03-02T12:00:00.000+01:00", + "key": 1583146800000, + "doc_count": 597, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7483.5 + } + }, + { + "key_as_string": "2020-03-02T13:00:00.000+01:00", + "key": 1583150400000, + "doc_count": 608, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7913.5 + } + }, + { + "key_as_string": "2020-03-02T14:00:00.000+01:00", + "key": 1583154000000, + "doc_count": 675, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9187.5 + } + }, + { + "key_as_string": "2020-03-02T15:00:00.000+01:00", + "key": 1583157600000, + "doc_count": 750, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9867.5 + } + }, + { + "key_as_string": "2020-03-02T16:00:00.000+01:00", + "key": 1583161200000, + "doc_count": 794, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10114 + } + }, + { + "key_as_string": "2020-03-02T17:00:00.000+01:00", + "key": 1583164800000, + "doc_count": 746, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11402.120002746582 + } + }, + { + "key_as_string": "2020-03-02T18:00:00.000+01:00", + "key": 1583168400000, + "doc_count": 883, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11340.5 + } + }, + { + "key_as_string": "2020-03-02T19:00:00.000+01:00", + "key": 1583172000000, + "doc_count": 1027, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11498.5 + } + }, + { + "key_as_string": "2020-03-02T20:00:00.000+01:00", + "key": 1583175600000, + "doc_count": 906, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10424.5 + } + }, + { + "key_as_string": "2020-03-02T21:00:00.000+01:00", + "key": 1583179200000, + "doc_count": 823, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9765.5 + } + }, + { + "key_as_string": "2020-03-02T22:00:00.000+01:00", + "key": 1583182800000, + "doc_count": 762, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9859.919998168945 + } + }, + { + "key_as_string": "2020-03-02T23:00:00.000+01:00", + "key": 1583186400000, + "doc_count": 660, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8426 + } + }, + { + "key_as_string": "2020-03-03T00:00:00.000+01:00", + "key": 1583190000000, + "doc_count": 433, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6526 + } + }, + { + "key_as_string": "2020-03-03T01:00:00.000+01:00", + "key": 1583193600000, + "doc_count": 244, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3307 + } + }, + { + "key_as_string": "2020-03-03T02:00:00.000+01:00", + "key": 1583197200000, + "doc_count": 108, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1570.5 + } + }, + { + "key_as_string": "2020-03-03T03:00:00.000+01:00", + "key": 1583200800000, + "doc_count": 77, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 864.5 + } + }, + { + "key_as_string": "2020-03-03T04:00:00.000+01:00", + "key": 1583204400000, + "doc_count": 49, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 704 + } + }, + { + "key_as_string": "2020-03-03T05:00:00.000+01:00", + "key": 1583208000000, + "doc_count": 50, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 874 + } + }, + { + "key_as_string": "2020-03-03T06:00:00.000+01:00", + "key": 1583211600000, + "doc_count": 109, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1651 + } + }, + { + "key_as_string": "2020-03-03T07:00:00.000+01:00", + "key": 1583215200000, + "doc_count": 317, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4007 + } + }, + { + "key_as_string": "2020-03-03T08:00:00.000+01:00", + "key": 1583218800000, + "doc_count": 656, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7833.5 + } + }, + { + "key_as_string": "2020-03-03T09:00:00.000+01:00", + "key": 1583222400000, + "doc_count": 789, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9583.08999633789 + } + }, + { + "key_as_string": "2020-03-03T10:00:00.000+01:00", + "key": 1583226000000, + "doc_count": 712, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8791 + } + }, + { + "key_as_string": "2020-03-03T11:00:00.000+01:00", + "key": 1583229600000, + "doc_count": 670, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8371.25 + } + }, + { + "key_as_string": "2020-03-03T12:00:00.000+01:00", + "key": 1583233200000, + "doc_count": 685, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8353.300000190735 + } + }, + { + "key_as_string": "2020-03-03T13:00:00.000+01:00", + "key": 1583236800000, + "doc_count": 735, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8736.79999923706 + } + }, + { + "key_as_string": "2020-03-03T14:00:00.000+01:00", + "key": 1583240400000, + "doc_count": 742, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9291.5 + } + }, + { + "key_as_string": "2020-03-03T15:00:00.000+01:00", + "key": 1583244000000, + "doc_count": 1002, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 12065.5 + } + }, + { + "key_as_string": "2020-03-03T16:00:00.000+01:00", + "key": 1583247600000, + "doc_count": 1063, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 12769.060001373291 + } + }, + { + "key_as_string": "2020-03-03T17:00:00.000+01:00", + "key": 1583251200000, + "doc_count": 761, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9358.5 + } + }, + { + "key_as_string": "2020-03-03T18:00:00.000+01:00", + "key": 1583254800000, + "doc_count": 934, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11757.119998931885 + } + }, + { + "key_as_string": "2020-03-03T19:00:00.000+01:00", + "key": 1583258400000, + "doc_count": 1084, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 12915.699996948242 + } + }, + { + "key_as_string": "2020-03-03T20:00:00.000+01:00", + "key": 1583262000000, + "doc_count": 960, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10987.5 + } + }, + { + "key_as_string": "2020-03-03T21:00:00.000+01:00", + "key": 1583265600000, + "doc_count": 855, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9995.800000190735 + } + }, + { + "key_as_string": "2020-03-03T22:00:00.000+01:00", + "key": 1583269200000, + "doc_count": 1048, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 12513.5 + } + }, + { + "key_as_string": "2020-03-03T23:00:00.000+01:00", + "key": 1583272800000, + "doc_count": 944, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11885.5 + } + }, + { + "key_as_string": "2020-03-04T00:00:00.000+01:00", + "key": 1583276400000, + "doc_count": 501, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6467.950000762939 + } + }, + { + "key_as_string": "2020-03-04T01:00:00.000+01:00", + "key": 1583280000000, + "doc_count": 256, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4048.5 + } + }, + { + "key_as_string": "2020-03-04T02:00:00.000+01:00", + "key": 1583283600000, + "doc_count": 136, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1891 + } + }, + { + "key_as_string": "2020-03-04T03:00:00.000+01:00", + "key": 1583287200000, + "doc_count": 86, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 969.5 + } + }, + { + "key_as_string": "2020-03-04T04:00:00.000+01:00", + "key": 1583290800000, + "doc_count": 35, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 423.5 + } + }, + { + "key_as_string": "2020-03-04T05:00:00.000+01:00", + "key": 1583294400000, + "doc_count": 71, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 957 + } + }, + { + "key_as_string": "2020-03-04T06:00:00.000+01:00", + "key": 1583298000000, + "doc_count": 96, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1486.5 + } + }, + { + "key_as_string": "2020-03-04T07:00:00.000+01:00", + "key": 1583301600000, + "doc_count": 334, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4366.5 + } + }, + { + "key_as_string": "2020-03-04T08:00:00.000+01:00", + "key": 1583305200000, + "doc_count": 620, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7661.5 + } + }, + { + "key_as_string": "2020-03-04T09:00:00.000+01:00", + "key": 1583308800000, + "doc_count": 792, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10021.5 + } + }, + { + "key_as_string": "2020-03-04T10:00:00.000+01:00", + "key": 1583312400000, + "doc_count": 721, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9184.5 + } + }, + { + "key_as_string": "2020-03-04T11:00:00.000+01:00", + "key": 1583316000000, + "doc_count": 698, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8309.5 + } + }, + { + "key_as_string": "2020-03-04T12:00:00.000+01:00", + "key": 1583319600000, + "doc_count": 728, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8765 + } + }, + { + "key_as_string": "2020-03-04T13:00:00.000+01:00", + "key": 1583323200000, + "doc_count": 715, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9193.5 + } + }, + { + "key_as_string": "2020-03-04T14:00:00.000+01:00", + "key": 1583326800000, + "doc_count": 716, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9372.620002746582 + } + }, + { + "key_as_string": "2020-03-04T15:00:00.000+01:00", + "key": 1583330400000, + "doc_count": 859, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11818.5 + } + }, + { + "key_as_string": "2020-03-04T16:00:00.000+01:00", + "key": 1583334000000, + "doc_count": 847, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11149 + } + }, + { + "key_as_string": "2020-03-04T17:00:00.000+01:00", + "key": 1583337600000, + "doc_count": 783, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9889.310001373291 + } + }, + { + "key_as_string": "2020-03-04T18:00:00.000+01:00", + "key": 1583341200000, + "doc_count": 931, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11584.620002746582 + } + }, + { + "key_as_string": "2020-03-04T19:00:00.000+01:00", + "key": 1583344800000, + "doc_count": 1201, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 14476.050000190735 + } + }, + { + "key_as_string": "2020-03-04T20:00:00.000+01:00", + "key": 1583348400000, + "doc_count": 1120, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 13277.419998168945 + } + }, + { + "key_as_string": "2020-03-04T21:00:00.000+01:00", + "key": 1583352000000, + "doc_count": 1139, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 13635.66999893263 + } + }, + { + "key_as_string": "2020-03-04T22:00:00.000+01:00", + "key": 1583355600000, + "doc_count": 1049, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 13240.5 + } + }, + { + "key_as_string": "2020-03-04T23:00:00.000+01:00", + "key": 1583359200000, + "doc_count": 821, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10683.04999923706 + } + }, + { + "key_as_string": "2020-03-05T00:00:00.000+01:00", + "key": 1583362800000, + "doc_count": 596, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7328 + } + }, + { + "key_as_string": "2020-03-05T01:00:00.000+01:00", + "key": 1583366400000, + "doc_count": 349, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4482.5 + } + }, + { + "key_as_string": "2020-03-05T02:00:00.000+01:00", + "key": 1583370000000, + "doc_count": 177, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2264 + } + }, + { + "key_as_string": "2020-03-05T03:00:00.000+01:00", + "key": 1583373600000, + "doc_count": 105, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1305 + } + }, + { + "key_as_string": "2020-03-05T04:00:00.000+01:00", + "key": 1583377200000, + "doc_count": 62, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1698 + } + }, + { + "key_as_string": "2020-03-05T05:00:00.000+01:00", + "key": 1583380800000, + "doc_count": 70, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 957.5 + } + }, + { + "key_as_string": "2020-03-05T06:00:00.000+01:00", + "key": 1583384400000, + "doc_count": 139, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2087 + } + }, + { + "key_as_string": "2020-03-05T07:00:00.000+01:00", + "key": 1583388000000, + "doc_count": 329, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4332 + } + }, + { + "key_as_string": "2020-03-05T08:00:00.000+01:00", + "key": 1583391600000, + "doc_count": 631, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7408 + } + }, + { + "key_as_string": "2020-03-05T09:00:00.000+01:00", + "key": 1583395200000, + "doc_count": 822, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9748 + } + }, + { + "key_as_string": "2020-03-05T10:00:00.000+01:00", + "key": 1583398800000, + "doc_count": 848, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10645.5 + } + }, + { + "key_as_string": "2020-03-05T11:00:00.000+01:00", + "key": 1583402400000, + "doc_count": 935, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11799 + } + }, + { + "key_as_string": "2020-03-05T12:00:00.000+01:00", + "key": 1583406000000, + "doc_count": 802, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10042.5 + } + }, + { + "key_as_string": "2020-03-05T13:00:00.000+01:00", + "key": 1583409600000, + "doc_count": 774, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10054 + } + }, + { + "key_as_string": "2020-03-05T14:00:00.000+01:00", + "key": 1583413200000, + "doc_count": 760, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10995 + } + }, + { + "key_as_string": "2020-03-05T15:00:00.000+01:00", + "key": 1583416800000, + "doc_count": 805, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11309 + } + }, + { + "key_as_string": "2020-03-05T16:00:00.000+01:00", + "key": 1583420400000, + "doc_count": 855, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 12082.039999008179 + } + }, + { + "key_as_string": "2020-03-05T17:00:00.000+01:00", + "key": 1583424000000, + "doc_count": 803, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11004 + } + }, + { + "key_as_string": "2020-03-05T18:00:00.000+01:00", + "key": 1583427600000, + "doc_count": 1016, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 12537.5 + } + }, + { + "key_as_string": "2020-03-05T19:00:00.000+01:00", + "key": 1583431200000, + "doc_count": 1238, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 15542.800003051758 + } + }, + { + "key_as_string": "2020-03-05T20:00:00.000+01:00", + "key": 1583434800000, + "doc_count": 1096, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 13037.5 + } + }, + { + "key_as_string": "2020-03-05T21:00:00.000+01:00", + "key": 1583438400000, + "doc_count": 1050, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 13084.949996948242 + } + }, + { + "key_as_string": "2020-03-05T22:00:00.000+01:00", + "key": 1583442000000, + "doc_count": 1048, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 13202.120002746582 + } + }, + { + "key_as_string": "2020-03-05T23:00:00.000+01:00", + "key": 1583445600000, + "doc_count": 1019, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 12844.41999912262 + } + }, + { + "key_as_string": "2020-03-06T00:00:00.000+01:00", + "key": 1583449200000, + "doc_count": 706, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8591.60000000149 + } + }, + { + "key_as_string": "2020-03-06T01:00:00.000+01:00", + "key": 1583452800000, + "doc_count": 455, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5492.5099999997765 + } + }, + { + "key_as_string": "2020-03-06T02:00:00.000+01:00", + "key": 1583456400000, + "doc_count": 257, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3021.5 + } + }, + { + "key_as_string": "2020-03-06T03:00:00.000+01:00", + "key": 1583460000000, + "doc_count": 163, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1778.3000001907349 + } + }, + { + "key_as_string": "2020-03-06T04:00:00.000+01:00", + "key": 1583463600000, + "doc_count": 117, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1572.5 + } + }, + { + "key_as_string": "2020-03-06T05:00:00.000+01:00", + "key": 1583467200000, + "doc_count": 111, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1675.5 + } + }, + { + "key_as_string": "2020-03-06T06:00:00.000+01:00", + "key": 1583470800000, + "doc_count": 138, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2167 + } + }, + { + "key_as_string": "2020-03-06T07:00:00.000+01:00", + "key": 1583474400000, + "doc_count": 325, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4342 + } + }, + { + "key_as_string": "2020-03-06T08:00:00.000+01:00", + "key": 1583478000000, + "doc_count": 590, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6865 + } + }, + { + "key_as_string": "2020-03-06T09:00:00.000+01:00", + "key": 1583481600000, + "doc_count": 822, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9504 + } + }, + { + "key_as_string": "2020-03-06T10:00:00.000+01:00", + "key": 1583485200000, + "doc_count": 770, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9356.5 + } + }, + { + "key_as_string": "2020-03-06T11:00:00.000+01:00", + "key": 1583488800000, + "doc_count": 656, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8131.800000190735 + } + }, + { + "key_as_string": "2020-03-06T12:00:00.000+01:00", + "key": 1583492400000, + "doc_count": 777, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9553 + } + }, + { + "key_as_string": "2020-03-06T13:00:00.000+01:00", + "key": 1583496000000, + "doc_count": 750, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9190 + } + }, + { + "key_as_string": "2020-03-06T14:00:00.000+01:00", + "key": 1583499600000, + "doc_count": 889, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10510.5 + } + }, + { + "key_as_string": "2020-03-06T15:00:00.000+01:00", + "key": 1583503200000, + "doc_count": 1056, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 13316.5 + } + }, + { + "key_as_string": "2020-03-06T16:00:00.000+01:00", + "key": 1583506800000, + "doc_count": 1042, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 14399 + } + }, + { + "key_as_string": "2020-03-06T17:00:00.000+01:00", + "key": 1583510400000, + "doc_count": 785, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10722.5 + } + }, + { + "key_as_string": "2020-03-06T18:00:00.000+01:00", + "key": 1583514000000, + "doc_count": 1124, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 14127.599996337667 + } + }, + { + "key_as_string": "2020-03-06T19:00:00.000+01:00", + "key": 1583517600000, + "doc_count": 1305, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 15875.009999999776 + } + }, + { + "key_as_string": "2020-03-06T20:00:00.000+01:00", + "key": 1583521200000, + "doc_count": 1175, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 13619.5 + } + }, + { + "key_as_string": "2020-03-06T21:00:00.000+01:00", + "key": 1583524800000, + "doc_count": 891, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10442.919998168945 + } + }, + { + "key_as_string": "2020-03-06T22:00:00.000+01:00", + "key": 1583528400000, + "doc_count": 989, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11409.5 + } + }, + { + "key_as_string": "2020-03-06T23:00:00.000+01:00", + "key": 1583532000000, + "doc_count": 1086, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 12667 + } + }, + { + "key_as_string": "2020-03-07T00:00:00.000+01:00", + "key": 1583535600000, + "doc_count": 932, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10899.5 + } + }, + { + "key_as_string": "2020-03-07T01:00:00.000+01:00", + "key": 1583539200000, + "doc_count": 734, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8533 + } + }, + { + "key_as_string": "2020-03-07T02:00:00.000+01:00", + "key": 1583542800000, + "doc_count": 579, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 6337 + } + }, + { + "key_as_string": "2020-03-07T03:00:00.000+01:00", + "key": 1583546400000, + "doc_count": 486, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5327.5 + } + }, + { + "key_as_string": "2020-03-07T04:00:00.000+01:00", + "key": 1583550000000, + "doc_count": 320, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 3741.800000190735 + } + }, + { + "key_as_string": "2020-03-07T05:00:00.000+01:00", + "key": 1583553600000, + "doc_count": 183, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2784 + } + }, + { + "key_as_string": "2020-03-07T06:00:00.000+01:00", + "key": 1583557200000, + "doc_count": 94, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1675.5 + } + }, + { + "key_as_string": "2020-03-07T07:00:00.000+01:00", + "key": 1583560800000, + "doc_count": 110, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 1871.5 + } + }, + { + "key_as_string": "2020-03-07T08:00:00.000+01:00", + "key": 1583564400000, + "doc_count": 178, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 2458.599998474121 + } + }, + { + "key_as_string": "2020-03-07T09:00:00.000+01:00", + "key": 1583568000000, + "doc_count": 323, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 4065 + } + }, + { + "key_as_string": "2020-03-07T10:00:00.000+01:00", + "key": 1583571600000, + "doc_count": 527, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 5719.330001831055 + } + }, + { + "key_as_string": "2020-03-07T11:00:00.000+01:00", + "key": 1583575200000, + "doc_count": 646, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 7408.5 + } + }, + { + "key_as_string": "2020-03-07T12:00:00.000+01:00", + "key": 1583578800000, + "doc_count": 795, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 8656 + } + }, + { + "key_as_string": "2020-03-07T13:00:00.000+01:00", + "key": 1583582400000, + "doc_count": 816, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 9640 + } + }, + { + "key_as_string": "2020-03-07T14:00:00.000+01:00", + "key": 1583586000000, + "doc_count": 879, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10915.79999923706 + } + }, + { + "key_as_string": "2020-03-07T15:00:00.000+01:00", + "key": 1583589600000, + "doc_count": 830, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10722 + } + }, + { + "key_as_string": "2020-03-07T16:00:00.000+01:00", + "key": 1583593200000, + "doc_count": 876, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11354.590000152588 + } + }, + { + "key_as_string": "2020-03-07T17:00:00.000+01:00", + "key": 1583596800000, + "doc_count": 877, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10905 + } + }, + { + "key_as_string": "2020-03-07T18:00:00.000+01:00", + "key": 1583600400000, + "doc_count": 988, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11761.919998168945 + } + }, + { + "key_as_string": "2020-03-07T19:00:00.000+01:00", + "key": 1583604000000, + "doc_count": 1119, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 12703.5 + } + }, + { + "key_as_string": "2020-03-07T20:00:00.000+01:00", + "key": 1583607600000, + "doc_count": 1132, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 13288.5 + } + }, + { + "key_as_string": "2020-03-07T21:00:00.000+01:00", + "key": 1583611200000, + "doc_count": 886, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10278.199996948242 + } + }, + { + "key_as_string": "2020-03-07T22:00:00.000+01:00", + "key": 1583614800000, + "doc_count": 947, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 10699 + } + }, + { + "key_as_string": "2020-03-07T23:00:00.000+01:00", + "key": 1583618400000, + "doc_count": 1118, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 12548 + } + }, + { + "key_as_string": "2020-03-08T00:00:00.000+01:00", + "key": 1583622000000, + "doc_count": 978, + "ffbd09b8-04af-4fcc-ba5f-b0fd50c4862b": { + "value": 11042.099999427795 + } + } + ] + } +} diff --git a/packages/osd-charts/src/utils/data_samples/babynames.ts b/packages/osd-charts/src/utils/data_samples/babynames.ts new file mode 100644 index 000000000000..85ece43f4bea --- /dev/null +++ b/packages/osd-charts/src/utils/data_samples/babynames.ts @@ -0,0 +1,1186 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * data taken from https://www.data-to-viz.com/graph/streamgraph.html + * @internal + */ +export const BABYNAME_DATA = [ + [1880, 'F', 'Helen', 636, 0.00651612638826278], + [1880, 'F', 'Amanda', 241, 0.00246916109995492], + [1880, 'F', 'Betty', 117, 0.00119872136387853], + [1880, 'F', 'Dorothy', 112, 0.00114749395516577], + [1880, 'F', 'Linda', 27, 0.000276628007048891], + [1880, 'F', 'Deborah', 12, 0.000122945780910618], + [1880, 'F', 'Jessica', 7, 7.17183721978607e-5], + [1881, 'F', 'Helen', 612, 0.00619088564058469], + [1881, 'F', 'Amanda', 263, 0.0026604622932578], + [1881, 'F', 'Betty', 112, 0.00113297253553184], + [1881, 'F', 'Dorothy', 109, 0.00110262505690152], + [1881, 'F', 'Linda', 38, 0.000384401395984017], + [1881, 'F', 'Deborah', 14, 0.00014162156694148], + [1881, 'F', 'Jessica', 7, 7.081078347074e-5], + [1882, 'F', 'Helen', 838, 0.00724311990042871], + [1882, 'F', 'Amanda', 288, 0.00248928225694925], + [1882, 'F', 'Betty', 123, 0.00106313096390541], + [1882, 'F', 'Dorothy', 115, 0.000993984234545706], + [1882, 'F', 'Linda', 36, 0.000311160282118656], + [1882, 'F', 'Deborah', 15, 0.00012965011754944], + [1882, 'F', 'Jessica', 8, 6.91467293597013e-5], + [1883, 'F', 'Helen', 862, 0.0071798032633955], + [1883, 'F', 'Amanda', 287, 0.00239049134175697], + [1883, 'F', 'Dorothy', 141, 0.00117442257556701], + [1883, 'F', 'Betty', 120, 0.000999508574950649], + [1883, 'F', 'Linda', 49, 0.000408132668104848], + [1883, 'F', 'Deborah', 16, 0.00013326780999342], + [1883, 'F', 'Jessica', 6, 4.99754287475325e-5], + [1884, 'F', 'Helen', 986, 0.00716642681668193], + [1884, 'F', 'Amanda', 337, 0.00244937711685782], + [1884, 'F', 'Dorothy', 163, 0.00118471356097277], + [1884, 'F', 'Betty', 144, 0.00104661811521521], + [1884, 'F', 'Linda', 33, 0.000239849984736819], + [1884, 'F', 'Jessica', 14, 0.000101754538979257], + [1884, 'F', 'Deborah', 13, 9.44863576235954e-5], + [1884, 'F', 'Patricia', 6, 4.36090881339671e-5], + [1885, 'F', 'Helen', 1134, 0.00798878470436565], + [1885, 'F', 'Amanda', 339, 0.0023881816708818], + [1885, 'F', 'Dorothy', 196, 0.00138077760322369], + [1885, 'F', 'Betty', 155, 0.0010919414719371], + [1885, 'F', 'Linda', 60, 0.000422687021395008], + [1885, 'F', 'Deborah', 10, 7.0447836899168e-5], + [1885, 'F', 'Jessica', 6, 4.22687021395008e-5], + [1885, 'F', 'Patricia', 5, 3.5223918449584e-5], + [1886, 'F', 'Helen', 1267, 0.00824140084300359], + [1886, 'F', 'Amanda', 370, 0.0024067232138211], + [1886, 'F', 'Dorothy', 230, 0.00149607118696987], + [1886, 'F', 'Betty', 167, 0.00108627777488682], + [1886, 'F', 'Linda', 49, 0.000318728209397929], + [1886, 'F', 'Jessica', 11, 7.15512306811677e-5], + [1886, 'F', 'Deborah', 10, 6.50465733465161e-5], + [1886, 'F', 'Patricia', 8, 5.20372586772129e-5], + [1887, 'F', 'Helen', 1405, 0.00903990426065808], + [1887, 'F', 'Amanda', 338, 0.00217472429900529], + [1887, 'F', 'Dorothy', 272, 0.00175007399209893], + [1887, 'F', 'Betty', 178, 0.00114526900953533], + [1887, 'F', 'Linda', 50, 0.000321704777959362], + [1887, 'F', 'Deborah', 15, 9.65114333878087e-5], + [1887, 'F', 'Patricia', 10, 6.43409555918724e-5], + [1887, 'F', 'Jessica', 8, 5.1472764473498e-5], + [1888, 'F', 'Helen', 1847, 0.0097494286000834], + [1888, 'F', 'Amanda', 404, 0.0021325225524817], + [1888, 'F', 'Dorothy', 373, 0.00196888839622691], + [1888, 'F', 'Betty', 214, 0.00112960353027496], + [1888, 'F', 'Linda', 77, 0.000406446130052205], + [1888, 'F', 'Deborah', 26, 0.000137241550407238], + [1888, 'F', 'Jessica', 18, 9.50133810511647e-5], + [1888, 'F', 'Patricia', 12, 6.33422540341098e-5], + [1889, 'F', 'Helen', 1909, 0.010088838858677], + [1889, 'F', 'Amanda', 413, 0.00218265607576406], + [1889, 'F', 'Dorothy', 377, 0.00199240034034637], + [1889, 'F', 'Betty', 189, 0.000998842610942876], + [1889, 'F', 'Linda', 74, 0.000391081233914142], + [1889, 'F', 'Patricia', 17, 8.9842986169465e-5], + [1889, 'F', 'Deborah', 12, 6.34185784725635e-5], + [1889, 'F', 'Jessica', 9, 4.75639338544226e-5], + [1890, 'F', 'Helen', 2312, 0.0114647849608997], + [1890, 'F', 'Dorothy', 458, 0.00227113819727166], + [1890, 'F', 'Amanda', 392, 0.00194385627364736], + [1890, 'F', 'Betty', 216, 0.00107110447731589], + [1890, 'F', 'Linda', 54, 0.000267776119328973], + [1890, 'F', 'Deborah', 24, 0.000119011608590655], + [1890, 'F', 'Jessica', 18, 8.9258706442991e-5], + [1890, 'F', 'Patricia', 11, 5.45469872707167e-5], + [1891, 'F', 'Helen', 2417, 0.0122960619025574], + [1891, 'F', 'Dorothy', 566, 0.00287942533589056], + [1891, 'F', 'Amanda', 371, 0.00188739717246537], + [1891, 'F', 'Betty', 239, 0.00121587041568524], + [1891, 'F', 'Linda', 78, 0.000396811265370077], + [1891, 'F', 'Deborah', 15, 7.63098587250149e-5], + [1891, 'F', 'Jessica', 14, 7.12225348100139e-5], + [1891, 'F', 'Patricia', 12, 6.10478869800119e-5], + [1892, 'F', 'Helen', 2936, 0.0130538203321255], + [1892, 'F', 'Dorothy', 626, 0.00278327368116844], + [1892, 'F', 'Amanda', 455, 0.00202298646155214], + [1892, 'F', 'Betty', 255, 0.00113376164328746], + [1892, 'F', 'Linda', 83, 0.000369028299579841], + [1892, 'F', 'Patricia', 21, 9.33686059177912e-5], + [1892, 'F', 'Deborah', 16, 7.11379854611742e-5], + [1892, 'F', 'Jessica', 14, 6.22457372785274e-5], + [1893, 'F', 'Helen', 3249, 0.0144251260922071], + [1893, 'F', 'Dorothy', 821, 0.00364513035447894], + [1893, 'F', 'Amanda', 387, 0.00171822831569226], + [1893, 'F', 'Betty', 298, 0.00132308020174753], + [1893, 'F', 'Linda', 82, 0.000364069048803012], + [1893, 'F', 'Patricia', 28, 0.000124316260566882], + [1893, 'F', 'Deborah', 24, 0.000106556794771613], + [1893, 'F', 'Jessica', 15, 6.65979967322583e-5], + [1894, 'F', 'Helen', 3676, 0.0155781194378994], + [1894, 'F', 'Dorothy', 1052, 0.004458156052413], + [1894, 'F', 'Amanda', 418, 0.00177139660637703], + [1894, 'F', 'Betty', 298, 0.00126286169545539], + [1894, 'F', 'Linda', 94, 0.000398352346888614], + [1894, 'F', 'Patricia', 36, 0.00015256047327649], + [1894, 'F', 'Deborah', 18, 7.62802366382452e-5], + [1894, 'F', 'Jessica', 10, 4.23779092434696e-5], + [1895, 'F', 'Helen', 4023, 0.0162803967512049], + [1895, 'F', 'Dorothy', 1127, 0.00456077731509022], + [1895, 'F', 'Amanda', 431, 0.00174418369370354], + [1895, 'F', 'Betty', 350, 0.00141639047052491], + [1895, 'F', 'Linda', 96, 0.000388495671915405], + [1895, 'F', 'Patricia', 35, 0.000141639047052491], + [1895, 'F', 'Jessica', 19, 7.68897683999239e-5], + [1895, 'F', 'Deborah', 13, 5.26087889052111e-5], + [1896, 'F', 'Helen', 4392, 0.0174290555690039], + [1896, 'F', 'Dorothy', 1366, 0.00542078549800987], + [1896, 'F', 'Amanda', 367, 0.00145638966161758], + [1896, 'F', 'Betty', 342, 0.00135718055660276], + [1896, 'F', 'Linda', 104, 0.000412709876861659], + [1896, 'F', 'Patricia', 37, 0.000146829475421936], + [1896, 'F', 'Deborah', 18, 7.14305556106717e-5], + [1896, 'F', 'Jessica', 9, 3.57152778053359e-5], + [1897, 'F', 'Helen', 4519, 0.0182015909777464], + [1897, 'F', 'Dorothy', 1472, 0.00592890947538012], + [1897, 'F', 'Betty', 367, 0.00147819957708186], + [1897, 'F', 'Amanda', 354, 0.00142583828416071], + [1897, 'F', 'Linda', 81, 0.000326251132816433], + [1897, 'F', 'Patricia', 49, 0.000197361796395126], + [1897, 'F', 'Deborah', 23, 9.26392105528144e-5], + [1897, 'F', 'Jessica', 9, 3.62501258684926e-5], + [1898, 'F', 'Helen', 5230, 0.0190774258971497], + [1898, 'F', 'Dorothy', 1671, 0.00609529228950997], + [1898, 'F', 'Betty', 421, 0.00153567806934991], + [1898, 'F', 'Amanda', 371, 0.00135329350054351], + [1898, 'F', 'Linda', 102, 0.000372064520365061], + [1898, 'F', 'Patricia', 47, 0.000171441494678018], + [1898, 'F', 'Deborah', 21, 7.6601518898689e-5], + [1898, 'F', 'Jessica', 13, 4.74199878896646e-5], + [1899, 'F', 'Helen', 5048, 0.0203967837084327], + [1899, 'F', 'Dorothy', 1687, 0.0068164370277587], + [1899, 'F', 'Betty', 410, 0.00165663259121581], + [1899, 'F', 'Amanda', 326, 0.00131722493838135], + [1899, 'F', 'Linda', 98, 0.000395975594973534], + [1899, 'F', 'Patricia', 56, 0.000226271768556305], + [1899, 'F', 'Jessica', 14, 5.65679421390763e-5], + [1899, 'F', 'Deborah', 11, 4.44462402521314e-5], + [1900, 'F', 'Helen', 6343, 0.0199606639918181], + [1900, 'F', 'Dorothy', 2491, 0.00783887971048698], + [1900, 'F', 'Betty', 664, 0.00208952875462198], + [1900, 'F', 'Amanda', 377, 0.00118637400676579], + [1900, 'F', 'Linda', 126, 0.000396506962473448], + [1900, 'F', 'Patricia', 89, 0.000280072378255055], + [1900, 'F', 'Deborah', 26, 8.18188970183306e-5], + [1900, 'F', 'Jessica', 23, 7.2378255054677e-5], + [1901, 'F', 'Helen', 5247, 0.0206386292834891], + [1901, 'F', 'Dorothy', 2173, 0.00854731111740458], + [1901, 'F', 'Betty', 474, 0.00186443878032663], + [1901, 'F', 'Amanda', 317, 0.00124689260203279], + [1901, 'F', 'Linda', 86, 0.000338273702759684], + [1901, 'F', 'Patricia', 68, 0.000267472230089053], + [1901, 'F', 'Deborah', 21, 8.26017181157368e-5], + [1901, 'F', 'Jessica', 8, 3.14673211869474e-5], + [1902, 'F', 'Helen', 5967, 0.0212853998637335], + [1902, 'F', 'Dorothy', 2707, 0.00965637295644822], + [1902, 'F', 'Betty', 580, 0.00206896797736977], + [1902, 'F', 'Amanda', 301, 0.00107372303653155], + [1902, 'F', 'Linda', 91, 0.000324613941276981], + [1902, 'F', 'Patricia', 85, 0.000303210824269708], + [1902, 'F', 'Deborah', 21, 7.49109095254572e-5], + [1902, 'F', 'Jessica', 9, 3.21046755109102e-5], + [1903, 'F', 'Helen', 6129, 0.0220310713951934], + [1903, 'F', 'Dorothy', 3078, 0.0110640622865729], + [1903, 'F', 'Betty', 596, 0.00214235903924543], + [1903, 'F', 'Amanda', 247, 0.000887856850157082], + [1903, 'F', 'Linda', 90, 0.000323510593174645], + [1903, 'F', 'Patricia', 79, 0.000283970409564411], + [1903, 'F', 'Deborah', 21, 7.54858050740839e-5], + [1903, 'F', 'Jessica', 16, 5.75129943421592e-5], + [1904, 'F', 'Helen', 6488, 0.0221858992333418], + [1904, 'F', 'Dorothy', 3477, 0.0118896996970298], + [1904, 'F', 'Betty', 707, 0.00241760646701181], + [1904, 'F', 'Amanda', 294, 0.00100534130311382], + [1904, 'F', 'Patricia', 124, 0.000424021501993585], + [1904, 'F', 'Linda', 101, 0.000345372352430259], + [1904, 'F', 'Deborah', 23, 7.86491495633262e-5], + [1904, 'F', 'Jessica', 16, 5.471245187014e-5], + [1905, 'F', 'Helen', 6811, 0.0219801852389712], + [1905, 'F', 'Dorothy', 3937, 0.0127053280407913], + [1905, 'F', 'Betty', 807, 0.0026043179397812], + [1905, 'F', 'Amanda', 311, 0.00100364669054765], + [1905, 'F', 'Patricia', 121, 0.000390486332978346], + [1905, 'F', 'Linda', 106, 0.000342078936328138], + [1905, 'F', 'Deborah', 18, 5.80888759802498e-5], + [1905, 'F', 'Jessica', 17, 5.48617162035692e-5], + [1906, 'F', 'Helen', 7176, 0.0228942608018734], + [1906, 'F', 'Dorothy', 4326, 0.013801640500126], + [1906, 'F', 'Betty', 865, 0.00275969002140754], + [1906, 'F', 'Amanda', 260, 0.000829502202966427], + [1906, 'F', 'Patricia', 157, 0.000500891714868189], + [1906, 'F', 'Linda', 98, 0.000312658522656576], + [1906, 'F', 'Deborah', 25, 7.97598272083103e-5], + [1906, 'F', 'Jessica', 18, 5.74270755899834e-5], + [1907, 'F', 'Helen', 7579, 0.0224607551721379], + [1907, 'F', 'Dorothy', 4967, 0.0147199592215343], + [1907, 'F', 'Betty', 1018, 0.00301689520586309], + [1907, 'F', 'Amanda', 285, 0.000844612115590354], + [1907, 'F', 'Patricia', 177, 0.000524548577050852], + [1907, 'F', 'Linda', 102, 0.000302282230842864], + [1907, 'F', 'Deborah', 21, 6.22345769382366e-5], + [1907, 'F', 'Jessica', 17, 5.03803718071439e-5], + [1908, 'F', 'Helen', 8439, 0.0238031438540277], + [1908, 'F', 'Dorothy', 5703, 0.0160859496859249], + [1908, 'F', 'Betty', 1128, 0.00318165022720029], + [1908, 'F', 'Amanda', 260, 0.000733359094922052], + [1908, 'F', 'Patricia', 205, 0.000578225440227003], + [1908, 'F', 'Linda', 93, 0.000262316907029811], + [1908, 'F', 'Deborah', 25, 7.05152975886589e-5], + [1908, 'F', 'Jessica', 17, 4.7950402360288e-5], + [1909, 'F', 'Helen', 9250, 0.0251291775559769], + [1909, 'F', 'Dorothy', 6253, 0.0169873240278404], + [1909, 'F', 'Betty', 1082, 0.00293943460708833], + [1909, 'F', 'Amanda', 271, 0.000736216985694027], + [1909, 'F', 'Patricia', 233, 0.00063298360762623], + [1909, 'F', 'Linda', 105, 0.000285250123608387], + [1909, 'F', 'Deborah', 26, 7.06333639411244e-5], + [1909, 'F', 'Jessica', 18, 4.89000211900092e-5], + [1910, 'F', 'Helen', 10479, 0.0249781896712004], + [1910, 'F', 'Dorothy', 7318, 0.0174434957547327], + [1910, 'F', 'Betty', 1389, 0.00331087942106091], + [1910, 'F', 'Patricia', 316, 0.000753231027397587], + [1910, 'F', 'Amanda', 278, 0.000662652612710535], + [1910, 'F', 'Linda', 137, 0.000326559021371739], + [1910, 'F', 'Deborah', 33, 7.8660202228229e-5], + [1910, 'F', 'Jessica', 28, 6.67419897694064e-5], + [1911, 'F', 'Helen', 11802, 0.0267127194372267], + [1911, 'F', 'Dorothy', 8869, 0.0200741491856265], + [1911, 'F', 'Betty', 1456, 0.00329551936117625], + [1911, 'F', 'Patricia', 325, 0.000735607000262555], + [1911, 'F', 'Amanda', 280, 0.000633753723303124], + [1911, 'F', 'Linda', 130, 0.000294242800105022], + [1911, 'F', 'Deborah', 36, 8.14826215675446e-5], + [1911, 'F', 'Jessica', 21, 4.75315292477343e-5], + [1912, 'F', 'Helen', 16133, 0.0274974944947061], + [1912, 'F', 'Dorothy', 12645, 0.0215524588040388], + [1912, 'F', 'Betty', 2011, 0.00342759941913183], + [1912, 'F', 'Patricia', 504, 0.000859030386495497], + [1912, 'F', 'Amanda', 310, 0.000528371864709532], + [1912, 'F', 'Linda', 189, 0.000322136394935811], + [1912, 'F', 'Jessica', 36, 6.13593133211069e-5], + [1912, 'F', 'Deborah', 32, 5.45416118409839e-5], + [1913, 'F', 'Helen', 18889, 0.0288423952212843], + [1913, 'F', 'Dorothy', 14674, 0.0224063374173925], + [1913, 'F', 'Betty', 2239, 0.00341882168989653], + [1913, 'F', 'Patricia', 588, 0.000897841515703065], + [1913, 'F', 'Amanda', 346, 0.000528321708219831], + [1913, 'F', 'Linda', 238, 0.000363412042070288], + [1913, 'F', 'Deborah', 42, 6.41315368359332e-5], + [1913, 'F', 'Jessica', 25, 3.81735338309126e-5], + [1914, 'F', 'Helen', 23221, 0.0291498453442597], + [1914, 'F', 'Dorothy', 18782, 0.0235774684662971], + [1914, 'F', 'Betty', 2933, 0.00368186109102595], + [1914, 'F', 'Patricia', 656, 0.000823491604402667], + [1914, 'F', 'Amanda', 375, 0.000470745962882622], + [1914, 'F', 'Linda', 219, 0.000274915642323451], + [1914, 'F', 'Deborah', 62, 7.78299991965936e-5], + [1914, 'F', 'Jessica', 42, 5.27235478428537e-5], + [1915, 'F', 'Helen', 30866, 0.0301460519864594], + [1915, 'F', 'Dorothy', 25154, 0.0245672841206311], + [1915, 'F', 'Betty', 4182, 0.00408445504462428], + [1915, 'F', 'Patricia', 895, 0.000874124166652017], + [1915, 'F', 'Amanda', 412, 0.000402390119173889], + [1915, 'F', 'Linda', 290, 0.000283235763496184], + [1915, 'F', 'Deborah', 106, 0.000103527554933088], + [1915, 'F', 'Jessica', 55, 5.37171275596211e-5], + [1916, 'F', 'Helen', 32661, 0.0300826741628504], + [1916, 'F', 'Dorothy', 27415, 0.0252508040836026], + [1916, 'F', 'Betty', 5136, 0.00473055370320565], + [1916, 'F', 'Patricia', 1078, 0.000992900485213335], + [1916, 'F', 'Amanda', 421, 0.000387765402852332], + [1916, 'F', 'Linda', 297, 0.000273554215313878], + [1916, 'F', 'Deborah', 90, 8.28952167617813e-5], + [1916, 'F', 'Jessica', 53, 4.8816072093049e-5], + [1917, 'F', 'Helen', 34249, 0.0304787212268021], + [1917, 'F', 'Dorothy', 28853, 0.0256767363589279], + [1917, 'F', 'Betty', 6639, 0.00590815002554058], + [1917, 'F', 'Patricia', 1441, 0.00128236845711763], + [1917, 'F', 'Amanda', 445, 0.000396012465938478], + [1917, 'F', 'Linda', 291, 0.000258965455254151], + [1917, 'F', 'Deborah', 89, 7.92024931876957e-5], + [1917, 'F', 'Jessica', 55, 4.89453609586883e-5], + [1917, 'F', 'Ashley', 5, 4.44957826897167e-6], + [1918, 'F', 'Helen', 36150, 0.030065795437817], + [1918, 'F', 'Dorothy', 32034, 0.026642536405395], + [1918, 'F', 'Betty', 8802, 0.00732058454892574], + [1918, 'F', 'Patricia', 1760, 0.0014637842315507], + [1918, 'F', 'Amanda', 397, 0.000330183147685017], + [1918, 'F', 'Linda', 322, 0.00026780597872689], + [1918, 'F', 'Deborah', 92, 7.65159939219687e-5], + [1918, 'F', 'Jessica', 56, 4.65749528220679e-5], + [1919, 'F', 'Helen', 33705, 0.0286936534286809], + [1919, 'F', 'Dorothy', 31734, 0.0270157068062827], + [1919, 'F', 'Betty', 10107, 0.00860426510024262], + [1919, 'F', 'Patricia', 2144, 0.00182522453496786], + [1919, 'F', 'Amanda', 379, 0.000322649299791427], + [1919, 'F', 'Linda', 282, 0.000240071510662751], + [1919, 'F', 'Deborah', 70, 5.95922189588388e-5], + [1919, 'F', 'Jessica', 40, 3.40526965479079e-5], + [1920, 'F', 'Dorothy', 36645, 0.0294564957018998], + [1920, 'F', 'Helen', 35098, 0.0282129645557451], + [1920, 'F', 'Betty', 14017, 0.011267340708242], + [1920, 'F', 'Patricia', 2502, 0.00201119258414936], + [1920, 'F', 'Amanda', 379, 0.000304653073298404], + [1920, 'F', 'Linda', 349, 0.000280538054303807], + [1920, 'F', 'Deborah', 82, 6.59143852518974e-5], + [1920, 'F', 'Jessica', 43, 3.45648605589218e-5], + [1921, 'F', 'Dorothy', 39083, 0.0305412051909812], + [1921, 'F', 'Helen', 34819, 0.0272091247740648], + [1921, 'F', 'Betty', 17637, 0.013782341067813], + [1921, 'F', 'Patricia', 3382, 0.00264284614681315], + [1921, 'F', 'Amanda', 392, 0.000306326342268112], + [1921, 'F', 'Linda', 367, 0.000286790223501013], + [1921, 'F', 'Deborah', 94, 7.34558065642922e-5], + [1921, 'F', 'Jessica', 48, 3.75093480328301e-5], + [1922, 'F', 'Dorothy', 37711, 0.0302285073477132], + [1922, 'F', 'Helen', 32507, 0.0260570679205567], + [1922, 'F', 'Betty', 20893, 0.0167474796217489], + [1922, 'F', 'Patricia', 3902, 0.00312777798708008], + [1922, 'F', 'Linda', 365, 0.000292577899867819], + [1922, 'F', 'Amanda', 341, 0.000273339900972401], + [1922, 'F', 'Deborah', 71, 5.69124133989456e-5], + [1922, 'F', 'Jessica', 40, 3.20633314923637e-5], + [1923, 'F', 'Dorothy', 39045, 0.0311757435229189], + [1923, 'F', 'Helen', 31492, 0.0251449997444938], + [1923, 'F', 'Betty', 25990, 0.020751890745567], + [1923, 'F', 'Patricia', 4800, 0.00383259236547601], + [1923, 'F', 'Linda', 423, 0.000337747202207573], + [1923, 'F', 'Amanda', 362, 0.000289041340896316], + [1923, 'F', 'Deborah', 118, 9.42178956512852e-5], + [1923, 'F', 'Jessica', 52, 4.15197506259901e-5], + [1924, 'F', 'Dorothy', 39996, 0.0308678753677503], + [1924, 'F', 'Helen', 31193, 0.0240739483034863], + [1924, 'F', 'Betty', 30602, 0.0236178298330807], + [1924, 'F', 'Patricia', 6958, 0.00537000392061223], + [1924, 'F', 'Linda', 454, 0.000350385423966363], + [1924, 'F', 'Amanda', 341, 0.000263174955005572], + [1924, 'F', 'Deborah', 125, 9.64717576999898e-5], + [1924, 'F', 'Jessica', 41, 3.16427365255967e-5], + [1925, 'F', 'Dorothy', 38572, 0.030538871901736], + [1925, 'F', 'Betty', 32817, 0.0259824266099572], + [1925, 'F', 'Helen', 29168, 0.023093379021825], + [1925, 'F', 'Patricia', 8095, 0.00640910940694163], + [1925, 'F', 'Linda', 438, 0.000346780718992024], + [1925, 'F', 'Amanda', 310, 0.000245438408419012], + [1925, 'F', 'Deborah', 82, 6.49224177108356e-5], + [1925, 'F', 'Jessica', 34, 2.69190512459562e-5], + [1926, 'F', 'Dorothy', 36614, 0.0297646484033633], + [1926, 'F', 'Betty', 32959, 0.026793386320163], + [1926, 'F', 'Helen', 26884, 0.0218548316948713], + [1926, 'F', 'Patricia', 8590, 0.0069830755936224], + [1926, 'F', 'Linda', 479, 0.000389393854405719], + [1926, 'F', 'Amanda', 310, 0.000252008548780319], + [1926, 'F', 'Deborah', 116, 9.42999730919904e-5], + [1926, 'F', 'Jessica', 35, 2.84525780881006e-5], + [1927, 'F', 'Dorothy', 35987, 0.0291075004772119], + [1927, 'F', 'Betty', 35422, 0.0286505094035013], + [1927, 'F', 'Helen', 25316, 0.0204764354372717], + [1927, 'F', 'Patricia', 10553, 0.00853562265640418], + [1927, 'F', 'Linda', 516, 0.000417358219530423], + [1927, 'F', 'Amanda', 234, 0.000189267099554494], + [1927, 'F', 'Deborah', 91, 7.360387204897e-5], + [1927, 'F', 'Jessica', 39, 3.15445165924157e-5], + [1928, 'F', 'Betty', 36078, 0.0301810713703117], + [1928, 'F', 'Dorothy', 33728, 0.0282151775369442], + [1928, 'F', 'Helen', 22936, 0.0191871238136667], + [1928, 'F', 'Patricia', 12332, 0.0103163415970587], + [1928, 'F', 'Linda', 554, 0.000463449014334294], + [1928, 'F', 'Amanda', 247, 0.000206627990145434], + [1928, 'F', 'Deborah', 130, 0.000108751573760755], + [1928, 'F', 'Jessica', 36, 3.01158204260552e-5], + [1929, 'F', 'Betty', 36669, 0.0316800016587745], + [1929, 'F', 'Dorothy', 31477, 0.0271943988713422], + [1929, 'F', 'Helen', 20983, 0.0181281593391166], + [1929, 'F', 'Patricia', 13626, 0.0117721154818092], + [1929, 'F', 'Linda', 509, 0.000439748039060684], + [1929, 'F', 'Amanda', 209, 0.000180564518985625], + [1929, 'F', 'Deborah', 161, 0.000139095155773615], + [1929, 'F', 'Jessica', 42, 3.62856928105083e-5], + [1930, 'F', 'Betty', 38239, 0.0327853532062582], + [1930, 'F', 'Dorothy', 30404, 0.0260677810320111], + [1930, 'F', 'Helen', 19914, 0.0170738650003772], + [1930, 'F', 'Patricia', 15749, 0.0135028773672261], + [1930, 'F', 'Linda', 493, 0.000422688332087274], + [1930, 'F', 'Amanda', 196, 0.00016804647685417], + [1930, 'F', 'Deborah', 165, 0.000141467697351725], + [1930, 'F', 'Jessica', 38, 3.25804393900942e-5], + [1931, 'F', 'Betty', 36102, 0.0327140362625423], + [1931, 'F', 'Dorothy', 26521, 0.0240321576566086], + [1931, 'F', 'Helen', 17657, 0.0159999927507537], + [1931, 'F', 'Patricia', 16468, 0.0149225735186845], + [1931, 'F', 'Linda', 534, 0.000483887190853626], + [1931, 'F', 'Amanda', 209, 0.00018938655971612], + [1931, 'F', 'Deborah', 158, 0.000143172614522234], + [1931, 'F', 'Jessica', 39, 3.53400757365008e-5], + [1932, 'F', 'Betty', 34411, 0.0311083227035339], + [1932, 'F', 'Dorothy', 24968, 0.0225716370132177], + [1932, 'F', 'Patricia', 17991, 0.0162642711272349], + [1932, 'F', 'Helen', 16375, 0.0148033705579718], + [1932, 'F', 'Linda', 774, 0.000699713515228713], + [1932, 'F', 'Amanda', 213, 0.00019255682008232], + [1932, 'F', 'Deborah', 208, 0.000188036706934848], + [1932, 'F', 'Jessica', 44, 3.97769956977563e-5], + [1933, 'F', 'Betty', 31526, 0.0301434985810775], + [1933, 'F', 'Dorothy', 22050, 0.0210830471265862], + [1933, 'F', 'Patricia', 18625, 0.017808242754316], + [1933, 'F', 'Helen', 14645, 0.0140027766516488], + [1933, 'F', 'Linda', 786, 0.000751531747913687], + [1933, 'F', 'Deborah', 243, 0.000232343784660338], + [1933, 'F', 'Amanda', 199, 0.000190273305133363], + [1933, 'F', 'Jessica', 43, 4.11143322649981e-5], + [1934, 'F', 'Betty', 31078, 0.0287181174191951], + [1934, 'F', 'Dorothy', 21280, 0.0196641205573226], + [1934, 'F', 'Patricia', 20847, 0.0192640000591402], + [1934, 'F', 'Helen', 14099, 0.0130284039350419], + [1934, 'F', 'Linda', 1001, 0.000924989881479318], + [1934, 'F', 'Deborah', 280, 0.000258738428385823], + [1934, 'F', 'Amanda', 189, 0.000174648439160431], + [1934, 'F', 'Jessica', 46, 4.25070275205281e-5], + [1935, 'F', 'Betty', 28673, 0.0263859454097216], + [1935, 'F', 'Patricia', 22876, 0.0210513335609385], + [1935, 'F', 'Dorothy', 19400, 0.0178525909722944], + [1935, 'F', 'Helen', 12778, 0.0117587838888649], + [1935, 'F', 'Linda', 1197, 0.00110152326772353], + [1935, 'F', 'Deborah', 279, 0.00025674602480774], + [1935, 'F', 'Amanda', 211, 0.000194169932739903], + [1935, 'F', 'Jessica', 47, 4.32511224586515e-5], + [1936, 'F', 'Betty', 25863, 0.0240043659489396], + [1936, 'F', 'Patricia', 23912, 0.022193573776091], + [1936, 'F', 'Dorothy', 17668, 0.0163982963146528], + [1936, 'F', 'Helen', 12232, 0.0113529522594992], + [1936, 'F', 'Linda', 2439, 0.0022637222499116], + [1936, 'F', 'Deborah', 299, 0.00027751248574152], + [1936, 'F', 'Amanda', 192, 0.000178201997533016], + [1936, 'F', 'Jessica', 43, 3.99098223641651e-5], + [1937, 'F', 'Patricia', 26837, 0.0243591174914203], + [1937, 'F', 'Betty', 25328, 0.0229894447152324], + [1937, 'F', 'Dorothy', 16571, 0.0150409858013312], + [1937, 'F', 'Helen', 11452, 0.0103946273246542], + [1937, 'F', 'Linda', 4380, 0.00397559096070428], + [1937, 'F', 'Deborah', 323, 0.000293177141622713], + [1937, 'F', 'Amanda', 176, 0.00015974977376346], + [1937, 'F', 'Jessica', 72, 6.53521801759607e-5], + [1938, 'F', 'Patricia', 27555, 0.0241429070846355], + [1938, 'F', 'Betty', 25502, 0.0223441268906687], + [1938, 'F', 'Dorothy', 16348, 0.0143236525138676], + [1938, 'F', 'Helen', 10833, 0.00949156641073696], + [1938, 'F', 'Linda', 7047, 0.00617438091908643], + [1938, 'F', 'Deborah', 410, 0.000359230335862841], + [1938, 'F', 'Amanda', 192, 0.000168224937769916], + [1938, 'F', 'Jessica', 50, 4.38085775442489e-5], + [1938, 'F', 'Ashley', 7, 6.13320085619484e-6], + [1939, 'F', 'Patricia', 29704, 0.0261941652197148], + [1939, 'F', 'Betty', 23639, 0.0208458076901709], + [1939, 'F', 'Dorothy', 15170, 0.0133775076212993], + [1939, 'F', 'Linda', 10714, 0.00944803010247859], + [1939, 'F', 'Helen', 10417, 0.00918612372386778], + [1939, 'F', 'Deborah', 443, 0.000390654968769648], + [1939, 'F', 'Amanda', 185, 0.000163140336845113], + [1939, 'F', 'Jessica', 77, 6.79016537139118e-5], + [1940, 'F', 'Patricia', 32661, 0.027650413304301], + [1940, 'F', 'Betty', 22074, 0.0186875852937491], + [1940, 'F', 'Linda', 18368, 0.0155501298666116], + [1940, 'F', 'Dorothy', 14874, 0.0125921511125861], + [1940, 'F', 'Helen', 10201, 0.00863604501139508], + [1940, 'F', 'Deborah', 469, 0.000397049809856317], + [1940, 'F', 'Amanda', 218, 0.000184556201596326], + [1940, 'F', 'Jessica', 61, 5.16418729237427e-5], + [1941, 'F', 'Patricia', 36901, 0.0296201340816562], + [1941, 'F', 'Linda', 23715, 0.0190358385882897], + [1941, 'F', 'Betty', 20900, 0.0167762608684484], + [1941, 'F', 'Dorothy', 14561, 0.0116879968662908], + [1941, 'F', 'Helen', 9889, 0.00793782027407113], + [1941, 'F', 'Deborah', 608, 0.000488036679809409], + [1941, 'F', 'Amanda', 223, 0.000179000295390622], + [1941, 'F', 'Jessica', 81, 6.50180445140824e-5], + [1941, 'F', 'Ashley', 6, 4.81615144548759e-6], + [1942, 'F', 'Patricia', 39454, 0.0283767212206867], + [1942, 'F', 'Linda', 31611, 0.0227357564380576], + [1942, 'F', 'Betty', 21654, 0.0155743276046218], + [1942, 'F', 'Dorothy', 15032, 0.0108115494852071], + [1942, 'F', 'Helen', 10013, 0.00720170602683468], + [1942, 'F', 'Deborah', 676, 0.000486203263171901], + [1942, 'F', 'Amanda', 295, 0.000212174500940401], + [1942, 'F', 'Jessica', 127, 9.13429207438335e-5], + [1942, 'F', 'Ashley', 8, 5.7538847712651e-6], + [1943, 'F', 'Patricia', 39620, 0.0276053892520138], + [1943, 'F', 'Linda', 38437, 0.0267811293962558], + [1943, 'F', 'Betty', 21595, 0.0150464003255234], + [1943, 'F', 'Dorothy', 14785, 0.0103015063122419], + [1943, 'F', 'Helen', 9799, 0.00682749140031507], + [1943, 'F', 'Deborah', 788, 0.000549042067909815], + [1943, 'F', 'Amanda', 284, 0.000197878105693385], + [1943, 'F', 'Jessica', 121, 8.43072210876746e-5], + [1943, 'F', 'Ashley', 10, 6.96753893286567e-6], + [1944, 'F', 'Linda', 38411, 0.0281104982403037], + [1944, 'F', 'Patricia', 36872, 0.0269842048141543], + [1944, 'F', 'Betty', 19757, 0.0144588558937201], + [1944, 'F', 'Dorothy', 13378, 0.00979048307669114], + [1944, 'F', 'Helen', 8693, 0.00636183804647003], + [1944, 'F', 'Deborah', 1293, 0.000946262118265933], + [1944, 'F', 'Amanda', 238, 0.000174176631204402], + [1944, 'F', 'Jessica', 124, 9.07474885266633e-5], + [1944, 'F', 'Ashley', 12, 8.78201501870935e-6], + [1945, 'F', 'Linda', 41464, 0.0308041820109342], + [1945, 'F', 'Patricia', 35840, 0.0266260342290151], + [1945, 'F', 'Betty', 18383, 0.0136569862508924], + [1945, 'F', 'Dorothy', 12328, 0.00915864257743577], + [1945, 'F', 'Helen', 8300, 0.00616618538227749], + [1945, 'F', 'Deborah', 1464, 0.00108762595176557], + [1945, 'F', 'Amanda', 280, 0.00020801589241418], + [1945, 'F', 'Jessica', 132, 9.80646349952565e-5], + [1945, 'F', 'Ashley', 10, 7.42913901479216e-6], + [1946, 'F', 'Linda', 52708, 0.0326804234293612], + [1946, 'F', 'Patricia', 46295, 0.028704185373421], + [1946, 'F', 'Betty', 19714, 0.012223227356121], + [1946, 'F', 'Dorothy', 12796, 0.00793387527893499], + [1946, 'F', 'Helen', 8852, 0.00548848577439298], + [1946, 'F', 'Deborah', 2470, 0.00153146857916297], + [1946, 'F', 'Amanda', 274, 0.000169887607567067], + [1946, 'F', 'Jessica', 240, 0.000148806663562394], + [1946, 'F', 'Ashley', 7, 4.34019435390317e-6], + [1947, 'F', 'Linda', 99680, 0.0548360886157353], + [1947, 'F', 'Patricia', 51274, 0.0282069182151205], + [1947, 'F', 'Betty', 18962, 0.0104313996020423], + [1947, 'F', 'Dorothy', 12751, 0.00701459636776927], + [1947, 'F', 'Helen', 8978, 0.00493898880008098], + [1947, 'F', 'Deborah', 5838, 0.00321160799898338], + [1947, 'F', 'Jessica', 430, 0.000236552147921009], + [1947, 'F', 'Amanda', 310, 0.000170537595012821], + [1947, 'F', 'Ashley', 11, 6.05133401658396e-6], + [1948, 'F', 'Linda', 96211, 0.0552115905834257], + [1948, 'F', 'Patricia', 46135, 0.0264750052651604], + [1948, 'F', 'Betty', 16622, 0.00953869161195395], + [1948, 'F', 'Dorothy', 11326, 0.00649953201762667], + [1948, 'F', 'Deborah', 11246, 0.00645362326242535], + [1948, 'F', 'Helen', 8305, 0.00476590264933688], + [1948, 'F', 'Jessica', 482, 0.000276600250087944], + [1948, 'F', 'Amanda', 306, 0.000175600988645043], + [1948, 'F', 'Ashley', 13, 7.46017272021426e-6], + [1949, 'F', 'Linda', 91010, 0.0518428093664536], + [1949, 'F', 'Patricia', 46337, 0.0263953440019049], + [1949, 'F', 'Deborah', 19208, 0.0109416183090962], + [1949, 'F', 'Betty', 14946, 0.00851381857807951], + [1949, 'F', 'Dorothy', 10406, 0.00592765931510072], + [1949, 'F', 'Helen', 7709, 0.00439134399962632], + [1949, 'F', 'Jessica', 406, 0.000231273273297222], + [1949, 'F', 'Amanda', 333, 0.000189689655192056], + [1949, 'F', 'Ashley', 11, 6.26602464598385e-6], + [1950, 'F', 'Linda', 80439, 0.0457319621154536], + [1950, 'F', 'Patricia', 47952, 0.0272621371145866], + [1950, 'F', 'Deborah', 29073, 0.0165288645381293], + [1950, 'F', 'Betty', 13614, 0.00773996360272735], + [1950, 'F', 'Dorothy', 9555, 0.00543230147084324], + [1950, 'F', 'Helen', 7060, 0.0040138198204242], + [1950, 'F', 'Jessica', 402, 0.000228548947281945], + [1950, 'F', 'Amanda', 379, 0.000215472763730988], + [1950, 'F', 'Ashley', 15, 8.52794579410241e-6], + [1951, 'F', 'Linda', 73947, 0.0400434945769865], + [1951, 'F', 'Patricia', 56433, 0.0305593807654547], + [1951, 'F', 'Deborah', 42045, 0.0227680464317606], + [1951, 'F', 'Betty', 12820, 0.00694223701403664], + [1951, 'F', 'Dorothy', 9082, 0.00491804965378165], + [1951, 'F', 'Helen', 6945, 0.00376082964605963], + [1951, 'F', 'Jessica', 466, 0.000252346524847198], + [1951, 'F', 'Amanda', 409, 0.000221480104425974], + [1951, 'F', 'Ashley', 15, 8.12274221611151e-6], + [1952, 'F', 'Linda', 67092, 0.0352717589878795], + [1952, 'F', 'Patricia', 53090, 0.0279105956696256], + [1952, 'F', 'Deborah', 49809, 0.0261857008797962], + [1952, 'F', 'Betty', 12125, 0.00637438260490131], + [1952, 'F', 'Dorothy', 8608, 0.00452541735777241], + [1952, 'F', 'Helen', 6470, 0.00340142313020301], + [1952, 'F', 'Jessica', 452, 0.000237626469065187], + [1952, 'F', 'Amanda', 418, 0.000219751911657629], + [1952, 'F', 'Ashley', 24, 1.26173346406294e-5], + [1953, 'F', 'Linda', 61264, 0.0317575840515387], + [1953, 'F', 'Deborah', 52196, 0.0270569805620611], + [1953, 'F', 'Patricia', 51007, 0.0264406354419697], + [1953, 'F', 'Betty', 11367, 0.00589234228770306], + [1953, 'F', 'Dorothy', 8154, 0.00422681085721217], + [1953, 'F', 'Helen', 6120, 0.00317244081998265], + [1953, 'F', 'Jessica', 495, 0.000256594478086832], + [1953, 'F', 'Amanda', 428, 0.000221863508325584], + [1953, 'F', 'Ashley', 15, 7.7755902450555e-6], + [1954, 'F', 'Linda', 55371, 0.0278157891564086], + [1954, 'F', 'Deborah', 54674, 0.0274656491003862], + [1954, 'F', 'Patricia', 49133, 0.0246821110079613], + [1954, 'F', 'Betty', 10615, 0.00533247732378461], + [1954, 'F', 'Dorothy', 7791, 0.00391383239091907], + [1954, 'F', 'Helen', 5940, 0.00298397694802455], + [1954, 'F', 'Amanda', 428, 0.000215007093224664], + [1954, 'F', 'Jessica', 424, 0.000212997681138453], + [1954, 'F', 'Ashley', 21, 1.0549413452612e-5], + [1955, 'F', 'Deborah', 52314, 0.026099932497594], + [1955, 'F', 'Linda', 51279, 0.0255835615426869], + [1955, 'F', 'Patricia', 46210, 0.0230545911364801], + [1955, 'F', 'Betty', 9928, 0.00495316989402671], + [1955, 'F', 'Dorothy', 7241, 0.00361260104780897], + [1955, 'F', 'Helen', 5596, 0.00279189552044455], + [1955, 'F', 'Amanda', 452, 0.000225506929099524], + [1955, 'F', 'Jessica', 386, 0.000192578926177912], + [1955, 'F', 'Ashley', 8, 3.9912730814075e-6], + [1956, 'F', 'Linda', 48067, 0.0233410866641092], + [1956, 'F', 'Deborah', 47829, 0.0232255150947153], + [1956, 'F', 'Patricia', 43332, 0.0210417951469653], + [1956, 'F', 'Betty', 9213, 0.00447378516313558], + [1956, 'F', 'Dorothy', 6862, 0.00333215171924849], + [1956, 'F', 'Helen', 5279, 0.00256345510433005], + [1956, 'F', 'Amanda', 643, 0.000312237475295363], + [1956, 'F', 'Jessica', 406, 0.00019715150073082], + [1956, 'F', 'Ashley', 25, 1.21398707346564e-5], + [1957, 'F', 'Linda', 44495, 0.021213187408344], + [1957, 'F', 'Deborah', 40065, 0.0191011653784763], + [1957, 'F', 'Patricia', 39277, 0.0187254829045404], + [1957, 'F', 'Betty', 8474, 0.00404001685803589], + [1957, 'F', 'Dorothy', 6401, 0.00305170496911585], + [1957, 'F', 'Helen', 5015, 0.0023909233588683], + [1957, 'F', 'Amanda', 667, 0.000317995190501527], + [1957, 'F', 'Jessica', 476, 0.000226935098468856], + [1957, 'F', 'Ashley', 27, 1.28723690307964e-5], + [1958, 'F', 'Linda', 41898, 0.0202894802981679], + [1958, 'F', 'Patricia', 37932, 0.0183689094150104], + [1958, 'F', 'Deborah', 32936, 0.015949551842581], + [1958, 'F', 'Betty', 7709, 0.00373315202679308], + [1958, 'F', 'Dorothy', 5539, 0.00268231016687078], + [1958, 'F', 'Helen', 4763, 0.0023065252436912], + [1958, 'F', 'Amanda', 796, 0.00038547010161205], + [1958, 'F', 'Jessica', 529, 0.000256172969538661], + [1958, 'F', 'Ashley', 38, 1.84018390216808e-5], + [1959, 'F', 'Linda', 40409, 0.0194416392387311], + [1959, 'F', 'Patricia', 35221, 0.0169455808267303], + [1959, 'F', 'Deborah', 29552, 0.0142181029667396], + [1959, 'F', 'Betty', 7317, 0.00352036611422691], + [1959, 'F', 'Dorothy', 5238, 0.00252011448767535], + [1959, 'F', 'Helen', 4378, 0.00210634998607153], + [1959, 'F', 'Amanda', 858, 0.000412802258576833], + [1959, 'F', 'Jessica', 523, 0.000251626551556741], + [1959, 'F', 'Ashley', 37, 1.78014959992341e-5], + [1960, 'F', 'Linda', 37314, 0.0179400268278259], + [1960, 'F', 'Patricia', 32107, 0.0154365771925017], + [1960, 'F', 'Deborah', 25269, 0.012148966551759], + [1960, 'F', 'Betty', 6503, 0.00312654752804181], + [1960, 'F', 'Dorothy', 5077, 0.00244094753188809], + [1960, 'F', 'Helen', 4069, 0.00195631583755223], + [1960, 'F', 'Amanda', 977, 0.000469727346593395], + [1960, 'F', 'Jessica', 560, 0.000269239830186593], + [1960, 'F', 'Ashley', 57, 2.74047684297068e-5], + [1961, 'F', 'Linda', 35574, 0.0171339811436622], + [1961, 'F', 'Patricia', 28867, 0.0139035990800612], + [1961, 'F', 'Deborah', 24092, 0.0116037520018302], + [1961, 'F', 'Betty', 5580, 0.00268756998880179], + [1961, 'F', 'Dorothy', 4726, 0.00227624655323965], + [1961, 'F', 'Helen', 3855, 0.00185673518043565], + [1961, 'F', 'Amanda', 1057, 0.0005090970390974], + [1961, 'F', 'Jessica', 669, 0.000322219412635914], + [1961, 'F', 'Ashley', 80, 3.85314693735024e-5], + [1962, 'F', 'Linda', 31462, 0.015521322454956], + [1962, 'F', 'Patricia', 26538, 0.0130921383036559], + [1962, 'F', 'Deborah', 22893, 0.0112939302956363], + [1962, 'F', 'Betty', 4765, 0.00235074380197906], + [1962, 'F', 'Dorothy', 4075, 0.00201034228605765], + [1962, 'F', 'Helen', 3587, 0.00176959454726105], + [1962, 'F', 'Amanda', 948, 0.000467682082744208], + [1962, 'F', 'Jessica', 867, 0.000427721904788216], + [1962, 'F', 'Ashley', 95, 4.6866875380485e-5], + [1963, 'F', 'Linda', 27715, 0.0139419183153997], + [1963, 'F', 'Patricia', 25363, 0.0127587542570263], + [1963, 'F', 'Deborah', 21062, 0.0105951536553833], + [1963, 'F', 'Betty', 4154, 0.00208965284799461], + [1963, 'F', 'Dorothy', 3791, 0.00190704717061809], + [1963, 'F', 'Helen', 3340, 0.00168017345024121], + [1963, 'F', 'Jessica', 1121, 0.000563914502311496], + [1963, 'F', 'Amanda', 1035, 0.000520652551197501], + [1963, 'F', 'Ashley', 108, 5.4328961864087e-5], + [1964, 'F', 'Patricia', 26087, 0.013328857510441], + [1964, 'F', 'Linda', 23633, 0.0120750139741731], + [1964, 'F', 'Deborah', 19306, 0.00986418227839823], + [1964, 'F', 'Betty', 4067, 0.00207798763732755], + [1964, 'F', 'Dorothy', 3535, 0.00180616825619692], + [1964, 'F', 'Helen', 3095, 0.00158135523420918], + [1964, 'F', 'Amanda', 1275, 0.000651446825078097], + [1964, 'F', 'Jessica', 1172, 0.000598820140385513], + [1964, 'F', 'Ashley', 180, 9.19689635404372e-5], + [1965, 'F', 'Patricia', 23551, 0.0128881183708359], + [1965, 'F', 'Linda', 19339, 0.0105831311270687], + [1965, 'F', 'Deborah', 17083, 0.00934855106488003], + [1965, 'F', 'Betty', 3565, 0.00195092106458452], + [1965, 'F', 'Dorothy', 2960, 0.00161983908868728], + [1965, 'F', 'Helen', 2804, 0.00153446919076998], + [1965, 'F', 'Amanda', 1650, 0.000902950843356088], + [1965, 'F', 'Jessica', 1529, 0.000836734448176641], + [1965, 'F', 'Ashley', 218, 0.000119298959910077], + [1966, 'F', 'Patricia', 20115, 0.0114579150183275], + [1966, 'F', 'Deborah', 16250, 0.00925633204314305], + [1966, 'F', 'Linda', 15560, 0.00886329394408036], + [1966, 'F', 'Betty', 2948, 0.00167924103773451], + [1966, 'F', 'Dorothy', 2667, 0.00151917769594231], + [1966, 'F', 'Helen', 2449, 0.00139500044145584], + [1966, 'F', 'Amanda', 2329, 0.00132664598944493], + [1966, 'F', 'Jessica', 1665, 0.000948418021651273], + [1966, 'F', 'Ashley', 263, 0.000149810173990561], + [1967, 'F', 'Patricia', 17746, 0.0103375520052381], + [1967, 'F', 'Deborah', 14007, 0.00815947768158289], + [1967, 'F', 'Linda', 13199, 0.00768879459693101], + [1967, 'F', 'Amanda', 2663, 0.00155127358221284], + [1967, 'F', 'Betty', 2543, 0.00148137015379919], + [1967, 'F', 'Dorothy', 2316, 0.00134913616838338], + [1967, 'F', 'Helen', 2153, 0.00125418401145484], + [1967, 'F', 'Jessica', 1761, 0.00102583281197026], + [1967, 'F', 'Ashley', 386, 0.000224856028063896], + [1968, 'F', 'Patricia', 15806, 0.00924608652923696], + [1968, 'F', 'Deborah', 12286, 0.0071869808362777], + [1968, 'F', 'Linda', 11368, 0.00664997543112525], + [1968, 'F', 'Amanda', 2430, 0.00142148489599176], + [1968, 'F', 'Betty', 2134, 0.00124833282635655], + [1968, 'F', 'Dorothy', 2084, 0.0012190841659452], + [1968, 'F', 'Helen', 1881, 0.00110033460467511], + [1968, 'F', 'Jessica', 1841, 0.00107693567634602], + [1968, 'F', 'Ashley', 544, 0.000318225425275522], + [1969, 'F', 'Patricia', 14957, 0.00848518199146773], + [1969, 'F', 'Deborah', 11186, 0.00634587455750204], + [1969, 'F', 'Linda', 10248, 0.00581374239811201], + [1969, 'F', 'Amanda', 2817, 0.00159809839339203], + [1969, 'F', 'Jessica', 2492, 0.00141372424434964], + [1969, 'F', 'Betty', 2133, 0.00121006172279205], + [1969, 'F', 'Helen', 1857, 0.00105348552237451], + [1969, 'F', 'Dorothy', 1778, 0.00100866842153036], + [1969, 'F', 'Ashley', 643, 0.000364777162566942], + [1970, 'F', 'Patricia', 13404, 0.00731676961673752], + [1970, 'F', 'Deborah', 9853, 0.00537840428481907], + [1970, 'F', 'Linda', 8734, 0.00476758175414693], + [1970, 'F', 'Jessica', 4023, 0.00219601344137086], + [1970, 'F', 'Amanda', 3550, 0.00193781946727978], + [1970, 'F', 'Betty', 1967, 0.0010737157442646], + [1970, 'F', 'Dorothy', 1802, 0.000983648078883991], + [1970, 'F', 'Helen', 1715, 0.000936157855319669], + [1970, 'F', 'Ashley', 932, 0.000508745843240776], + [1971, 'F', 'Patricia', 11466, 0.006543190995437], + [1971, 'F', 'Deborah', 8675, 0.00495047809919902], + [1971, 'F', 'Linda', 7379, 0.00421090235089217], + [1971, 'F', 'Jessica', 5360, 0.00305873920596043], + [1971, 'F', 'Amanda', 4133, 0.00235853901832733], + [1971, 'F', 'Betty', 1763, 0.00100607410822915], + [1971, 'F', 'Dorothy', 1626, 0.000927893647181281], + [1971, 'F', 'Helen', 1441, 0.000822321491751676], + [1971, 'F', 'Ashley', 1164, 0.000664248588757079], + [1972, 'F', 'Patricia', 9602, 0.00595464005496972], + [1972, 'F', 'Deborah', 6279, 0.00389389553271765], + [1972, 'F', 'Jessica', 6208, 0.00384986518030119], + [1972, 'F', 'Linda', 5746, 0.00356335781668986], + [1972, 'F', 'Amanda', 4181, 0.00259282962610169], + [1972, 'F', 'Betty', 1366, 0.000847119174660346], + [1972, 'F', 'Dorothy', 1277, 0.0007919261976876], + [1972, 'F', 'Helen', 1245, 0.000772081531809759], + [1972, 'F', 'Ashley', 1176, 0.000729291471010664], + [1973, 'F', 'Patricia', 8477, 0.00545491634229101], + [1973, 'F', 'Jessica', 7226, 0.00464990273556622], + [1973, 'F', 'Amanda', 5627, 0.00362095249004029], + [1973, 'F', 'Deborah', 4984, 0.00320718450512899], + [1973, 'F', 'Linda', 4735, 0.00304695397909024], + [1973, 'F', 'Betty', 1323, 0.000851345325097441], + [1973, 'F', 'Ashley', 1253, 0.00080630059890181], + [1973, 'F', 'Dorothy', 1175, 0.000756107903998106], + [1973, 'F', 'Helen', 1133, 0.000729081068280726], + [1974, 'F', 'Jessica', 10653, 0.00680199571179919], + [1974, 'F', 'Patricia', 8040, 0.00513358166928241], + [1974, 'F', 'Amanda', 7476, 0.00477346474621335], + [1974, 'F', 'Deborah', 4345, 0.00277430501903384], + [1974, 'F', 'Linda', 4085, 0.00260829367151973], + [1974, 'F', 'Ashley', 1626, 0.00103820942714592], + [1974, 'F', 'Helen', 1140, 0.000727895908331088], + [1974, 'F', 'Betty', 1130, 0.000721510856503622], + [1974, 'F', 'Dorothy', 1100, 0.000702355701021225], + [1975, 'F', 'Jessica', 12930, 0.00828471748344498], + [1975, 'F', 'Amanda', 12653, 0.00810723358994813], + [1975, 'F', 'Patricia', 7056, 0.00452103376358761], + [1975, 'F', 'Linda', 3525, 0.00225859467356099], + [1975, 'F', 'Deborah', 3415, 0.00218811370502433], + [1975, 'F', 'Ashley', 1988, 0.00127378332228064], + [1975, 'F', 'Helen', 1057, 0.000677258034029493], + [1975, 'F', 'Betty', 1021, 0.000654191535235679], + [1975, 'F', 'Dorothy', 975, 0.000624717675665805], + [1976, 'F', 'Jessica', 18370, 0.0116864866171597], + [1976, 'F', 'Amanda', 15591, 0.00991856357366017], + [1976, 'F', 'Patricia', 6017, 0.00382784920933316], + [1976, 'F', 'Linda', 3141, 0.00199821744499176], + [1976, 'F', 'Deborah', 2994, 0.00190470010515929], + [1976, 'F', 'Ashley', 2292, 0.00145810709453076], + [1976, 'F', 'Dorothy', 983, 0.000625357449355907], + [1976, 'F', 'Helen', 942, 0.000599274381783586], + [1976, 'F', 'Betty', 908, 0.000577644520869953], + [1977, 'F', 'Jessica', 24843, 0.0151032385968816], + [1977, 'F', 'Amanda', 18280, 0.01111327945703], + [1977, 'F', 'Patricia', 5907, 0.00359114561010263], + [1977, 'F', 'Linda', 2909, 0.0017685191433534], + [1977, 'F', 'Ashley', 2706, 0.00164510581021461], + [1977, 'F', 'Deborah', 2677, 0.00162747533405193], + [1977, 'F', 'Helen', 992, 0.00060308387425458], + [1977, 'F', 'Dorothy', 934, 0.000567822921929212], + [1977, 'F', 'Betty', 807, 0.000490613595285732], + [1978, 'F', 'Jessica', 26105, 0.0158812818667457], + [1978, 'F', 'Amanda', 20522, 0.0124847985623197], + [1978, 'F', 'Patricia', 5498, 0.00334477256094111], + [1978, 'F', 'Ashley', 3484, 0.00211953212119295], + [1978, 'F', 'Linda', 2936, 0.00178614991613734], + [1978, 'F', 'Deborah', 2479, 0.00150812862469498], + [1978, 'F', 'Helen', 922, 0.000560909476389179], + [1978, 'F', 'Dorothy', 913, 0.00055543422119666], + [1978, 'F', 'Betty', 715, 0.000434978606961239], + [1979, 'F', 'Amanda', 31927, 0.018529201469011], + [1979, 'F', 'Jessica', 27777, 0.0161207012624023], + [1979, 'F', 'Patricia', 5651, 0.0032796228114568], + [1979, 'F', 'Ashley', 4450, 0.0025826086552792], + [1979, 'F', 'Linda', 2739, 0.00158961013636174], + [1979, 'F', 'Deborah', 2181, 0.00126576842183459], + [1979, 'F', 'Dorothy', 889, 0.000515941369560272], + [1979, 'F', 'Helen', 879, 0.000510137754604588], + [1979, 'F', 'Betty', 711, 0.000412637023349104], + [1980, 'F', 'Amanda', 35819, 0.0201204564808553], + [1980, 'F', 'Jessica', 33921, 0.0190543009097711], + [1980, 'F', 'Ashley', 7296, 0.00409835144711801], + [1980, 'F', 'Patricia', 5309, 0.00298220227970799], + [1980, 'F', 'Linda', 2805, 0.00157564087296683], + [1980, 'F', 'Deborah', 1938, 0.00108862460314072], + [1980, 'F', 'Helen', 909, 0.000510608753485509], + [1980, 'F', 'Dorothy', 895, 0.00050274459226571], + [1980, 'F', 'Betty', 658, 0.000369615577330544], + [1981, 'F', 'Jessica', 42527, 0.0237842499463097], + [1981, 'F', 'Amanda', 34373, 0.0192239288782304], + [1981, 'F', 'Ashley', 8877, 0.00496467624740497], + [1981, 'F', 'Patricia', 5285, 0.0029557636552366], + [1981, 'F', 'Linda', 2730, 0.00152681831197652], + [1981, 'F', 'Deborah', 1905, 0.00106541717374186], + [1981, 'F', 'Helen', 896, 0.000501109599828191], + [1981, 'F', 'Dorothy', 787, 0.00044014872217052], + [1981, 'F', 'Betty', 662, 0.000370239458801632], + [1982, 'F', 'Jessica', 45445, 0.0250562932675237], + [1982, 'F', 'Amanda', 34210, 0.0188618284229725], + [1982, 'F', 'Ashley', 14851, 0.00818816176292209], + [1982, 'F', 'Patricia', 5167, 0.00284884733883364], + [1982, 'F', 'Linda', 2787, 0.00153662425649881], + [1982, 'F', 'Deborah', 1842, 0.00101559450321881], + [1982, 'F', 'Helen', 875, 0.000482434956740747], + [1982, 'F', 'Dorothy', 828, 0.000456521307635815], + [1982, 'F', 'Betty', 635, 0.000350109940034713], + [1983, 'F', 'Jessica', 45278, 0.0253076258799791], + [1983, 'F', 'Amanda', 33754, 0.0188664164484477], + [1983, 'F', 'Ashley', 33290, 0.0186070688975773], + [1983, 'F', 'Patricia', 4922, 0.00275109621850031], + [1983, 'F', 'Linda', 2473, 0.0013822553734968], + [1983, 'F', 'Deborah', 1605, 0.000897096592989232], + [1983, 'F', 'Helen', 844, 0.000471744252014275], + [1983, 'F', 'Dorothy', 759, 0.000424234463600515], + [1983, 'F', 'Betty', 563, 0.000314682480905257], + [1984, 'F', 'Jessica', 45851, 0.025436304942746], + [1984, 'F', 'Ashley', 38759, 0.0215019463757801], + [1984, 'F', 'Amanda', 33906, 0.0188096956530664], + [1984, 'F', 'Patricia', 4475, 0.00248255140823075], + [1984, 'F', 'Linda', 2334, 0.00129481005291857], + [1984, 'F', 'Deborah', 1468, 0.000814387813917932], + [1984, 'F', 'Helen', 859, 0.00047653891836206], + [1984, 'F', 'Dorothy', 682, 0.000378346382215279], + [1984, 'F', 'Betty', 503, 0.000279044325886049], + [1985, 'F', 'Jessica', 48343, 0.0261934794528861], + [1985, 'F', 'Ashley', 47007, 0.0254696003276962], + [1985, 'F', 'Amanda', 39050, 0.0211582932924147], + [1985, 'F', 'Patricia', 4398, 0.00238294939564762], + [1985, 'F', 'Linda', 2113, 0.00114487768826817], + [1985, 'F', 'Deborah', 1401, 0.000759097795202892], + [1985, 'F', 'Helen', 809, 0.000438336985238501], + [1985, 'F', 'Dorothy', 719, 0.000389572672912833], + [1985, 'F', 'Betty', 502, 0.000271996497638724], + [1986, 'F', 'Jessica', 52667, 0.0285499155975875], + [1986, 'F', 'Ashley', 49674, 0.0269274594602799], + [1986, 'F', 'Amanda', 40522, 0.0219663105900363], + [1986, 'F', 'Patricia', 4246, 0.00230168685566591], + [1986, 'F', 'Linda', 1951, 0.00105760505308624], + [1986, 'F', 'Deborah', 1284, 0.000696035309155683], + [1986, 'F', 'Helen', 766, 0.000415236017767331], + [1986, 'F', 'Dorothy', 629, 0.000340970568114427], + [1986, 'F', 'Betty', 407, 0.000220628014662277], + [1987, 'F', 'Jessica', 55990, 0.0298840611579793], + [1987, 'F', 'Ashley', 54845, 0.029272929705472], + [1987, 'F', 'Amanda', 41786, 0.0223028287113293], + [1987, 'F', 'Patricia', 3913, 0.00208852172372161], + [1987, 'F', 'Linda', 1929, 0.00102958303221543], + [1987, 'F', 'Deborah', 1203, 0.000642088329577588], + [1987, 'F', 'Helen', 815, 0.000434997496762871], + [1987, 'F', 'Dorothy', 614, 0.000327715905536691], + [1987, 'F', 'Betty', 435, 0.000232176578026809], + [1988, 'F', 'Jessica', 51532, 0.0268079862120844], + [1988, 'F', 'Ashley', 49963, 0.025991760752821], + [1988, 'F', 'Amanda', 39451, 0.0205232062418098], + [1988, 'F', 'Patricia', 3798, 0.00197579623599892], + [1988, 'F', 'Linda', 1844, 0.000959286008210115], + [1988, 'F', 'Deborah', 1056, 0.000549352507955467], + [1988, 'F', 'Helen', 775, 0.000403170637940802], + [1988, 'F', 'Dorothy', 608, 0.000316293868216784], + [1988, 'F', 'Betty', 395, 0.000205486970305312], + [1989, 'F', 'Jessica', 47885, 0.0240412252916602], + [1989, 'F', 'Ashley', 47585, 0.0238906067767286], + [1989, 'F', 'Amanda', 36827, 0.0184894268312827], + [1989, 'F', 'Patricia', 3606, 0.00181043454947743], + [1989, 'F', 'Linda', 1844, 0.000925801805112695], + [1989, 'F', 'Deborah', 1076, 0.000540218406887885], + [1989, 'F', 'Helen', 857, 0.000430266890987842], + [1989, 'F', 'Dorothy', 620, 0.000311278264191904], + [1989, 'F', 'Betty', 399, 0.000200322624858983], + [1990, 'F', 'Jessica', 46470, 0.0226274308541356], + [1990, 'F', 'Ashley', 45553, 0.0221809201140185], + [1990, 'F', 'Amanda', 34405, 0.0167526739517223], + [1990, 'F', 'Patricia', 3578, 0.00174221965991171], + [1990, 'F', 'Linda', 1658, 0.000807322581367696], + [1990, 'F', 'Deborah', 1094, 0.000532696564545392], + [1990, 'F', 'Helen', 861, 0.000419242908659582], + [1990, 'F', 'Dorothy', 596, 0.000290207634798038], + [1990, 'F', 'Betty', 406, 0.00019769177806712], + [1991, 'F', 'Ashley', 43482, 0.0213883278618854], + [1991, 'F', 'Jessica', 43394, 0.0213450416089107], + [1991, 'F', 'Amanda', 28887, 0.0142092044281837], + [1991, 'F', 'Patricia', 3418, 0.00168127741667642], + [1991, 'F', 'Linda', 1608, 0.000790957895264976], + [1991, 'F', 'Deborah', 1014, 0.00049877568768575], + [1991, 'F', 'Helen', 773, 0.000380230381243673], + [1991, 'F', 'Dorothy', 498, 0.000244960840697735], + [1991, 'F', 'Betty', 345, 0.000169701787230359], + [1992, 'F', 'Ashley', 38452, 0.0191860354880783], + [1992, 'F', 'Jessica', 38352, 0.0191361394215848], + [1992, 'F', 'Amanda', 25034, 0.0124909812859813], + [1992, 'F', 'Patricia', 2951, 0.00147243292222301], + [1992, 'F', 'Linda', 1580, 0.000788357850597206], + [1992, 'F', 'Deborah', 919, 0.00045854485107521], + [1992, 'F', 'Helen', 827, 0.000412640469901196], + [1992, 'F', 'Dorothy', 508, 0.00025347201778695], + [1992, 'F', 'Betty', 296, 0.000147692356820742], + [1993, 'F', 'Jessica', 34987, 0.0177506267031079], + [1993, 'F', 'Ashley', 34847, 0.0176795978141367], + [1993, 'F', 'Amanda', 20809, 0.0105574296471538], + [1993, 'F', 'Patricia', 2660, 0.00134954889045265], + [1993, 'F', 'Linda', 1487, 0.000754428270715449], + [1993, 'F', 'Helen', 868, 0.000440379111621392], + [1993, 'F', 'Deborah', 797, 0.000404357317928858], + [1993, 'F', 'Dorothy', 476, 0.000241498222502053], + [1993, 'F', 'Betty', 292, 0.000148145968425629], + [1994, 'F', 'Jessica', 32117, 0.0164797639663909], + [1994, 'F', 'Ashley', 30278, 0.0155361426463986], + [1994, 'F', 'Amanda', 18715, 0.00960297607594125], + [1994, 'F', 'Patricia', 2363, 0.00121249438778783], + [1994, 'F', 'Linda', 1281, 0.000657302289782567], + [1994, 'F', 'Helen', 848, 0.000435122827272144], + [1994, 'F', 'Deborah', 740, 0.000379706240779937], + [1994, 'F', 'Dorothy', 442, 0.000226797511384773], + [1994, 'F', 'Betty', 275, 0.00014110704893849], + [1995, 'F', 'Jessica', 27935, 0.0145416403136851], + [1995, 'F', 'Ashley', 26603, 0.0138482640868074], + [1995, 'F', 'Amanda', 16345, 0.00850843425549248], + [1995, 'F', 'Patricia', 2160, 0.0011243938814233], + [1995, 'F', 'Linda', 1233, 0.000641841507312464], + [1995, 'F', 'Helen', 837, 0.000435702629051527], + [1995, 'F', 'Deborah', 660, 0.000343564797101562], + [1995, 'F', 'Dorothy', 376, 0.000195727823803314], + [1995, 'F', 'Betty', 234, 0.00012180933715419], + [1996, 'F', 'Jessica', 24192, 0.012621838409999], + [1996, 'F', 'Ashley', 23676, 0.0123526226105793], + [1996, 'F', 'Amanda', 13973, 0.00729021776219062], + [1996, 'F', 'Patricia', 1970, 0.00102782000941212], + [1996, 'F', 'Linda', 987, 0.000514953476796833], + [1996, 'F', 'Helen', 900, 0.000469562440848176], + [1996, 'F', 'Deborah', 633, 0.000330258916729884], + [1996, 'F', 'Dorothy', 350, 0.000182607615885402], + [1996, 'F', 'Betty', 217, 0.000113216721848949], + [1997, 'F', 'Jessica', 21043, 0.0110255841966801], + [1997, 'F', 'Ashley', 20895, 0.0109480388627872], + [1997, 'F', 'Amanda', 12239, 0.00641268473996901], + [1997, 'F', 'Patricia', 1781, 0.000933163781508686], + [1997, 'F', 'Linda', 1095, 0.00057373067981584], + [1997, 'F', 'Helen', 812, 0.000425451426493573], + [1997, 'F', 'Deborah', 638, 0.000334283263673522], + [1997, 'F', 'Dorothy', 315, 0.000165045812001817], + [1997, 'F', 'Betty', 189, 9.90274872010902e-5], + [1998, 'F', 'Ashley', 19871, 0.0102549361330773], + [1998, 'F', 'Jessica', 18233, 0.00940960447458096], + [1998, 'F', 'Amanda', 10908, 0.00562935148405249], + [1998, 'F', 'Patricia', 1704, 0.000879392641073107], + [1998, 'F', 'Linda', 970, 0.000500593228779879], + [1998, 'F', 'Helen', 832, 0.000429374810664803], + [1998, 'F', 'Deborah', 553, 0.00028538974795389], + [1998, 'F', 'Dorothy', 312, 0.000161015553999301], + [1998, 'F', 'Betty', 194, 0.000100118645755976], + [1999, 'F', 'Ashley', 18132, 0.00931837897619116], + [1999, 'F', 'Jessica', 16346, 0.0084005196748743], + [1999, 'F', 'Amanda', 9741, 0.00500608480074333], + [1999, 'F', 'Patricia', 1532, 0.000787323879964971], + [1999, 'F', 'Linda', 898, 0.000461499245566935], + [1999, 'F', 'Helen', 841, 0.000432205863610014], + [1999, 'F', 'Deborah', 524, 0.000269293546410995], + [1999, 'F', 'Dorothy', 335, 0.000172162858869625], + [1999, 'F', 'Betty', 182, 9.35332546694679e-5], + [2000, 'F', 'Ashley', 17997, 0.00902349055709788], + [2000, 'F', 'Jessica', 15705, 0.00787430789571719], + [2000, 'F', 'Amanda', 8550, 0.00428687249337039], + [2000, 'F', 'Patricia', 1392, 0.000697932925236442], + [2000, 'F', 'Helen', 890, 0.000446235850187093], + [2000, 'F', 'Linda', 849, 0.000425678917762744], + [2000, 'F', 'Deborah', 545, 0.00027325678466513], + [2000, 'F', 'Dorothy', 312, 0.000156433241863341], + [2000, 'F', 'Betty', 174, 8.72416156545553e-5], + [2001, 'F', 'Ashley', 16524, 0.00834726311913036], + [2001, 'F', 'Jessica', 13917, 0.00703031111286233], + [2001, 'F', 'Amanda', 6963, 0.00351742877623485], + [2001, 'F', 'Patricia', 1223, 0.000617810626645874], + [2001, 'F', 'Helen', 884, 0.000446561401434957], + [2001, 'F', 'Linda', 837, 0.000422818883485361], + [2001, 'F', 'Deborah', 489, 0.000247023218667075], + [2001, 'F', 'Dorothy', 317, 0.00016013570617068], + [2001, 'F', 'Betty', 153, 7.72894733252811e-5], + [2002, 'F', 'Ashley', 15339, 0.00777226488632702], + [2002, 'F', 'Jessica', 11913, 0.00603631211883524], + [2002, 'F', 'Amanda', 6132, 0.00310708183603607], + [2002, 'F', 'Patricia', 1113, 0.000563956634622985], + [2002, 'F', 'Helen', 875, 0.000443362134137567], + [2002, 'F', 'Linda', 769, 0.000389651978459187], + [2002, 'F', 'Deborah', 474, 0.000240175601807093], + [2002, 'F', 'Dorothy', 263, 0.000133261990032206], + [2002, 'F', 'Betty', 127, 6.43508468976811e-5], + [2003, 'F', 'Ashley', 14512, 0.00723815072032368], + [2003, 'F', 'Jessica', 10445, 0.00520965299571257], + [2003, 'F', 'Amanda', 5339, 0.00266293320671225], + [2003, 'F', 'Patricia', 1011, 0.000504256503462462], + [2003, 'F', 'Helen', 783, 0.000390536935916031], + [2003, 'F', 'Linda', 739, 0.000368591054459702], + [2003, 'F', 'Deborah', 421, 0.000209982183934418], + [2003, 'F', 'Dorothy', 291, 0.000145142079631628], + [2003, 'F', 'Betty', 142, 7.08253446999699e-5], + [2004, 'F', 'Ashley', 14370, 0.00712775344818093], + [2004, 'F', 'Jessica', 9469, 0.00469677782886745], + [2004, 'F', 'Amanda', 4677, 0.00231986798031609], + [2004, 'F', 'Patricia', 997, 0.000494528196787501], + [2004, 'F', 'Helen', 860, 0.000426573971150703], + [2004, 'F', 'Linda', 727, 0.000360603810496001], + [2004, 'F', 'Deborah', 427, 0.000211798936838779], + [2004, 'F', 'Dorothy', 288, 0.000142852678710933], + [2004, 'F', 'Betty', 136, 6.7458209391274e-5], + [2005, 'F', 'Ashley', 13270, 0.00654498034039816], + [2005, 'F', 'Jessica', 8108, 0.00399899778447237], + [2005, 'F', 'Amanda', 4088, 0.00201626824653713], + [2005, 'F', 'Helen', 960, 0.000473487650850206], + [2005, 'F', 'Patricia', 877, 0.000432550697703782], + [2005, 'F', 'Linda', 745, 0.000367446145711879], + [2005, 'F', 'Deborah', 425, 0.00020961692876181], + [2005, 'F', 'Dorothy', 234, 0.000115412614894738], + [2005, 'F', 'Betty', 132, 6.51045519919034e-5], + [2006, 'F', 'Ashley', 12340, 0.00590922303294464], + [2006, 'F', 'Jessica', 6809, 0.00326060774970179], + [2006, 'F', 'Amanda', 3355, 0.00160659994129086], + [2006, 'F', 'Helen', 948, 0.000453966242725406], + [2006, 'F', 'Patricia', 775, 0.000371122192101466], + [2006, 'F', 'Linda', 698, 0.000334249406563643], + [2006, 'F', 'Deborah', 423, 0.000202560886785704], + [2006, 'F', 'Dorothy', 265, 0.000126899846331469], + [2006, 'F', 'Betty', 135, 6.46470915273522e-5], + [2007, 'F', 'Ashley', 11423, 0.00540374075290008], + [2007, 'F', 'Jessica', 5704, 0.00269832244196289], + [2007, 'F', 'Amanda', 3038, 0.00143714999626284], + [2007, 'F', 'Helen', 931, 0.000440416934338613], + [2007, 'F', 'Patricia', 725, 0.000342967000424806], + [2007, 'F', 'Linda', 659, 0.000311745176937858], + [2007, 'F', 'Deborah', 371, 0.000175504492631177], + [2007, 'F', 'Dorothy', 262, 0.000123941178084551], + [2007, 'F', 'Betty', 134, 6.33897628371366e-5], + [2008, 'F', 'Ashley', 9402, 0.00452030531532519], + [2008, 'F', 'Jessica', 4732, 0.00227505687642191], + [2008, 'F', 'Amanda', 2439, 0.00117262546948289], + [2008, 'F', 'Helen', 884, 0.000425010625265632], + [2008, 'F', 'Patricia', 629, 0.000302411406439007], + [2008, 'F', 'Linda', 611, 0.000293757343933598], + [2008, 'F', 'Deborah', 355, 0.000170677343856673], + [2008, 'F', 'Dorothy', 242, 0.000116349062572718], + [2008, 'F', 'Betty', 137, 6.58670312911669e-5], + [2009, 'F', 'Ashley', 7811, 0.00386339097012116], + [2009, 'F', 'Jessica', 3793, 0.00187605197153624], + [2009, 'F', 'Amanda', 1952, 0.000965476785773462], + [2009, 'F', 'Helen', 826, 0.000408547041520942], + [2009, 'F', 'Patricia', 564, 0.000278959481135365], + [2009, 'F', 'Linda', 550, 0.000272034955007891], + [2009, 'F', 'Deborah', 346, 0.000171134717150419], + [2009, 'F', 'Dorothy', 226, 0.000111781636057788], + [2009, 'F', 'Betty', 148, 7.32021333475781e-5], + [2010, 'F', 'Ashley', 6306, 0.00322252105804575], + [2010, 'F', 'Jessica', 3195, 0.00163272356175962], + [2010, 'F', 'Amanda', 1655, 0.000845745694745594], + [2010, 'F', 'Helen', 703, 0.000359250286046014], + [2010, 'F', 'Patricia', 479, 0.000244780778116701], + [2010, 'F', 'Linda', 476, 0.00024324770434979], + [2010, 'F', 'Deborah', 354, 0.000180902704495432], + [2010, 'F', 'Dorothy', 240, 0.000122645901352835], + [2010, 'F', 'Betty', 132, 6.74552457440595e-5], + [2011, 'F', 'Ashley', 5398, 0.00279205644940212], + [2011, 'F', 'Jessica', 2620, 0.00135516633890952], + [2011, 'F', 'Amanda', 1409, 0.000728789836459354], + [2011, 'F', 'Helen', 729, 0.000377067275215663], + [2011, 'F', 'Linda', 488, 0.000252412661598414], + [2011, 'F', 'Patricia', 427, 0.000220861078898612], + [2011, 'F', 'Deborah', 332, 0.000171723368136626], + [2011, 'F', 'Dorothy', 276, 0.000142757980740086], + [2011, 'F', 'Betty', 167, 8.63789231289653e-5], + [2012, 'F', 'Ashley', 4696, 0.00242806420306393], + [2012, 'F', 'Jessica', 2327, 0.00120317406314518], + [2012, 'F', 'Amanda', 1228, 0.00063493672090343], + [2012, 'F', 'Helen', 772, 0.000399162173076098], + [2012, 'F', 'Linda', 448, 0.000231638152251414], + [2012, 'F', 'Patricia', 394, 0.000203717482113967], + [2012, 'F', 'Deborah', 336, 0.000173728614188561], + [2012, 'F', 'Dorothy', 277, 0.000143222696816165], + [2012, 'F', 'Betty', 140, 7.2386922578567e-5], + [2013, 'F', 'Ashley', 3936, 0.00204898297876106], + [2013, 'F', 'Jessica', 1946, 0.00101303884061713], + [2013, 'F', 'Amanda', 1064, 0.000553891740193539], + [2013, 'F', 'Helen', 738, 0.000384184308517699], + [2013, 'F', 'Linda', 441, 0.000229573550211796], + [2013, 'F', 'Patricia', 419, 0.000218120901448396], + [2013, 'F', 'Dorothy', 334, 0.000173872031226167], + [2013, 'F', 'Deborah', 329, 0.000171269156507213], + [2013, 'F', 'Betty', 174, 9.05800402196202e-5], + [2014, 'F', 'Ashley', 3547, 0.00182125980653672], + [2014, 'F', 'Jessica', 1790, 0.000919102073217006], + [2014, 'F', 'Amanda', 1048, 0.000538111157950515], + [2014, 'F', 'Helen', 801, 0.000411285341143476], + [2014, 'F', 'Linda', 470, 0.000241328477325136], + [2014, 'F', 'Dorothy', 382, 0.000196143570932344], + [2014, 'F', 'Patricia', 377, 0.000193576246705481], + [2014, 'F', 'Deborah', 369, 0.0001894685279425], + [2014, 'F', 'Betty', 193, 9.90987151569174e-5], + [2015, 'F', 'Ashley', 3409, 0.00176165787043521], + [2015, 'F', 'Jessica', 1577, 0.000814941173856356], + [2015, 'F', 'Amanda', 1013, 0.000523484723599549], + [2015, 'F', 'Helen', 757, 0.000391192434121282], + [2015, 'F', 'Linda', 423, 0.000218592337692605], + [2015, 'F', 'Dorothy', 395, 0.00020412286853092], + [2015, 'F', 'Deborah', 346, 0.00017880129749797], + [2015, 'F', 'Patricia', 346, 0.00017880129749797], + [2015, 'F', 'Betty', 186, 9.61186165740535e-5], +]; diff --git a/packages/osd-charts/src/utils/data_samples/test_anomaly_swim_lane.ts b/packages/osd-charts/src/utils/data_samples/test_anomaly_swim_lane.ts new file mode 100644 index 000000000000..77451cc8433e --- /dev/null +++ b/packages/osd-charts/src/utils/data_samples/test_anomaly_swim_lane.ts @@ -0,0 +1,732 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const SWIM_LANE_DATA = [ + { + laneLabel: 'i-71a7f77b', + time: 1572847200, + value: 1.393504, + }, + { + laneLabel: 'i-71a7f77b', + time: 1572892200, + value: 98.22463906512188, + }, + { + laneLabel: 'i-71a7f77b', + time: 1572894000, + value: 35.80307, + }, + { + laneLabel: 'i-71a7f77b', + time: 1572895800, + value: 0.22128246815642463, + }, + { + laneLabel: 'i-71a7f77b', + time: 1572897600, + value: 0.28036102544259556, + }, + { + laneLabel: 'i-71a7f77b', + time: 1572899400, + value: 23.045729601789432, + }, + { + laneLabel: 'i-71a7f77b', + time: 1572901200, + value: 0.5144934818824536, + }, + { + laneLabel: 'i-71a7f77b', + time: 1572903000, + value: 0.2689519481619711, + }, + { + laneLabel: 'i-71a7f77b', + time: 1572904800, + value: 0.2141724473865323, + }, + { + laneLabel: 'i-71a7f77b', + time: 1572906600, + value: 0.10464817254089763, + }, + { + laneLabel: 'i-71a7f77b', + time: 1572908400, + value: 0.07397995823713518, + }, + { + laneLabel: 'i-71a7f77b', + time: 1572910200, + value: 0.03091943662761636, + }, + { + laneLabel: 'i-5d303091', + time: 1572836400, + value: 0.08552883, + }, + { + laneLabel: 'i-5d303091', + time: 1572883200, + value: 93.72707, + }, + { + laneLabel: 'i-5d303091', + time: 1572885000, + value: 50.42717, + }, + { + laneLabel: 'i-5d303091', + time: 1572910200, + value: 2.397241548271217, + }, + { + laneLabel: 'i-9f07c700', + time: 1572879600, + value: 76.10974, + }, + { + laneLabel: 'i-9f07c700', + time: 1572881400, + value: 90.75613, + }, + { + laneLabel: 'i-9f07c700', + time: 1572883200, + value: 28.016079894074096, + }, + { + laneLabel: 'i-9f07c700', + time: 1572885000, + value: 2.1780847014803517, + }, + { + laneLabel: 'i-9f07c700', + time: 1572899400, + value: 0.02531410357696053, + }, + { + laneLabel: 'i-9f07c700', + time: 1572910200, + value: 2.258382748340588, + }, + { + laneLabel: 'i-d17dcd4c', + time: 1572849000, + value: 4.210354, + }, + { + laneLabel: 'i-d17dcd4c', + time: 1572892200, + value: 89.6491, + }, + { + laneLabel: 'i-d17dcd4c', + time: 1572894000, + value: 21.289199095933952, + }, + { + laneLabel: 'i-d17dcd4c', + time: 1572895800, + value: 2.6694704470251565, + }, + { + laneLabel: 'i-d17dcd4c', + time: 1572897600, + value: 0.5850535152380327, + }, + { + laneLabel: 'i-d17dcd4c', + time: 1572899400, + value: 65.51073, + }, + { + laneLabel: 'i-d17dcd4c', + time: 1572901200, + value: 0.792024488699497, + }, + { + laneLabel: 'i-d17dcd4c', + time: 1572903000, + value: 0.5615131943374475, + }, + { + laneLabel: 'i-d17dcd4c', + time: 1572904800, + value: 0.394169564717292, + }, + { + laneLabel: 'i-d17dcd4c', + time: 1572906600, + value: 0.2046931978696015, + }, + { + laneLabel: 'i-d17dcd4c', + time: 1572908400, + value: 0.15470219555712722, + }, + { + laneLabel: 'i-d17dcd4c', + time: 1572910200, + value: 0.11787869182904363, + }, + { + laneLabel: 'i-37fbfb39', + time: 1572883200, + value: 83.92723, + }, + { + laneLabel: 'i-37fbfb39', + time: 1572885000, + value: 21.49062, + }, + { + laneLabel: 'i-37fbfb39', + time: 1572910200, + value: 0.5619960991147903, + }, + { + laneLabel: 'i-b118880c', + time: 1572847200, + value: 0.7038798, + }, + { + laneLabel: 'i-b118880c', + time: 1572849000, + value: 8.885096, + }, + { + laneLabel: 'i-b118880c', + time: 1572892200, + value: 81.92052, + }, + { + laneLabel: 'i-b118880c', + time: 1572894000, + value: 6.385433818669964, + }, + { + laneLabel: 'i-b118880c', + time: 1572895800, + value: 0.7529454579485226, + }, + { + laneLabel: 'i-b118880c', + time: 1572897600, + value: 0.16135425990161706, + }, + { + laneLabel: 'i-b118880c', + time: 1572899400, + value: 32.23389, + }, + { + laneLabel: 'i-b118880c', + time: 1572901200, + value: 0.7578572100943552, + }, + { + laneLabel: 'i-b118880c', + time: 1572903000, + value: 0.6111162033658646, + }, + { + laneLabel: 'i-b118880c', + time: 1572904800, + value: 0.443324186956941, + }, + { + laneLabel: 'i-b118880c', + time: 1572906600, + value: 0.3075144354570065, + }, + { + laneLabel: 'i-b118880c', + time: 1572908400, + value: 0.18905638316915416, + }, + { + laneLabel: 'i-b118880c', + time: 1572910200, + value: 0.15234682243112949, + }, + { + laneLabel: 'i-66578749', + time: 1572881400, + value: 60.28918, + }, + { + laneLabel: 'i-66578749', + time: 1572883200, + value: 67.31324, + }, + { + laneLabel: 'i-66578749', + time: 1572885000, + value: 81.34977, + }, + { + laneLabel: 'i-66578749', + time: 1572892200, + value: 48.25439, + }, + { + laneLabel: 'i-66578749', + time: 1572894000, + value: 31.06416, + }, + { + laneLabel: 'i-66578749', + time: 1572895800, + value: 3.0462498034282, + }, + { + laneLabel: 'i-66578749', + time: 1572897600, + value: 0.23590009859709954, + }, + { + laneLabel: 'i-66578749', + time: 1572899400, + value: 59.04865, + }, + { + laneLabel: 'i-66578749', + time: 1572901200, + value: 0.9189229698167014, + }, + { + laneLabel: 'i-66578749', + time: 1572903000, + value: 0.3479760519757592, + }, + { + laneLabel: 'i-66578749', + time: 1572904800, + value: 0.09983690866310621, + }, + { + laneLabel: 'i-66578749', + time: 1572906600, + value: 0.026896391677229674, + }, + { + laneLabel: 'i-5d302081', + time: 1572883200, + value: 79.62794, + }, + { + laneLabel: 'i-5d302081', + time: 1572885000, + value: 38.70934, + }, + { + laneLabel: 'i-5d302081', + time: 1572910200, + value: 2.3953009901171605, + }, + { + laneLabel: 'i-ef74d410', + time: 1572849000, + value: 0.1538905, + }, + { + laneLabel: 'i-ef74d410', + time: 1572881400, + value: 77.86751, + }, + { + laneLabel: 'i-ef74d410', + time: 1572883200, + value: 7.1111139045789935, + }, + { + laneLabel: 'i-ef74d410', + time: 1572885000, + value: 0.07120867159413205, + }, + { + laneLabel: 'i-ef74d410', + time: 1572892200, + value: 0.46005659357549517, + }, + { + laneLabel: 'i-ef74d410', + time: 1572899400, + value: 0.38022323217471177, + }, + { + laneLabel: 'i-ef74d410', + time: 1572901200, + value: 0.05279469228246696, + }, + { + laneLabel: 'i-ef74d410', + time: 1572903000, + value: 0.04307715617784296, + }, + { + laneLabel: 'i-3b3565e0', + time: 1572881400, + value: 49.34176, + }, + { + laneLabel: 'i-3b3565e0', + time: 1572883200, + value: 74.35002, + }, + { + laneLabel: 'i-3b3565e0', + time: 1572885000, + value: 70.82227, + }, + { + laneLabel: 'i-3b3565e0', + time: 1572899400, + value: 1.129179725764338, + }, + { + laneLabel: 'i-3b3565e0', + time: 1572901200, + value: 0.08944716315823782, + }, + { + laneLabel: 'i-3b3565e0', + time: 1572903000, + value: 0.025215653478830508, + }, + { + laneLabel: 'i-7db7c747', + time: 1572881400, + value: 56.8998, + }, + { + laneLabel: 'i-7db7c747', + time: 1572883200, + value: 34.59895, + }, + { + laneLabel: 'i-7db7c747', + time: 1572885000, + value: 69.07187, + }, + { + laneLabel: 'i-7db7c747', + time: 1572899400, + value: 20.04246, + }, + { + laneLabel: 'i-7db7c747', + time: 1572901200, + value: 0.501246678069153, + }, + { + laneLabel: 'i-7db7c747', + time: 1572903000, + value: 0.10228357345625516, + }, + { + laneLabel: 'i-8270d519', + time: 1572858000, + value: 23.41472, + }, + { + laneLabel: 'i-8270d519', + time: 1572870600, + value: 0.09778774, + }, + { + laneLabel: 'i-8270d519', + time: 1572881400, + value: 57.96897, + }, + { + laneLabel: 'i-8270d519', + time: 1572883200, + value: 63.73998, + }, + { + laneLabel: 'i-8270d519', + time: 1572885000, + value: 45.19735, + }, + { + laneLabel: 'i-8270d519', + time: 1572892200, + value: 2.166846374797418, + }, + { + laneLabel: 'i-8270d519', + time: 1572894000, + value: 0.2283247569954898, + }, + { + laneLabel: 'i-8270d519', + time: 1572899400, + value: 42.24531, + }, + { + laneLabel: 'i-8270d519', + time: 1572901200, + value: 0.49207037746175214, + }, + { + laneLabel: 'i-8270d519', + time: 1572903000, + value: 0.3243143048875506, + }, + { + laneLabel: 'i-8270d519', + time: 1572904800, + value: 0.10024450293348822, + }, + { + laneLabel: 'i-8270d519', + time: 1572906600, + value: 0.033965805201251434, + }, + { + laneLabel: 'i-4fefbf6c', + time: 1572847200, + value: 0.1867262, + }, + { + laneLabel: 'i-4fefbf6c', + time: 1572849000, + value: 11.47219, + }, + { + laneLabel: 'i-4fefbf6c', + time: 1572892200, + value: 53.59314, + }, + { + laneLabel: 'i-4fefbf6c', + time: 1572894000, + value: 15.91959, + }, + { + laneLabel: 'i-4fefbf6c', + time: 1572895800, + value: 0.5736916311300446, + }, + { + laneLabel: 'i-4fefbf6c', + time: 1572897600, + value: 29.28071, + }, + { + laneLabel: 'i-4fefbf6c', + time: 1572899400, + value: 25.1765, + }, + { + laneLabel: 'i-4fefbf6c', + time: 1572901200, + value: 1.4671989108080628, + }, + { + laneLabel: 'i-4fefbf6c', + time: 1572903000, + value: 0.8367316591855911, + }, + { + laneLabel: 'i-4fefbf6c', + time: 1572904800, + value: 0.48490204846685775, + }, + { + laneLabel: 'i-4fefbf6c', + time: 1572906600, + value: 0.24413767062868427, + }, + { + laneLabel: 'i-4fefbf6c', + time: 1572908400, + value: 0.17736807671463703, + }, + { + laneLabel: 'i-4fefbf6c', + time: 1572910200, + value: 0.10326196476266834, + }, + { + laneLabel: 'i-16fd8d2a', + time: 1572859800, + value: 33.42309, + }, + { + laneLabel: 'i-16fd8d2a', + time: 1572861600, + value: 47.17204, + }, + { + laneLabel: 'i-16fd8d2a', + time: 1572863400, + value: 0.16179846422070132, + }, + { + laneLabel: 'i-ca80c01a', + time: 1572847200, + value: 0.6692097, + }, + { + laneLabel: 'i-ca80c01a', + time: 1572870600, + value: 44.60156, + }, + { + laneLabel: 'i-ca80c01a', + time: 1572883200, + value: 26.06775, + }, + { + laneLabel: 'i-ca80c01a', + time: 1572885000, + value: 0.24539550844649843, + }, + { + laneLabel: 'i-ca80c01a', + time: 1572910200, + value: 0.09130230719255052, + }, + { + laneLabel: 'i-4a90d021', + time: 1572883200, + value: 43.41428, + }, + { + laneLabel: 'i-4a90d021', + time: 1572885000, + value: 20.30829, + }, + { + laneLabel: 'i-4a90d021', + time: 1572910200, + value: 20.706159141229445, + }, + { + laneLabel: 'i-4ff414ac', + time: 1572883200, + value: 42.82781, + }, + { + laneLabel: 'i-4ff414ac', + time: 1572885000, + value: 3.643815188524499, + }, + { + laneLabel: 'i-4ff414ac', + time: 1572910200, + value: 7.987421742621203, + }, + { + laneLabel: 'i-850643a7', + time: 1572870600, + value: 37.51045, + }, + { + laneLabel: 'i-850643a7', + time: 1572904800, + value: 0.08720035850172568, + }, + { + laneLabel: 'i-c961f137', + time: 1572859800, + value: 0.7612613, + }, + { + laneLabel: 'i-c961f137', + time: 1572861600, + value: 36.94459, + }, + { + laneLabel: 'i-c961f137', + time: 1572863400, + value: 3.530620723871948, + }, + { + laneLabel: 'i-7cdbab5b', + time: 1572874200, + value: 3.468401, + }, + { + laneLabel: 'i-7cdbab5b', + time: 1572876000, + value: 15.87705, + }, + { + laneLabel: 'i-7cdbab5b', + time: 1572877800, + value: 21.19452, + }, + { + laneLabel: 'i-7cdbab5b', + time: 1572879600, + value: 22.534, + }, + { + laneLabel: 'i-7cdbab5b', + time: 1572881400, + value: 30.46893, + }, + { + laneLabel: 'i-7cdbab5b', + time: 1572883200, + value: 9.532251, + }, + { + laneLabel: 'i-7cdbab5b', + time: 1572885000, + value: 6.127118137471535, + }, + { + laneLabel: 'i-7cdbab5b', + time: 1572892200, + value: 0.13956047806108005, + }, + { + laneLabel: 'i-7cdbab5b', + time: 1572894000, + value: 0.11986169822285726, + }, + { + laneLabel: 'i-7cdbab5b', + time: 1572899400, + value: 4.098007780030352, + }, + { + laneLabel: 'i-7cdbab5b', + time: 1572901200, + value: 0.6927671145991932, + }, + { + laneLabel: 'i-7cdbab5b', + time: 1572903000, + value: 0.2113142229906149, + }, + { + laneLabel: 'i-7cdbab5b', + time: 1572904800, + value: 0.0823203266441151, + }, +]; diff --git a/packages/osd-charts/src/utils/data_samples/test_dataset.ts b/packages/osd-charts/src/utils/data_samples/test_dataset.ts new file mode 100644 index 000000000000..60c3c4e3534f --- /dev/null +++ b/packages/osd-charts/src/utils/data_samples/test_dataset.ts @@ -0,0 +1,175 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const BARCHART_1Y0G = [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 10 }, + { x: 3, y: 6 }, +]; + +/** @internal */ +export const BARCHART_1Y0G_LINEAR = [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2.5, y: 10 }, + { x: 3.5, y: 6 }, +]; +/** @internal */ +export const BARCHART_1Y1G = [ + { x: 0, g: 'a', y: 1 }, + { x: 0, g: 'b', y: 2 }, + { x: 1, g: 'a', y: 2 }, + { x: 1, g: 'b', y: 3 }, + { x: 2, g: 'a', y: 3 }, + { x: 2, g: 'b', y: 4 }, + { x: 3, g: 'a', y: 4 }, + { x: 3, g: 'b', y: 5 }, +]; +/** @internal */ +export const BARCHART_1Y1G_ORDINAL = [ + { x: 'a', g: 'a', y: 1 }, + { x: 'a', g: 'b', y: 2 }, + { x: 'b', g: 'a', y: 2 }, + { x: 'b', g: 'b', y: 3 }, + { x: 'c', g: 'a', y: 3 }, + { x: 'd', g: 'b', y: 4 }, + { x: 'e', g: 'a', y: 4 }, + { x: 'e', g: 'b', y: 5 }, +]; + +/** @internal */ +export const BARCHART_1Y1G_LINEAR = [ + { x: 0, g: 'a', y: 1 }, + { x: 0, g: 'b', y: 1 }, + { x: 1, g: 'a', y: 2 }, + { x: 1, g: 'b', y: 2 }, + { x: 2, g: 'a', y: 10 }, + { x: 2, g: 'b', y: 20 }, + { x: 3, g: 'a', y: 6 }, + { x: 5, g: 'a', y: 2 }, + { x: 7, g: 'b', y: 3 }, +]; + +/** @internal */ +export const BARCHART_1Y2G = [ + { x: 0, g1: 'a', g2: 's', y: 1 }, + { x: 0, g1: 'a', g2: 'p', y: 1 }, + { x: 0, g1: 'b', g2: 's', y: 1 }, + { x: 0, g1: 'b', g2: 'p', y: 1 }, + { x: 1, g1: 'a', g2: 's', y: 2 }, + { x: 1, g1: 'a', g2: 'p', y: 2 }, + { x: 1, g1: 'b', g2: 's', y: 2 }, + { x: 1, g1: 'b', g2: 'p', y: 2 }, + { x: 2, g1: 'a', g2: 's', y: 1 }, + { x: 2, g1: 'a', g2: 'p', y: 2 }, + { x: 2, g1: 'b', g2: 's', y: 3 }, + { x: 2, g1: 'b', g2: 'p', y: 4 }, + { x: 3, g1: 'a', g2: 's', y: 6 }, + { x: 3, g1: 'a', g2: 'p', y: 6 }, + { x: 3, g1: 'b', g2: 's', y: 6 }, + { x: 3, g1: 'b', g2: 'p', y: 6 }, +]; + +/** @internal */ +export const BARCHART_2Y0G = [ + { x: 0, y1: 1, y2: 3 }, + { x: 1, y1: 2, y2: 7 }, + { x: 2, y1: 1, y2: 2 }, + { x: 3, y1: 6, y2: 10 }, +]; + +/** @internal */ +export const CHART_ORDINAL_2Y0G = [ + { x: 'a', y1: 1, y2: 3 }, + { x: 'b', y1: 2, y2: 7 }, + { x: 'c', y1: 1, y2: 2 }, + { x: 'd', y1: 6, y2: 10 }, +]; + +/** @internal */ +export const BARCHART_2Y1G = [ + { x: 0, g: 'a', y1: 1, y2: 4 }, + { x: 0, g: 'b', y1: 3, y2: 6 }, + { x: 1, g: 'a', y1: 2, y2: 1 }, + { x: 1, g: 'b', y1: 2, y2: 5 }, + { x: 2, g: 'a', y1: 10, y2: 5 }, + { x: 2, g: 'b', y1: 3, y2: 1 }, + { x: 3, g: 'a', y1: 7, y2: 3 }, + { x: 3, g: 'b', y1: 6, y2: 4 }, +]; + +/** @internal */ +export const BARCHART_2Y2G = [ + { x: 0, g1: 'cdn.google.com', g2: 'direct-cdn', y1: 1, y2: 4 }, + { x: 0, g1: 'cdn.google.com', g2: 'indirect-cdn', y1: 1, y2: 4 }, + { x: 0, g1: 'cloudflare.com', g2: 'direct-cdn', y1: 3, y2: 6 }, + { x: 0, g1: 'cloudflare.com', g2: 'indirect-cdn', y1: 3, y2: 6 }, + { x: 1, g1: 'cdn.google.com', g2: 'direct-cdn', y1: 2, y2: 1 }, + { x: 1, g1: 'cdn.google.com', g2: 'indirect-cdn', y1: 2, y2: 1 }, + { x: 1, g1: 'cloudflare.com', g2: 'direct-cdn', y1: 2, y2: 5 }, + { x: 1, g1: 'cloudflare.com', g2: 'indirect-cdn', y1: 2, y2: 5 }, + { x: 2, g1: 'cdn.google.com', g2: 'direct-cdn', y1: 10, y2: 5 }, + { x: 2, g1: 'cdn.google.com', g2: 'indirect-cdn', y1: 10, y2: 5 }, + { x: 2, g1: 'cloudflare.com', g2: 'direct-cdn', y1: 3, y2: 1 }, + { x: 2, g1: 'cloudflare.com', g2: 'indirect-cdn', y1: 3, y2: 1 }, + { x: 3, g1: 'cdn.google.com', g2: 'direct-cdn', y1: 7, y2: 3 }, + { x: 3, g1: 'cdn.google.com', g2: 'indirect-cdn', y1: 7, y2: 3 }, + { x: 3, g1: 'cloudflare.com', g2: 'direct-cdn', y1: 6, y2: 4 }, + { x: 3, g1: 'cloudflare.com', g2: 'indirect-cdn', y1: 6, y2: 4 }, + { x: 6, g1: 'cdn.google.com', g2: 'direct-cdn', y1: 7, y2: 3 }, + { x: 6, g1: 'cdn.google.com', g2: 'indirect-cdn', y1: 7, y2: 3 }, + { x: 6, g1: 'cloudflare.com', g2: 'direct-cdn', y1: 6, y2: 4 }, + { x: 6, g1: 'cloudflare.com', g2: 'indirect-cdn', y1: 6, y2: 4 }, +]; + +/** @internal */ +export const BARCHART_2Y3G = [ + { x: 0, g1: 'cdn.google.com', g2: 'direct-cdn', y1: 1, y2: 4, g3: 'somevalue' }, + { x: 0, g1: 'cdn.google.com', g2: 'indirect-cdn', y1: 1, y2: 4, g3: 'newvalue' }, + { x: 0, g1: 'cloudflare.com', g2: 'direct-cdn', y1: 3, y2: 6, g3: 'somevalue' }, + { x: 0, g1: 'cloudflare.com', g2: 'indirect-cdn', y1: 3, y2: 6, g3: 'newvalue' }, + { x: 1, g1: 'cdn.google.com', g2: 'direct-cdn', y1: 2, y2: 1, g3: 'somevalue' }, + { x: 1, g1: 'cdn.google.com', g2: 'indirect-cdn', y1: 2, y2: 1, g3: 'newvalue' }, + { x: 1, g1: 'cloudflare.com', g2: 'direct-cdn', y1: 2, y2: 5, g3: 'somevalue' }, + { x: 1, g1: 'cloudflare.com', g2: 'indirect-cdn', y1: 2, y2: 5, g3: 'newvalue' }, + { x: 2, g1: 'cdn.google.com', g2: 'direct-cdn', y1: 10, y2: 5, g3: 'somevalue' }, + { x: 2, g1: 'cdn.google.com', g2: 'indirect-cdn', y1: 10, y2: 5, g3: 'newvalue' }, + { x: 2, g1: 'cloudflare.com', g2: 'direct-cdn', y1: 3, y2: 1, g3: 'somevalue' }, + { x: 2, g1: 'cloudflare.com', g2: 'indirect-cdn', y1: 3, y2: 1, g3: 'newvalue' }, + { x: 3, g1: 'cdn.google.com', g2: 'direct-cdn', y1: 7, y2: 3, g3: 'somevalue' }, + { x: 3, g1: 'cdn.google.com', g2: 'indirect-cdn', y1: 7, y2: 3, g3: 'newvalue' }, + { x: 3, g1: 'cloudflare.com', g2: 'direct-cdn', y1: 6, y2: 4, g3: 'somevalue' }, + { x: 3, g1: 'cloudflare.com', g2: 'indirect-cdn', y1: 6, y2: 4, g3: 'newvalue' }, + { x: 6, g1: 'cdn.google.com', g2: 'direct-cdn', y1: 7, y2: 3, g3: 'somevalue' }, + { x: 6, g1: 'cdn.google.com', g2: 'indirect-cdn', y1: 7, y2: 3, g3: 'newvalue' }, + { x: 6, g1: 'cloudflare.com', g2: 'direct-cdn', y1: 6, y2: 4, g3: 'somevalue' }, + { x: 6, g1: 'cloudflare.com', g2: 'indirect-cdn', y1: 6, y2: 4, g3: 'newvalue' }, +]; + +const NOW = Date.now(); +const DAY = 24 * 60 * 60 * 1000; +/** @internal */ +export const TIME_CHART_2Y0G = [ + { x: NOW, y1: 1, y2: 3 }, + { x: NOW + DAY, y1: 2, y2: 7 }, + { x: NOW + DAY * 2, y1: 1, y2: 2 }, + { x: NOW + DAY * 3, y1: 6, y2: 10 }, +]; diff --git a/packages/osd-charts/src/utils/data_samples/test_dataset_github.ts b/packages/osd-charts/src/utils/data_samples/test_dataset_github.ts new file mode 100644 index 000000000000..95075b6373bd --- /dev/null +++ b/packages/osd-charts/src/utils/data_samples/test_dataset_github.ts @@ -0,0 +1,842 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const GITHUB_DATASET = [ + { + authorAssociation: 'Team Member', + vizType: 'Data Table', + issueType: 'Bug', + count: 14, + }, + { + authorAssociation: 'Team Member', + vizType: 'Data Table', + issueType: 'Other', + count: 22, + }, + { + authorAssociation: 'Team Member', + vizType: 'Heatmap', + issueType: 'Bug', + count: 12, + }, + { + authorAssociation: 'Team Member', + vizType: 'Heatmap', + issueType: 'Other', + count: 6, + }, + { + authorAssociation: 'Team Member', + vizType: 'Markdown', + issueType: 'Bug', + count: 6, + }, + { + authorAssociation: 'Team Member', + vizType: 'Markdown', + issueType: 'Other', + count: 11, + }, + { + authorAssociation: 'Team Member', + vizType: 'MetricVis', + issueType: 'Bug', + count: 16, + }, + { + authorAssociation: 'Team Member', + vizType: 'MetricVis', + issueType: 'Other', + count: 8, + }, + { + authorAssociation: 'Team Member', + vizType: 'Pie Chart', + issueType: 'Bug', + count: 7, + }, + { + authorAssociation: 'Team Member', + vizType: 'Pie Chart', + issueType: 'Other', + count: 4, + }, + { + authorAssociation: 'Team Member', + vizType: 'Tagcloud', + issueType: 'Bug', + count: 19, + }, + { + authorAssociation: 'Team Member', + vizType: 'Tagcloud', + issueType: 'Other', + count: 13, + }, + { + authorAssociation: 'Team Member', + vizType: 'TSVB', + issueType: 'Bug', + count: 86, + }, + { + authorAssociation: 'Team Member', + vizType: 'TSVB', + issueType: 'Other', + count: 123, + }, + { + authorAssociation: 'Team Member', + vizType: 'Timelion', + issueType: 'Bug', + count: 58, + }, + { + authorAssociation: 'Team Member', + vizType: 'Timelion', + issueType: 'Other', + count: 93, + }, + { + authorAssociation: 'Team Member', + vizType: 'Vega vis', + issueType: 'Bug', + count: 11, + }, + { + authorAssociation: 'Team Member', + vizType: 'Vega vis', + issueType: 'Other', + count: 38, + }, + { + authorAssociation: 'Team Member', + vizType: 'Point Series', + issueType: 'Bug', + count: 1, + }, + { + authorAssociation: 'Team Member', + vizType: 'Point Series', + issueType: 'Other', + count: 1, + }, + { + authorAssociation: 'Team Member', + vizType: 'Inspector', + issueType: 'Bug', + count: 15, + }, + { + authorAssociation: 'Team Member', + vizType: 'Inspector', + issueType: 'Other', + count: 11, + }, + { + authorAssociation: 'Community', + vizType: 'Data Table', + issueType: 'Bug', + count: 6, + }, + { + authorAssociation: 'Community', + vizType: 'Data Table', + issueType: 'Other', + count: 24, + }, + { + authorAssociation: 'Community', + vizType: 'Heatmap', + issueType: 'Bug', + count: 11, + }, + { + authorAssociation: 'Community', + vizType: 'Heatmap', + issueType: 'Other', + count: 5, + }, + { + authorAssociation: 'Community', + vizType: 'Markdown', + issueType: 'Bug', + count: 0, + }, + { + authorAssociation: 'Community', + vizType: 'Markdown', + issueType: 'Other', + count: 1, + }, + { + authorAssociation: 'Community', + vizType: 'MetricVis', + issueType: 'Bug', + count: 6, + }, + { + authorAssociation: 'Community', + vizType: 'MetricVis', + issueType: 'Other', + count: 10, + }, + { + authorAssociation: 'Community', + vizType: 'Pie Chart', + issueType: 'Bug', + count: 3, + }, + { + authorAssociation: 'Community', + vizType: 'Pie Chart', + issueType: 'Other', + count: 5, + }, + { + authorAssociation: 'Community', + vizType: 'Tagcloud', + issueType: 'Bug', + count: 2, + }, + { + authorAssociation: 'Community', + vizType: 'Tagcloud', + issueType: 'Other', + count: 1, + }, + { + authorAssociation: 'Community', + vizType: 'TSVB', + issueType: 'Bug', + count: 28, + }, + { + authorAssociation: 'Community', + vizType: 'TSVB', + issueType: 'Other', + count: 51, + }, + { + authorAssociation: 'Community', + vizType: 'Timelion', + issueType: 'Bug', + count: 29, + }, + { + authorAssociation: 'Community', + vizType: 'Timelion', + issueType: 'Other', + count: 43, + }, + { + authorAssociation: 'Community', + vizType: 'Vega vis', + issueType: 'Bug', + count: 2, + }, + { + authorAssociation: 'Community', + vizType: 'Vega vis', + issueType: 'Other', + count: 9, + }, + { + authorAssociation: 'Community', + vizType: 'Point Series', + issueType: 'Bug', + count: 2, + }, + { + authorAssociation: 'Community', + vizType: 'Point Series', + issueType: 'Other', + count: 3, + }, + { + authorAssociation: 'Community', + vizType: 'Inspector', + issueType: 'Bug', + count: 5, + }, + { + authorAssociation: 'Community', + vizType: 'Inspector', + issueType: 'Other', + count: 8, + }, +]; + +/** @internal */ +export const GROUPED_BAR_CHART = [ + { + timestamp: '2018-08-13', + status: 'success', + count: 3942, + }, + { + timestamp: '2018-08-13', + status: 'info', + count: 3502, + }, + { + timestamp: '2018-08-13', + status: 'security', + count: 937, + }, + { + timestamp: '2018-08-13', + status: 'warning', + count: 476, + }, + { + timestamp: '2018-08-13', + status: 'error', + count: 248, + }, + { + timestamp: '2018-08-14', + status: 'success', + count: 3939, + }, + { + timestamp: '2018-08-14', + status: 'info', + count: 3506, + }, + { + timestamp: '2018-08-14', + status: 'security', + count: 923, + }, + { + timestamp: '2018-08-14', + status: 'warning', + count: 453, + }, + { + timestamp: '2018-08-14', + status: 'error', + count: 275, + }, + { + timestamp: '2018-08-15', + status: 'success', + count: 3888, + }, + { + timestamp: '2018-08-15', + status: 'info', + count: 3539, + }, + { + timestamp: '2018-08-15', + status: 'security', + count: 916, + }, + { + timestamp: '2018-08-15', + status: 'warning', + count: 482, + }, + { + timestamp: '2018-08-15', + status: 'error', + count: 295, + }, + { + timestamp: '2018-08-16', + status: 'success', + count: 7, + }, + { + timestamp: '2018-08-16', + status: 'info', + count: 6, + }, + { + timestamp: '2018-08-16', + status: 'security', + count: 1, + }, +]; + +/** @internal */ +export const MULTI_GROUPED_BAR_CHART = [ + { + timestamp: '2018-08-13', + status: 'success', + os: 'win 7', + count: 817, + }, + { + timestamp: '2018-08-13', + status: 'success', + os: 'ios', + count: 813, + }, + { + timestamp: '2018-08-13', + status: 'success', + os: 'win xp', + count: 790, + }, + { + timestamp: '2018-08-13', + status: 'success', + os: 'win 8', + count: 773, + }, + { + timestamp: '2018-08-13', + status: 'success', + os: 'osx', + count: 749, + }, + { + timestamp: '2018-08-13', + status: 'info', + os: 'win 7', + count: 715, + }, + { + timestamp: '2018-08-13', + status: 'info', + os: 'ios', + count: 713, + }, + { + timestamp: '2018-08-13', + status: 'info', + os: 'win xp', + count: 704, + }, + { + timestamp: '2018-08-13', + status: 'info', + os: 'win 8', + count: 700, + }, + { + timestamp: '2018-08-13', + status: 'info', + os: 'osx', + count: 670, + }, + { + timestamp: '2018-08-13', + status: 'security', + os: 'win 7', + count: 201, + }, + { + timestamp: '2018-08-13', + status: 'security', + os: 'ios', + count: 196, + }, + { + timestamp: '2018-08-13', + status: 'security', + os: 'osx', + count: 195, + }, + { + timestamp: '2018-08-13', + status: 'security', + os: 'win xp', + count: 176, + }, + { + timestamp: '2018-08-13', + status: 'security', + os: 'win 8', + count: 169, + }, + { + timestamp: '2018-08-13', + status: 'warning', + os: 'osx', + count: 103, + }, + { + timestamp: '2018-08-13', + status: 'warning', + os: 'win 7', + count: 100, + }, + { + timestamp: '2018-08-13', + status: 'warning', + os: 'win 8', + count: 95, + }, + { + timestamp: '2018-08-13', + status: 'warning', + os: 'ios', + count: 90, + }, + { + timestamp: '2018-08-13', + status: 'warning', + os: 'win xp', + count: 88, + }, + { + timestamp: '2018-08-13', + status: 'error', + os: 'osx', + count: 60, + }, + { + timestamp: '2018-08-13', + status: 'error', + os: 'ios', + count: 52, + }, + { + timestamp: '2018-08-13', + status: 'error', + os: 'win xp', + count: 48, + }, + { + timestamp: '2018-08-13', + status: 'error', + os: 'win 8', + count: 45, + }, + { + timestamp: '2018-08-13', + status: 'error', + os: 'win 7', + count: 43, + }, + { + timestamp: '2018-08-14', + status: 'success', + os: 'osx', + count: 811, + }, + { + timestamp: '2018-08-14', + status: 'success', + os: 'ios', + count: 786, + }, + { + timestamp: '2018-08-14', + status: 'success', + os: 'win 7', + count: 782, + }, + { + timestamp: '2018-08-14', + status: 'success', + os: 'win xp', + count: 782, + }, + { + timestamp: '2018-08-14', + status: 'success', + os: 'win 8', + count: 778, + }, + { + timestamp: '2018-08-14', + status: 'info', + os: 'osx', + count: 718, + }, + { + timestamp: '2018-08-14', + status: 'info', + os: 'win 8', + count: 717, + }, + { + timestamp: '2018-08-14', + status: 'info', + os: 'win 7', + count: 701, + }, + { + timestamp: '2018-08-14', + status: 'info', + os: 'ios', + count: 692, + }, + { + timestamp: '2018-08-14', + status: 'info', + os: 'win xp', + count: 678, + }, + { + timestamp: '2018-08-14', + status: 'security', + os: 'win xp', + count: 206, + }, + { + timestamp: '2018-08-14', + status: 'security', + os: 'ios', + count: 197, + }, + { + timestamp: '2018-08-14', + status: 'security', + os: 'win 7', + count: 180, + }, + { + timestamp: '2018-08-14', + status: 'security', + os: 'osx', + count: 179, + }, + { + timestamp: '2018-08-14', + status: 'security', + os: 'win 8', + count: 161, + }, + { + timestamp: '2018-08-14', + status: 'warning', + os: 'win xp', + count: 97, + }, + { + timestamp: '2018-08-14', + status: 'warning', + os: 'ios', + count: 93, + }, + { + timestamp: '2018-08-14', + status: 'warning', + os: 'win 8', + count: 93, + }, + { + timestamp: '2018-08-14', + status: 'warning', + os: 'osx', + count: 88, + }, + { + timestamp: '2018-08-14', + status: 'warning', + os: 'win 7', + count: 82, + }, + { + timestamp: '2018-08-14', + status: 'error', + os: 'win 7', + count: 62, + }, + { + timestamp: '2018-08-14', + status: 'error', + os: 'ios', + count: 58, + }, + { + timestamp: '2018-08-14', + status: 'error', + os: 'win xp', + count: 54, + }, + { + timestamp: '2018-08-14', + status: 'error', + os: 'win 8', + count: 51, + }, + { + timestamp: '2018-08-14', + status: 'error', + os: 'osx', + count: 50, + }, + { + timestamp: '2018-08-15', + status: 'success', + os: 'osx', + count: 795, + }, + { + timestamp: '2018-08-15', + status: 'success', + os: 'ios', + count: 781, + }, + { + timestamp: '2018-08-15', + status: 'success', + os: 'win xp', + count: 780, + }, + { + timestamp: '2018-08-15', + status: 'success', + os: 'win 7', + count: 766, + }, + { + timestamp: '2018-08-15', + status: 'success', + os: 'win 8', + count: 766, + }, + { + timestamp: '2018-08-15', + status: 'info', + os: 'win xp', + count: 723, + }, + { + timestamp: '2018-08-15', + status: 'info', + os: 'ios', + count: 716, + }, + { + timestamp: '2018-08-15', + status: 'info', + os: 'win 7', + count: 708, + }, + { + timestamp: '2018-08-15', + status: 'info', + os: 'osx', + count: 701, + }, + { + timestamp: '2018-08-15', + status: 'info', + os: 'win 8', + count: 691, + }, + { + timestamp: '2018-08-15', + status: 'security', + os: 'osx', + count: 199, + }, + { + timestamp: '2018-08-15', + status: 'security', + os: 'ios', + count: 188, + }, + { + timestamp: '2018-08-15', + status: 'security', + os: 'win 8', + count: 188, + }, + { + timestamp: '2018-08-15', + status: 'security', + os: 'win 7', + count: 179, + }, + { + timestamp: '2018-08-15', + status: 'security', + os: 'win xp', + count: 162, + }, + { + timestamp: '2018-08-15', + status: 'warning', + os: 'ios', + count: 103, + }, + { + timestamp: '2018-08-15', + status: 'warning', + os: 'win 7', + count: 97, + }, + { + timestamp: '2018-08-15', + status: 'warning', + os: 'win 8', + count: 97, + }, + { + timestamp: '2018-08-15', + status: 'warning', + os: 'win xp', + count: 97, + }, + { + timestamp: '2018-08-15', + status: 'warning', + os: 'osx', + count: 88, + }, + { + timestamp: '2018-08-15', + status: 'error', + os: 'osx', + count: 64, + }, + { + timestamp: '2018-08-15', + status: 'error', + os: 'win 7', + count: 60, + }, + { + timestamp: '2018-08-15', + status: 'error', + os: 'ios', + count: 59, + }, + { + timestamp: '2018-08-15', + status: 'error', + os: 'win 8', + count: 57, + }, + { + timestamp: '2018-08-15', + status: 'error', + os: 'win xp', + count: 55, + }, +]; + +/** @internal */ +export const BAR_CHART_2Y = [ + { x: 0, y1: 1, y2: 3 }, + { x: 1, y1: 2, y2: 7 }, + { x: 2, y1: 1, y2: 2 }, + { x: 3, y1: 6, y2: 10 }, +]; diff --git a/packages/osd-charts/src/utils/data_samples/test_dataset_kibana.ts b/packages/osd-charts/src/utils/data_samples/test_dataset_kibana.ts new file mode 100644 index 000000000000..68a8adcb681e --- /dev/null +++ b/packages/osd-charts/src/utils/data_samples/test_dataset_kibana.ts @@ -0,0 +1,1426 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const KIBANA_METRICS = { + metrics: { + kibana_os_load: [ + { + bucket_size: '30 seconds', + timeRange: { min: 1551438000000, max: 1551441600000 }, + metric: { + app: 'kibana', + field: 'kibana_stats.os.load.1m', + metricAgg: 'max', + label: '1m', + title: 'System Load', + description: 'Load average over the last minute.', + units: '', + format: '0,0.[00]', + hasCalculation: false, + isDerivative: false, + }, + data: [ + [1551438000000, 8.3203125], + [1551438030000, 7.9140625], + [1551438060000, 7.8671875], + [1551438090000, 7.125], + [1551438120000, 8.765625], + [1551438150000, 11.546875], + [1551438180000, 12.984375], + [1551438210000, 13.546875], + [1551438240000, 13.390625], + [1551438270000, 11.5625], + [1551438300000, 11.5859375], + [1551438330000, 10.0546875], + [1551438360000, 9.921875], + [1551438390000, 9.4921875], + [1551438420000, 9.78125], + [1551438450000, 10.046875], + [1551438480000, 14.0546875], + [1551438510000, 10.640625], + [1551438540000, 8.2421875], + [1551438570000, 8.5], + [1551438600000, 7.2578125], + [1551438630000, 8.515625], + [1551438660000, 10.796875], + [1551438690000, 11.125], + [1551438720000, 21.40625], + [1551438750000, 17.921875], + [1551438780000, 26.640625], + [1551438810000, 31.390625], + [1551438840000, 23.953125], + [1551438870000, 16], + [1551438900000, 11.9765625], + [1551438930000, 9.1640625], + [1551438960000, 7.98046875], + [1551438990000, 7.1640625], + [1551439020000, 7.39453125], + [1551439050000, 5.68359375], + [1551439080000, 4.95703125], + [1551439110000, 4.26171875], + [1551439140000, 11.1171875], + [1551439170000, 10.8515625], + [1551439200000, 12.6171875], + [1551439230000, 11.1171875], + [1551439260000, 11.6640625], + [1551439290000, 11.109375], + [1551439320000, 10.6015625], + [1551439350000, 11.21875], + [1551439380000, 13.53125], + [1551439410000, 15.4609375], + [1551439440000, 15.1796875], + [1551439470000, 11.984375], + [1551439500000, 24.8125], + [1551439530000, 21.46875], + [1551439560000, 14.484375], + [1551439590000, 9.9609375], + [1551439620000, 10.8515625], + [1551439650000, 12.1171875], + [1551439680000, 19.375], + [1551439710000, 20.609375], + [1551439740000, 16.484375], + [1551439770000, 15.515625], + [1551439800000, 14.9140625], + [1551439830000, 10.8828125], + [1551439860000, 9.7578125], + [1551439890000, 8.625], + [1551439920000, 9.21875], + [1551439950000, 8.5390625], + [1551439980000, 8.40625], + [1551440010000, 6.671875], + [1551440040000, 7.24609375], + [1551440070000, 7.1015625], + [1551440100000, 7.09375], + [1551440130000, 10.8125], + [1551440160000, 10.90625], + [1551440190000, 12.453125], + [1551440220000, 11.8984375], + [1551440250000, 10.875], + [1551440280000, 12.4140625], + [1551440310000, 12.78125], + [1551440340000, 34.28125], + [1551440370000, 29.84375], + [1551440400000, 22.40625], + [1551440430000, 16.046875], + [1551440460000, 12.6328125], + [1551440490000, 8.8125], + [1551440520000, 6.93359375], + [1551440550000, 6.12890625], + [1551440580000, 5.69921875], + [1551440610000, 5.48828125], + [1551440640000, 12.0234375], + [1551440670000, 14.484375], + [1551440700000, 12.890625], + [1551440730000, 11.578125], + [1551440760000, 10.7578125], + [1551440790000, 9.921875], + [1551440820000, 10.5078125], + [1551440850000, 11.375], + [1551440880000, 15.890625], + [1551440910000, 14.1953125], + [1551440940000, 11.625], + [1551440970000, 11.734375], + [1551441000000, 10.1640625], + [1551441030000, 9.296875], + [1551441060000, 7.5546875], + [1551441090000, 7.17578125], + [1551441120000, 5.8671875], + [1551441150000, 6.828125], + [1551441180000, 10.578125], + [1551441210000, 16.140625], + [1551441240000, 15.640625], + [1551441270000, 13.1484375], + [1551441300000, 11.9140625], + [1551441330000, 10.0625], + [1551441360000, 7.66015625], + [1551441390000, 9.0078125], + [1551441420000, 8.78125], + [1551441450000, 8.0390625], + [1551441480000, 25.515625], + [1551441510000, 18.640625], + [1551441540000, 13.1953125], + [1551441570000, 10.1953125], + ], + }, + { + bucket_size: '30 seconds', + timeRange: { min: 1551438000000, max: 1551441600000 }, + metric: { + app: 'kibana', + field: 'kibana_stats.os.load.5m', + metricAgg: 'max', + label: '5m', + title: 'System Load', + description: 'Load average over the last 5 minutes.', + units: '', + format: '0,0.[00]', + hasCalculation: false, + isDerivative: false, + }, + data: [ + [1551438000000, 9.984375], + [1551438030000, 9.71875], + [1551438060000, 9.28125], + [1551438090000, 9.078125], + [1551438120000, 8.9921875], + [1551438150000, 9.640625], + [1551438180000, 10.171875], + [1551438210000, 10.421875], + [1551438240000, 10.625], + [1551438270000, 10.390625], + [1551438300000, 10.4296875], + [1551438330000, 10.1328125], + [1551438360000, 9.984375], + [1551438390000, 9.8203125], + [1551438420000, 9.8125], + [1551438450000, 9.78125], + [1551438480000, 10.625], + [1551438510000, 10.09375], + [1551438540000, 9.5546875], + [1551438570000, 9.390625], + [1551438600000, 9.015625], + [1551438630000, 8.8828125], + [1551438660000, 9.359375], + [1551438690000, 9.53125], + [1551438720000, 11.9453125], + [1551438750000, 11.828125], + [1551438780000, 14.4609375], + [1551438810000, 16.421875], + [1551438840000, 15.6875], + [1551438870000, 14.5625], + [1551438900000, 13.75], + [1551438930000, 12.8359375], + [1551438960000, 12.2109375], + [1551438990000, 11.6328125], + [1551439020000, 11.015625], + [1551439050000, 10.3828125], + [1551439080000, 9.7421875], + [1551439110000, 9.046875], + [1551439140000, 9.921875], + [1551439170000, 9.890625], + [1551439200000, 10.34375], + [1551439230000, 10.140625], + [1551439260000, 10.3515625], + [1551439290000, 10.28125], + [1551439320000, 10.203125], + [1551439350000, 10.296875], + [1551439380000, 10.875], + [1551439410000, 11.4765625], + [1551439440000, 11.484375], + [1551439470000, 11.046875], + [1551439500000, 13.53125], + [1551439530000, 13.1875], + [1551439560000, 12.3046875], + [1551439590000, 11.3984375], + [1551439620000, 11.1328125], + [1551439650000, 11.390625], + [1551439680000, 13.3046875], + [1551439710000, 13.71875], + [1551439740000, 13.3671875], + [1551439770000, 13.4296875], + [1551439800000, 13.3359375], + [1551439830000, 12.4765625], + [1551439860000, 12.09375], + [1551439890000, 11.4765625], + [1551439920000, 11.328125], + [1551439950000, 10.8984375], + [1551439980000, 10.7109375], + [1551440010000, 10.0546875], + [1551440040000, 9.6328125], + [1551440070000, 9.34375], + [1551440100000, 9.1953125], + [1551440130000, 9.6328125], + [1551440160000, 9.7109375], + [1551440190000, 10.1171875], + [1551440220000, 10.171875], + [1551440250000, 10.0546875], + [1551440280000, 10.4140625], + [1551440310000, 10.5234375], + [1551440340000, 15.140625], + [1551440370000, 14.90625], + [1551440400000, 14.4921875], + [1551440430000, 13.65625], + [1551440460000, 13.0390625], + [1551440490000, 12.09375], + [1551440520000, 11.3125], + [1551440550000, 10.7265625], + [1551440580000, 10.1640625], + [1551440610000, 9.4453125], + [1551440640000, 10.546875], + [1551440670000, 11.1328125], + [1551440700000, 10.96875], + [1551440730000, 10.875], + [1551440760000, 10.7109375], + [1551440790000, 10.453125], + [1551440820000, 10.546875], + [1551440850000, 10.671875], + [1551440880000, 11.78125], + [1551440910000, 11.5703125], + [1551440940000, 11.1640625], + [1551440970000, 11.1875], + [1551441000000, 10.8671875], + [1551441030000, 10.5390625], + [1551441060000, 10.03125], + [1551441090000, 9.6640625], + [1551441120000, 9.0859375], + [1551441150000, 8.90625], + [1551441180000, 9.453125], + [1551441210000, 10.7109375], + [1551441240000, 10.734375], + [1551441270000, 10.6484375], + [1551441300000, 10.5234375], + [1551441330000, 10.1796875], + [1551441360000, 9.546875], + [1551441390000, 9.5390625], + [1551441420000, 9.3984375], + [1551441450000, 9.21875], + [1551441480000, 12.671875], + [1551441510000, 12.0859375], + [1551441540000, 11.375], + [1551441570000, 10.84375], + ], + }, + { + bucket_size: '30 seconds', + timeRange: { min: 1551438000000, max: 1551441600000 }, + metric: { + app: 'kibana', + field: 'kibana_stats.os.load.15m', + metricAgg: 'max', + label: '15m', + title: 'System Load', + description: 'Load average over the last 15 minutes.', + units: '', + format: '0,0.[00]', + hasCalculation: false, + isDerivative: false, + }, + data: [ + [1551438000000, 10.3359375], + [1551438030000, 10.234375], + [1551438060000, 10.046875], + [1551438090000, 9.9765625], + [1551438120000, 9.8828125], + [1551438150000, 10.078125], + [1551438180000, 10.234375], + [1551438210000, 10.3125], + [1551438240000, 10.3828125], + [1551438270000, 10.3046875], + [1551438300000, 10.3203125], + [1551438330000, 10.21875], + [1551438360000, 10.15625], + [1551438390000, 10.09375], + [1551438420000, 10.0859375], + [1551438450000, 10.0546875], + [1551438480000, 10.328125], + [1551438510000, 10.15625], + [1551438540000, 9.9765625], + [1551438570000, 9.8984375], + [1551438600000, 9.7578125], + [1551438630000, 9.65625], + [1551438660000, 9.796875], + [1551438690000, 9.84375], + [1551438720000, 10.640625], + [1551438750000, 10.6328125], + [1551438780000, 11.578125], + [1551438810000, 12.265625], + [1551438840000, 12.109375], + [1551438870000, 11.84375], + [1551438900000, 11.6640625], + [1551438930000, 11.421875], + [1551438960000, 11.2578125], + [1551438990000, 11.1015625], + [1551439020000, 10.90625], + [1551439050000, 10.703125], + [1551439080000, 10.4765625], + [1551439110000, 10.21875], + [1551439140000, 10.4375], + [1551439170000, 10.421875], + [1551439200000, 10.5390625], + [1551439230000, 10.4609375], + [1551439260000, 10.5234375], + [1551439290000, 10.5], + [1551439320000, 10.4609375], + [1551439350000, 10.4765625], + [1551439380000, 10.65625], + [1551439410000, 10.859375], + [1551439440000, 10.8671875], + [1551439470000, 10.734375], + [1551439500000, 11.5390625], + [1551439530000, 11.4453125], + [1551439560000, 11.203125], + [1551439590000, 10.9375], + [1551439620000, 10.859375], + [1551439650000, 10.9453125], + [1551439680000, 11.609375], + [1551439710000, 11.7578125], + [1551439740000, 11.703125], + [1551439770000, 11.8046875], + [1551439800000, 11.78125], + [1551439830000, 11.546875], + [1551439860000, 11.453125], + [1551439890000, 11.265625], + [1551439920000, 11.21875], + [1551439950000, 11.078125], + [1551439980000, 11.0078125], + [1551440010000, 10.78125], + [1551440040000, 10.6171875], + [1551440070000, 10.4921875], + [1551440100000, 10.3984375], + [1551440130000, 10.4765625], + [1551440160000, 10.4765625], + [1551440190000, 10.5859375], + [1551440220000, 10.5859375], + [1551440250000, 10.5390625], + [1551440280000, 10.625], + [1551440310000, 10.65625], + [1551440340000, 12.1328125], + [1551440370000, 12.125], + [1551440400000, 12.0390625], + [1551440430000, 11.8359375], + [1551440460000, 11.6875], + [1551440490000, 11.4140625], + [1551440520000, 11.1796875], + [1551440550000, 10.984375], + [1551440580000, 10.7890625], + [1551440610000, 10.53125], + [1551440640000, 10.8359375], + [1551440670000, 11.0234375], + [1551440700000, 10.96875], + [1551440730000, 10.9296875], + [1551440760000, 10.875], + [1551440790000, 10.7890625], + [1551440820000, 10.8046875], + [1551440850000, 10.828125], + [1551440880000, 11.1875], + [1551440910000, 11.125], + [1551440940000, 11], + [1551440970000, 11.015625], + [1551441000000, 10.9140625], + [1551441030000, 10.796875], + [1551441060000, 10.625], + [1551441090000, 10.4765625], + [1551441120000, 10.2578125], + [1551441150000, 10.1640625], + [1551441180000, 10.265625], + [1551441210000, 10.6484375], + [1551441240000, 10.65625], + [1551441270000, 10.625], + [1551441300000, 10.5859375], + [1551441330000, 10.46875], + [1551441360000, 10.2421875], + [1551441390000, 10.203125], + [1551441420000, 10.140625], + [1551441450000, 10.0625], + [1551441480000, 11.140625], + [1551441510000, 10.9921875], + [1551441540000, 10.7890625], + [1551441570000, 10.625], + ], + }, + ], + kibana_average_concurrent_connections: [ + { + bucket_size: '30 seconds', + timeRange: { min: 1551438000000, max: 1551441600000 }, + metric: { + app: 'kibana', + field: 'kibana_stats.concurrent_connections', + metricAgg: 'max', + label: 'HTTP Connections', + description: 'Total number of open socket connections to the Kibana instance.', + units: '', + format: '0.[00]', + hasCalculation: false, + isDerivative: false, + }, + data: [ + [1551438000000, 20], + [1551438030000, 18], + [1551438060000, 21], + [1551438090000, 21], + [1551438120000, 20], + [1551438150000, 19], + [1551438180000, 19], + [1551438210000, 19], + [1551438240000, 21], + [1551438270000, 21], + [1551438300000, 19], + [1551438330000, 21], + [1551438360000, 18], + [1551438390000, 18], + [1551438420000, 19], + [1551438450000, 19], + [1551438480000, 19], + [1551438510000, 19], + [1551438540000, 20], + [1551438570000, 23], + [1551438600000, 19], + [1551438630000, 21], + [1551438660000, 20], + [1551438690000, 19], + [1551438720000, 24], + [1551438750000, 20], + [1551438780000, 20], + [1551438810000, 19], + [1551438840000, 20], + [1551438870000, 21], + [1551438900000, 20], + [1551438930000, 19], + [1551438960000, 21], + [1551438990000, 20], + [1551439020000, 23], + [1551439050000, 18], + [1551439080000, 18], + [1551439110000, 18], + [1551439140000, 18], + [1551439170000, 17], + [1551439200000, 18], + [1551439230000, 19], + [1551439260000, 20], + [1551439290000, 20], + [1551439320000, 18], + [1551439350000, 20], + [1551439380000, 18], + [1551439410000, 20], + [1551439440000, 18], + [1551439470000, 19], + [1551439500000, 17], + [1551439530000, 17], + [1551439560000, 16], + [1551439590000, 15], + [1551439620000, 17], + [1551439650000, 18], + [1551439680000, 19], + [1551439710000, 20], + [1551439740000, 18], + [1551439770000, 21], + [1551439800000, 19], + [1551439830000, 20], + [1551439860000, 19], + [1551439890000, 18], + [1551439920000, 19], + [1551439950000, 19], + [1551439980000, 20], + [1551440010000, 19], + [1551440040000, 19], + [1551440070000, 19], + [1551440100000, 19], + [1551440130000, 17], + [1551440160000, 18], + [1551440190000, 18], + [1551440220000, 21], + [1551440250000, 18], + [1551440280000, 20], + [1551440310000, 17], + [1551440340000, 19], + [1551440370000, 20], + [1551440400000, 20], + [1551440430000, 20], + [1551440460000, 18], + [1551440490000, 16], + [1551440520000, 16], + [1551440550000, 17], + [1551440580000, 18], + [1551440610000, 16], + [1551440640000, 25], + [1551440670000, 16], + [1551440700000, 18], + [1551440730000, 17], + [1551440760000, 19], + [1551440790000, 17], + [1551440820000, 22], + [1551440850000, 20], + [1551440880000, 22], + [1551440910000, 31], + [1551440940000, 18], + [1551440970000, 17], + [1551441000000, 16], + [1551441030000, 17], + [1551441060000, 18], + [1551441090000, 16], + [1551441120000, 14], + [1551441150000, 14], + [1551441180000, 15], + [1551441210000, 19], + [1551441240000, 16], + [1551441270000, 17], + [1551441300000, 16], + [1551441330000, 16], + [1551441360000, 17], + [1551441390000, 18], + [1551441420000, 18], + [1551441450000, 17], + [1551441480000, 17], + [1551441510000, 16], + [1551441540000, 16], + [1551441570000, 17], + ], + }, + ], + kibana_process_delay: [ + { + bucket_size: '30 seconds', + timeRange: { min: 1551438000000, max: 1551441600000 }, + metric: { + app: 'kibana', + field: 'kibana_stats.process.event_loop_delay', + metricAgg: 'max', + label: 'Event Loop Delay', + description: 'Delay in Kibana server event loops.', + units: 'ms', + format: '0.[00]', + hasCalculation: false, + isDerivative: false, + }, + data: [ + [1551438000000, 1.917205810546875], + [1551438030000, 1.7739791870117188], + [1551438060000, 2.943075180053711], + [1551438090000, 5.207357406616211], + [1551438120000, 1.6650104522705078], + [1551438150000, 2.154033660888672], + [1551438180000, 2.118760108947754], + [1551438210000, 3.37868595123291], + [1551438240000, 2.1819963455200195], + [1551438270000, 2.568490982055664], + [1551438300000, 353.22412109375], + [1551438330000, 4.138860702514648], + [1551438360000, 1.835433006286621], + [1551438390000, 1.8117866516113281], + [1551438420000, 1.7387809753417969], + [1551438450000, 2.006241798400879], + [1551438480000, 1.8574104309082031], + [1551438510000, 2.101459503173828], + [1551438540000, 2.2292041778564453], + [1551438570000, 1.8641471862792969], + [1551438600000, 2.124319076538086], + [1551438630000, 2.1186132431030273], + [1551438660000, 1.7360162734985352], + [1551438690000, 2.1855764389038086], + [1551438720000, 3.210604667663574], + [1551438750000, 2.510148048400879], + [1551438780000, 1.6755142211914062], + [1551438810000, 1.9307565689086914], + [1551438840000, 9.354450225830078], + [1551438870000, 1.9756240844726562], + [1551438900000, 2.2764291763305664], + [1551438930000, 346.5954284667969], + [1551438960000, 4.56385612487793], + [1551438990000, 2.3435449600219727], + [1551439020000, 3.8228683471679688], + [1551439050000, 2.6483001708984375], + [1551439080000, 1.8082962036132812], + [1551439110000, 1.6172676086425781], + [1551439140000, 1.8350811004638672], + [1551439170000, 1.659804344177246], + [1551439200000, 2.4164390563964844], + [1551439230000, 1.999464988708496], + [1551439260000, 1.9630374908447266], + [1551439290000, 2.0218467712402344], + [1551439320000, 2.076573371887207], + [1551439350000, 2.4036598205566406], + [1551439380000, 2.602895736694336], + [1551439410000, 2.5561323165893555], + [1551439440000, 2.3957443237304688], + [1551439470000, 2.3182430267333984], + [1551439500000, 2.0863637924194336], + [1551439530000, 1.9851713180541992], + [1551439560000, 1.9189224243164062], + [1551439590000, 1.937936782836914], + [1551439620000, 3.844411849975586], + [1551439650000, 5.052459716796875], + [1551439680000, 2.012505531311035], + [1551439710000, 2.27213191986084], + [1551439740000, 1.951359748840332], + [1551439770000, 2.6224374771118164], + [1551439800000, 2.6582508087158203], + [1551439830000, 2.607870101928711], + [1551439860000, 2.4416723251342773], + [1551439890000, 2.369551658630371], + [1551439920000, 2.0154476165771484], + [1551439950000, 2.096695899963379], + [1551439980000, 1.9279394149780273], + [1551440010000, 3.023202896118164], + [1551440040000, 3.772576332092285], + [1551440070000, 2.4855575561523438], + [1551440100000, 4.25732421875], + [1551440130000, 2.0820703506469727], + [1551440160000, 2.489288330078125], + [1551440190000, 2.602682113647461], + [1551440220000, 4.733266830444336], + [1551440250000, 1.8897781372070312], + [1551440280000, 2.365001678466797], + [1551440310000, 2.295949935913086], + [1551440340000, 3.2801055908203125], + [1551440370000, 1.8901805877685547], + [1551440400000, 2.0735225677490234], + [1551440430000, 1.8940362930297852], + [1551440460000, 3.0348567962646484], + [1551440490000, 2.0472803115844727], + [1551440520000, 2.2077903747558594], + [1551440550000, 5.1581220626831055], + [1551440580000, 2.039125442504883], + [1551440610000, 1.6546344757080078], + [1551440640000, 3.1943721771240234], + [1551440670000, 2.0258655548095703], + [1551440700000, 2.2764244079589844], + [1551440730000, 1.8293533325195312], + [1551440760000, 2.745746612548828], + [1551440790000, 2.426107406616211], + [1551440820000, 2.497190475463867], + [1551440850000, 2.6376562118530273], + [1551440880000, 6.415732383728027], + [1551440910000, 22.362375259399414], + [1551440940000, 1.8069639205932617], + [1551440970000, 2.2364587783813477], + [1551441000000, 2.1819095611572266], + [1551441030000, 2.6490097045898438], + [1551441060000, 3.7438411712646484], + [1551441090000, 2.2370100021362305], + [1551441120000, 2.1098766326904297], + [1551441150000, 3.0454416275024414], + [1551441180000, 2.211244583129883], + [1551441210000, 2.860243797302246], + [1551441240000, 2.255979537963867], + [1551441270000, 2.0102500915527344], + [1551441300000, 2.06740665435791], + [1551441330000, 1.9841184616088867], + [1551441360000, 2.046261787414551], + [1551441390000, 5.3361921310424805], + [1551441420000, 3.9412336349487305], + [1551441450000, 2.885173797607422], + [1551441480000, 3.661712646484375], + [1551441510000, 1.8046932220458984], + [1551441540000, 1.9574308395385742], + [1551441570000, 1.9149093627929688], + ], + }, + ], + kibana_memory: [ + { + bucket_size: '30 seconds', + timeRange: { min: 1551438000000, max: 1551441600000 }, + metric: { + app: 'kibana', + field: 'kibana_stats.process.memory.heap.size_limit', + metricAgg: 'max', + label: 'Heap Size Limit', + title: 'Memory Size', + description: 'Limit of memory usage before garbage collection.', + units: 'B', + format: '0,0.0 b', + hasCalculation: false, + isDerivative: false, + }, + data: [ + [1551438000000, 1501560832], + [1551438030000, 1501560832], + [1551438060000, 1501560832], + [1551438090000, 1501560832], + [1551438120000, 1501560832], + [1551438150000, 1501560832], + [1551438180000, 1501560832], + [1551438210000, 1501560832], + [1551438240000, 1501560832], + [1551438270000, 1501560832], + [1551438300000, 1501560832], + [1551438330000, 1501560832], + [1551438360000, 1501560832], + [1551438390000, 1501560832], + [1551438420000, 1501560832], + [1551438450000, 1501560832], + [1551438480000, 1501560832], + [1551438510000, 1501560832], + [1551438540000, 1501560832], + [1551438570000, 1501560832], + [1551438600000, 1501560832], + [1551438630000, 1501560832], + [1551438660000, 1501560832], + [1551438690000, 1501560832], + [1551438720000, 1501560832], + [1551438750000, 1501560832], + [1551438780000, 1501560832], + [1551438810000, 1501560832], + [1551438840000, 1501560832], + [1551438870000, 1501560832], + [1551438900000, 1501560832], + [1551438930000, 1501560832], + [1551438960000, 1501560832], + [1551438990000, 1501560832], + [1551439020000, 1501560832], + [1551439050000, 1501560832], + [1551439080000, 1501560832], + [1551439110000, 1501560832], + [1551439140000, 1501560832], + [1551439170000, 1501560832], + [1551439200000, 1501560832], + [1551439230000, 1501560832], + [1551439260000, 1501560832], + [1551439290000, 1501560832], + [1551439320000, 1501560832], + [1551439350000, 1501560832], + [1551439380000, 1501560832], + [1551439410000, 1501560832], + [1551439440000, 1501560832], + [1551439470000, 1501560832], + [1551439500000, 1501560832], + [1551439530000, 1501560832], + [1551439560000, 1501560832], + [1551439590000, 1501560832], + [1551439620000, 1501560832], + [1551439650000, 1501560832], + [1551439680000, 1501560832], + [1551439710000, 1501560832], + [1551439740000, 1501560832], + [1551439770000, 1501560832], + [1551439800000, 1501560832], + [1551439830000, 1501560832], + [1551439860000, 1501560832], + [1551439890000, 1501560832], + [1551439920000, 1501560832], + [1551439950000, 1501560832], + [1551439980000, 1501560832], + [1551440010000, 1501560832], + [1551440040000, 1501560832], + [1551440070000, 1501560832], + [1551440100000, 1501560832], + [1551440130000, 1501560832], + [1551440160000, 1501560832], + [1551440190000, 1501560832], + [1551440220000, 1501560832], + [1551440250000, 1501560832], + [1551440280000, 1501560832], + [1551440310000, 1501560832], + [1551440340000, 1501560832], + [1551440370000, 1501560832], + [1551440400000, 1501560832], + [1551440430000, 1501560832], + [1551440460000, 1501560832], + [1551440490000, 1501560832], + [1551440520000, 1501560832], + [1551440550000, 1501560832], + [1551440580000, 1501560832], + [1551440610000, 1501560832], + [1551440640000, 1501560832], + [1551440670000, 1501560832], + [1551440700000, 1501560832], + [1551440730000, 1501560832], + [1551440760000, 1501560832], + [1551440790000, 1501560832], + [1551440820000, 1501560832], + [1551440850000, 1501560832], + [1551440880000, 1501560832], + [1551440910000, 1501560832], + [1551440940000, 1501560832], + [1551440970000, 1501560832], + [1551441000000, 1501560832], + [1551441030000, 1501560832], + [1551441060000, 1501560832], + [1551441090000, 1501560832], + [1551441120000, 1501560832], + [1551441150000, 1501560832], + [1551441180000, 1501560832], + [1551441210000, 1501560832], + [1551441240000, 1501560832], + [1551441270000, 1501560832], + [1551441300000, 1501560832], + [1551441330000, 1501560832], + [1551441360000, 1501560832], + [1551441390000, 1501560832], + [1551441420000, 1501560832], + [1551441450000, 1501560832], + [1551441480000, 1501560832], + [1551441510000, 1501560832], + [1551441540000, 1501560832], + [1551441570000, 1501560832], + ], + }, + { + bucket_size: '30 seconds', + timeRange: { min: 1551438000000, max: 1551441600000 }, + metric: { + app: 'kibana', + field: 'kibana_stats.process.memory.resident_set_size_in_bytes', + metricAgg: 'max', + label: 'Memory Size', + title: 'Memory Size', + description: 'Total heap used by Kibana running in Node.js.', + units: 'B', + format: '0,0.0 b', + hasCalculation: false, + isDerivative: false, + }, + data: [ + [1551438000000, 645689344], + [1551438030000, 642293760], + [1551438060000, 649953280], + [1551438090000, 637751296], + [1551438120000, 643112960], + [1551438150000, 642781184], + [1551438180000, 642899968], + [1551438210000, 646262784], + [1551438240000, 643276800], + [1551438270000, 663547904], + [1551438300000, 654954496], + [1551438330000, 644222976], + [1551438360000, 645672960], + [1551438390000, 649728000], + [1551438420000, 646631424], + [1551438450000, 647000064], + [1551438480000, 647114752], + [1551438510000, 648630272], + [1551438540000, 647720960], + [1551438570000, 646979584], + [1551438600000, 651296768], + [1551438630000, 642248704], + [1551438660000, 645177344], + [1551438690000, 648171520], + [1551438720000, 670048256], + [1551438750000, 653139968], + [1551438780000, 644583424], + [1551438810000, 648630272], + [1551438840000, 647925760], + [1551438870000, 648986624], + [1551438900000, 644399104], + [1551438930000, 636719104], + [1551438960000, 650260480], + [1551438990000, 666669056], + [1551439020000, 661057536], + [1551439050000, 649957376], + [1551439080000, 655093760], + [1551439110000, 647913472], + [1551439140000, 642232320], + [1551439170000, 642490368], + [1551439200000, 643133440], + [1551439230000, 640008192], + [1551439260000, 648654848], + [1551439290000, 643506176], + [1551439320000, 647127040], + [1551439350000, 662966272], + [1551439380000, 646635520], + [1551439410000, 641519616], + [1551439440000, 658214912], + [1551439470000, 666677248], + [1551439500000, 651583488], + [1551439530000, 652963840], + [1551439560000, 662065152], + [1551439590000, 662417408], + [1551439620000, 665919488], + [1551439650000, 646316032], + [1551439680000, 643153920], + [1551439710000, 658288640], + [1551439740000, 662052864], + [1551439770000, 660353024], + [1551439800000, 649293824], + [1551439830000, 661753856], + [1551439860000, 663977984], + [1551439890000, 658100224], + [1551439920000, 657711104], + [1551439950000, 645820416], + [1551439980000, 648531968], + [1551440010000, 644272128], + [1551440040000, 649019392], + [1551440070000, 656228352], + [1551440100000, 643280896], + [1551440130000, 645763072], + [1551440160000, 649703424], + [1551440190000, 653647872], + [1551440220000, 647307264], + [1551440250000, 685629440], + [1551440280000, 711311360], + [1551440310000, 648220672], + [1551440340000, 645439488], + [1551440370000, 646225920], + [1551440400000, 650481664], + [1551440430000, 650178560], + [1551440460000, 651149312], + [1551440490000, 653852672], + [1551440520000, 651427840], + [1551440550000, 652050432], + [1551440580000, 646492160], + [1551440610000, 650129408], + [1551440640000, 659320832], + [1551440670000, 653029376], + [1551440700000, 661946368], + [1551440730000, 652808192], + [1551440760000, 656105472], + [1551440790000, 654086144], + [1551440820000, 649216000], + [1551440850000, 660570112], + [1551440880000, 663560192], + [1551440910000, 663306240], + [1551440940000, 750891008], + [1551440970000, 802795520], + [1551441000000, 682393600], + [1551441030000, 646311936], + [1551441060000, 648204288], + [1551441090000, 650903552], + [1551441120000, 648257536], + [1551441150000, 649920512], + [1551441180000, 643801088], + [1551441210000, 653606912], + [1551441240000, 654901248], + [1551441270000, 644833280], + [1551441300000, 658534400], + [1551441330000, 655249408], + [1551441360000, 647827456], + [1551441390000, 655147008], + [1551441420000, 652087296], + [1551441450000, 655159296], + [1551441480000, 655753216], + [1551441510000, 666820608], + [1551441540000, 662138880], + [1551441570000, 651341824], + ], + }, + ], + kibana_response_times: [ + { + bucket_size: '30 seconds', + timeRange: { min: 1551438000000, max: 1551441600000 }, + metric: { + app: 'kibana', + field: 'kibana_stats.response_times.max', + metricAgg: 'max', + label: 'Max', + title: 'Client Response Time', + description: 'Maximum response time for client requests to the Kibana instance.', + units: 'ms', + format: '0.[00]', + hasCalculation: false, + isDerivative: false, + }, + data: [ + [1551438000000, 11637], + [1551438030000, 12188], + [1551438060000, 11802], + [1551438090000, 11907], + [1551438120000, 11875], + [1551438150000, 11835], + [1551438180000, 12077], + [1551438210000, 11827], + [1551438240000, 11870], + [1551438270000, 11897], + [1551438300000, 12085], + [1551438330000, 11829], + [1551438360000, 11804], + [1551438390000, 12040], + [1551438420000, 11906], + [1551438450000, 11967], + [1551438480000, 11798], + [1551438510000, 11587], + [1551438540000, 11766], + [1551438570000, 11763], + [1551438600000, 11848], + [1551438630000, 12061], + [1551438660000, 11922], + [1551438690000, 11805], + [1551438720000, 12194], + [1551438750000, 11784], + [1551438780000, 12097], + [1551438810000, 11984], + [1551438840000, 11955], + [1551438870000, 11378], + [1551438900000, 12047], + [1551438930000, 11989], + [1551438960000, 11925], + [1551438990000, 12009], + [1551439020000, 11806], + [1551439050000, 11761], + [1551439080000, 11808], + [1551439110000, 11979], + [1551439140000, 11945], + [1551439170000, 11769], + [1551439200000, 12010], + [1551439230000, 11746], + [1551439260000, 12176], + [1551439290000, 12253], + [1551439320000, 12093], + [1551439350000, 12034], + [1551439380000, 12114], + [1551439410000, 12135], + [1551439440000, 12193], + [1551439470000, 12138], + [1551439500000, 12116], + [1551439530000, 11849], + [1551439560000, 12335], + [1551439590000, 11988], + [1551439620000, 12077], + [1551439650000, 11841], + [1551439680000, 11956], + [1551439710000, 11798], + [1551439740000, 11939], + [1551439770000, 11929], + [1551439800000, 11894], + [1551439830000, 11962], + [1551439860000, 12348], + [1551439890000, 11939], + [1551439920000, 12005], + [1551439950000, 12550], + [1551439980000, 11935], + [1551440010000, 11931], + [1551440040000, 11814], + [1551440070000, 11703], + [1551440100000, 11990], + [1551440130000, 12050], + [1551440160000, 11971], + [1551440190000, 11720], + [1551440220000, 12085], + [1551440250000, 11919], + [1551440280000, 12551], + [1551440310000, 11873], + [1551440340000, 11599], + [1551440370000, 11977], + [1551440400000, 12002], + [1551440430000, 11757], + [1551440460000, 11657], + [1551440490000, 11291], + [1551440520000, 11943], + [1551440550000, 12223], + [1551440580000, 1482], + [1551440610000, 12060], + [1551440640000, 12016], + [1551440670000, 12318], + [1551440700000, 11969], + [1551440730000, 11974], + [1551440760000, 11779], + [1551440790000, 11868], + [1551440820000, 12295], + [1551440850000, 12077], + [1551440880000, 12127], + [1551440910000, 12075], + [1551440940000, 12472], + [1551440970000, 11715], + [1551441000000, 12036], + [1551441030000, 12020], + [1551441060000, 12816], + [1551441090000, 12644], + [1551441120000, 11907], + [1551441150000, 11945], + [1551441180000, 12083], + [1551441210000, 11998], + [1551441240000, 12259], + [1551441270000, 11516], + [1551441300000, 11969], + [1551441330000, 12053], + [1551441360000, 12002], + [1551441390000, 12016], + [1551441420000, 12146], + [1551441450000, 11904], + [1551441480000, 11942], + [1551441510000, 12139], + [1551441540000, 11966], + [1551441570000, 12051], + ], + }, + { + bucket_size: '30 seconds', + timeRange: { min: 1551438000000, max: 1551441600000 }, + metric: { + app: 'kibana', + field: 'kibana_stats.response_times.average', + metricAgg: 'max', + label: 'Average', + title: 'Client Response Time', + description: 'Average response time for client requests to the Kibana instance.', + units: 'ms', + format: '0.[00]', + hasCalculation: false, + isDerivative: false, + }, + data: [ + [1551438000000, 237.5], + [1551438030000, 606.875], + [1551438060000, 514.413818359375], + [1551438090000, 722.0555419921875], + [1551438120000, 512], + [1551438150000, 294.32073974609375], + [1551438180000, 538.6333618164062], + [1551438210000, 720.8333129882812], + [1551438240000, 468.56097412109375], + [1551438270000, 559.5184936523438], + [1551438300000, 549.888916015625], + [1551438330000, 636.6190185546875], + [1551438360000, 626.6818237304688], + [1551438390000, 578.8846435546875], + [1551438420000, 562.4615478515625], + [1551438450000, 582.75], + [1551438480000, 491.0967712402344], + [1551438510000, 527.5357055664062], + [1551438540000, 436.25714111328125], + [1551438570000, 504.17242431640625], + [1551438600000, 527.8928833007812], + [1551438630000, 419.20513916015625], + [1551438660000, 494.258056640625], + [1551438690000, 529.5184936523438], + [1551438720000, 511.2257995605469], + [1551438750000, 297.3492126464844], + [1551438780000, 622.3666381835938], + [1551438810000, 284.5199890136719], + [1551438840000, 805.5555419921875], + [1551438870000, 519.2857055664062], + [1551438900000, 609.4583129882812], + [1551438930000, 278.6333312988281], + [1551438960000, 537.3461303710938], + [1551438990000, 522.0344848632812], + [1551439020000, 440.8571472167969], + [1551439050000, 518], + [1551439080000, 232.17808532714844], + [1551439110000, 494.0322570800781], + [1551439140000, 597], + [1551439170000, 297.7321472167969], + [1551439200000, 552.2857055664062], + [1551439230000, 270.17742919921875], + [1551439260000, 563.3076782226562], + [1551439290000, 591.375], + [1551439320000, 613.0370483398438], + [1551439350000, 557.1142578125], + [1551439380000, 474.875], + [1551439410000, 282.0428466796875], + [1551439440000, 285.3492126464844], + [1551439470000, 466.2894592285156], + [1551439500000, 452.3714294433594], + [1551439530000, 445.9459533691406], + [1551439560000, 410.5], + [1551439590000, 434.6486511230469], + [1551439620000, 243.028564453125], + [1551439650000, 477.125], + [1551439680000, 245.6666717529297], + [1551439710000, 266.63934326171875], + [1551439740000, 398.4285583496094], + [1551439770000, 395.9761962890625], + [1551439800000, 233.9545440673828], + [1551439830000, 427.76922607421875], + [1551439860000, 366.05999755859375], + [1551439890000, 514], + [1551439920000, 421.452392578125], + [1551439950000, 445.5128173828125], + [1551439980000, 279.76812744140625], + [1551440010000, 225.01123046875], + [1551440040000, 520.6551513671875], + [1551440070000, 525], + [1551440100000, 317.6000061035156], + [1551440130000, 432.9756164550781], + [1551440160000, 454.4210510253906], + [1551440190000, 507.625], + [1551440220000, 300.015869140625], + [1551440250000, 512.625], + [1551440280000, 476.7250061035156], + [1551440310000, 485.8484802246094], + [1551440340000, 446.97222900390625], + [1551440370000, 519.2142944335938], + [1551440400000, 461.941162109375], + [1551440430000, 534.74072265625], + [1551440460000, 498.258056640625], + [1551440490000, 515.6666870117188], + [1551440520000, 564.137939453125], + [1551440550000, 515.933349609375], + [1551440580000, 148.50877380371094], + [1551440610000, 445.6388854980469], + [1551440640000, 569.923095703125], + [1551440670000, 328.4313659667969], + [1551440700000, 386.47369384765625], + [1551440730000, 514.6785888671875], + [1551440760000, 667.6818237304688], + [1551440790000, 589.6153564453125], + [1551440820000, 337.5918273925781], + [1551440850000, 435.868408203125], + [1551440880000, 569.774169921875], + [1551440910000, 532.0714111328125], + [1551440940000, 365.9818115234375], + [1551440970000, 616.0833129882812], + [1551441000000, 521.8965454101562], + [1551441030000, 557.1851806640625], + [1551441060000, 540.3793334960938], + [1551441090000, 588.076904296875], + [1551441120000, 610.2000122070312], + [1551441150000, 621.3333129882812], + [1551441180000, 534], + [1551441210000, 533.8571166992188], + [1551441240000, 459.8611145019531], + [1551441270000, 508.96429443359375], + [1551441300000, 556.8148193359375], + [1551441330000, 614.8695678710938], + [1551441360000, 469.8285827636719], + [1551441390000, 484.18182373046875], + [1551441420000, 644.09521484375], + [1551441450000, 524], + [1551441480000, 719.5789184570312], + [1551441510000, 564.4000244140625], + [1551441540000, 561.0740966796875], + [1551441570000, 329.6833190917969], + ], + }, + ], + kibana_requests: [ + { + bucket_size: '30 seconds', + timeRange: { min: 1551438000000, max: 1551441600000 }, + metric: { + app: 'kibana', + field: 'kibana_stats.requests.total', + metricAgg: 'max', + label: 'Client Requests', + description: 'Total number of client requests received by the Kibana instance.', + units: '', + format: '0.[00]', + hasCalculation: false, + isDerivative: false, + }, + data: [ + [1551438000000, 62], + [1551438030000, 43], + [1551438060000, 64], + [1551438090000, 60], + [1551438120000, 44], + [1551438150000, 56], + [1551438180000, 32], + [1551438210000, 36], + [1551438240000, 56], + [1551438270000, 83], + [1551438300000, 59], + [1551438330000, 62], + [1551438360000, 34], + [1551438390000, 35], + [1551438420000, 30], + [1551438450000, 30], + [1551438480000, 32], + [1551438510000, 30], + [1551438540000, 62], + [1551438570000, 37], + [1551438600000, 33], + [1551438630000, 56], + [1551438660000, 36], + [1551438690000, 31], + [1551438720000, 85], + [1551438750000, 75], + [1551438780000, 53], + [1551438810000, 48], + [1551438840000, 39], + [1551438870000, 57], + [1551438900000, 37], + [1551438930000, 58], + [1551438960000, 91], + [1551438990000, 65], + [1551439020000, 43], + [1551439050000, 35], + [1551439080000, 71], + [1551439110000, 62], + [1551439140000, 34], + [1551439170000, 55], + [1551439200000, 61], + [1551439230000, 74], + [1551439260000, 62], + [1551439290000, 53], + [1551439320000, 97], + [1551439350000, 92], + [1551439380000, 37], + [1551439410000, 71], + [1551439440000, 64], + [1551439470000, 39], + [1551439500000, 60], + [1551439530000, 56], + [1551439560000, 39], + [1551439590000, 36], + [1551439620000, 70], + [1551439650000, 47], + [1551439680000, 66], + [1551439710000, 124], + [1551439740000, 52], + [1551439770000, 98], + [1551439800000, 86], + [1551439830000, 97], + [1551439860000, 50], + [1551439890000, 36], + [1551439920000, 42], + [1551439950000, 41], + [1551439980000, 72], + [1551440010000, 93], + [1551440040000, 53], + [1551440070000, 33], + [1551440100000, 66], + [1551440130000, 43], + [1551440160000, 44], + [1551440190000, 34], + [1551440220000, 104], + [1551440250000, 50], + [1551440280000, 38], + [1551440310000, 35], + [1551440340000, 37], + [1551440370000, 37], + [1551440400000, 40], + [1551440430000, 33], + [1551440460000, 34], + [1551440490000, 31], + [1551440520000, 35], + [1551440550000, 41], + [1551440580000, 59], + [1551440610000, 36], + [1551440640000, 80], + [1551440670000, 41], + [1551440700000, 53], + [1551440730000, 106], + [1551440760000, 62], + [1551440790000, 90], + [1551440820000, 67], + [1551440850000, 107], + [1551440880000, 83], + [1551440910000, 74], + [1551440940000, 91], + [1551440970000, 57], + [1551441000000, 36], + [1551441030000, 33], + [1551441060000, 55], + [1551441090000, 41], + [1551441120000, 32], + [1551441150000, 38], + [1551441180000, 57], + [1551441210000, 57], + [1551441240000, 43], + [1551441270000, 41], + [1551441300000, 34], + [1551441330000, 59], + [1551441360000, 36], + [1551441390000, 71], + [1551441420000, 55], + [1551441450000, 43], + [1551441480000, 67], + [1551441510000, 81], + [1551441540000, 58], + [1551441570000, 97], + ], + }, + ], + }, + kibanaSummary: { + uuid: '38cd1f5c-fc29-492e-b5b6-34777df7bdf6', + name: 'demo.elastic.co', + index: '.kibana', + host: '0.0.0.0', + transport_address: '0.0.0.0:5601', + version: '6.5.4', + snapshot: false, + status: 'green', + availability: true, + os_memory_free: 74041475072, + uptime: 243193903, + }, +}; diff --git a/packages/osd-charts/src/utils/data_samples/test_dataset_random.ts b/packages/osd-charts/src/utils/data_samples/test_dataset_random.ts new file mode 100644 index 000000000000..ca396eb9325a --- /dev/null +++ b/packages/osd-charts/src/utils/data_samples/test_dataset_random.ts @@ -0,0 +1,930 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const SINGLE_SERIES_DATASET_50 = [ + { + x: 0, + y: 4.357224066592332, + }, + { + x: 1, + y: 6.682038813036171, + }, + { + x: 2, + y: 7.440123004562445, + }, + { + x: 3, + y: 7.471251056610878, + }, + { + x: 4, + y: 4.68938316540927, + }, + { + x: 5, + y: 5.6186895345993095, + }, + { + x: 6, + y: 3.7432491152296934, + }, + { + x: 7, + y: 3.794646702960704, + }, + { + x: 8, + y: 6.171645140817315, + }, + { + x: 9, + y: 7.619627749843373, + }, + { + x: 10, + y: 4.2661698352435415, + }, + { + x: 11, + y: 5.88806060184687, + }, + { + x: 12, + y: 3.1777440603440716, + }, + { + x: 13, + y: 6.586470755360233, + }, + { + x: 14, + y: 5.60642995599288, + }, + { + x: 15, + y: 7.892213761954912, + }, + { + x: 16, + y: 4.227071447633065, + }, + { + x: 17, + y: 4.655650084389816, + }, + { + x: 18, + y: 4.391515289288547, + }, + { + x: 19, + y: 7.026918799713926, + }, + { + x: 20, + y: 4.020886038246215, + }, + { + x: 21, + y: 5.578690494143906, + }, + { + x: 22, + y: 5.56569471896756, + }, + { + x: 23, + y: 5.643403572631118, + }, + { + x: 24, + y: 7.131550668690907, + }, + { + x: 25, + y: 6.454040119785386, + }, + { + x: 26, + y: 6.06808645164466, + }, + { + x: 27, + y: 5.930937833725105, + }, + { + x: 28, + y: 7.039403044956153, + }, + { + x: 29, + y: 7.504039342944911, + }, + { + x: 30, + y: 5.9355670385198716, + }, + { + x: 31, + y: 4.874735945939797, + }, + { + x: 32, + y: 3.977088912846605, + }, + { + x: 33, + y: 3.7679627333272583, + }, + { + x: 34, + y: 6.054007769786198, + }, + { + x: 35, + y: 6.629107537550166, + }, + { + x: 36, + y: 6.14681017608612, + }, + { + x: 37, + y: 5.404728378363938, + }, + { + x: 38, + y: 6.413484580429399, + }, + { + x: 39, + y: 4.1607221550741675, + }, + { + x: 40, + y: 5.002360688411349, + }, + { + x: 41, + y: 5.778551597324432, + }, + { + x: 42, + y: 4.015401189641729, + }, + { + x: 43, + y: 4.257407586191913, + }, + { + x: 44, + y: 6.862217539119293, + }, + { + x: 45, + y: 5.1984045235473015, + }, + { + x: 46, + y: 6.675361070293565, + }, + { + x: 47, + y: 7.384560357913382, + }, + { + x: 48, + y: 6.201749961555677, + }, + { + x: 49, + y: 6.5609660774366585, + }, +]; + +/** @internal */ +export const SINGLE_SERIES_DATASET_50_2 = [ + { + x: 0, + y: 4.834074829815021, + }, + { + x: 1, + y: 4.866078018803835, + }, + { + x: 2, + y: 4.945768326296559, + }, + { + x: 3, + y: 5.3491806535537245, + }, + { + x: 4, + y: 5.37990817212068, + }, + { + x: 5, + y: 4.102843147085189, + }, + { + x: 6, + y: 7.332902679390628, + }, + { + x: 7, + y: 7.11330118562733, + }, + { + x: 8, + y: 5.54727600019827, + }, + { + x: 9, + y: 5.041890303289726, + }, + { + x: 10, + y: 3.7036131105388677, + }, + { + x: 11, + y: 5.024223707266038, + }, + { + x: 12, + y: 7.5418638671700275, + }, + { + x: 13, + y: 5.349283389246464, + }, + { + x: 14, + y: 4.058450923164113, + }, + { + x: 15, + y: 6.022074500533666, + }, + { + x: 16, + y: 6.0684061040686474, + }, + { + x: 17, + y: 4.351429111718383, + }, + { + x: 18, + y: 3.671595435394179, + }, + { + x: 19, + y: 4.112503219598622, + }, + { + x: 20, + y: 5.878511290461774, + }, + { + x: 21, + y: 3.71505981064523, + }, + { + x: 22, + y: 4.011678558871822, + }, + { + x: 23, + y: 3.2384216500652974, + }, + { + x: 24, + y: 3.829766422371897, + }, + { + x: 25, + y: 6.937321483432921, + }, + { + x: 26, + y: 4.292032722165884, + }, + { + x: 27, + y: 3.48809711524125, + }, + { + x: 28, + y: 4.621186370600406, + }, + { + x: 29, + y: 6.108891958616232, + }, + { + x: 30, + y: 5.830841187967463, + }, + { + x: 31, + y: 5.505736667568632, + }, + { + x: 32, + y: 4.268392919594471, + }, + { + x: 33, + y: 4.200461047033274, + }, + { + x: 34, + y: 5.068953733097461, + }, + { + x: 35, + y: 5.119758268175488, + }, + { + x: 36, + y: 6.6328978199443185, + }, + { + x: 37, + y: 7.277980507949751, + }, + { + x: 38, + y: 3.26567572870178, + }, + { + x: 39, + y: 5.204356595570733, + }, + { + x: 40, + y: 6.6588255656797015, + }, + { + x: 41, + y: 5.5883166863541955, + }, + { + x: 42, + y: 6.630111155163229, + }, + { + x: 43, + y: 7.607666390825341, + }, + { + x: 44, + y: 6.096484268224059, + }, + { + x: 45, + y: 7.437401969525534, + }, + { + x: 46, + y: 4.235551840511664, + }, + { + x: 47, + y: 6.61286880018545, + }, + { + x: 48, + y: 6.930610905119231, + }, + { + x: 49, + y: 4.502018263660976, + }, +]; + +/** @internal */ +export const GROUPED_DATASERIES_RANDOM_50 = [ + { + x: 0, + y: 5.2255617936650545, + g: 0, + }, + { + x: 1, + y: 3.7102673876165904, + g: 0, + }, + { + x: 2, + y: 3.889136296022602, + g: 0, + }, + { + x: 3, + y: 5.859315010722412, + g: 0, + }, + { + x: 4, + y: 5.830369432621481, + g: 0, + }, + { + x: 5, + y: 7.199393653295137, + g: 0, + }, + { + x: 6, + y: 6.49132574733884, + g: 0, + }, + { + x: 7, + y: 6.5359843818146475, + g: 0, + }, + { + x: 8, + y: 4.596214706609553, + g: 0, + }, + { + x: 9, + y: 6.425318731039043, + g: 0, + }, + { + x: 10, + y: 6.124529196591078, + g: 0, + }, + { + x: 11, + y: 5.06913248004545, + g: 0, + }, + { + x: 12, + y: 3.587300228160039, + g: 0, + }, + { + x: 13, + y: 3.452258170971803, + g: 0, + }, + { + x: 14, + y: 5.2323987755805685, + g: 0, + }, + { + x: 15, + y: 7.692435125684049, + g: 0, + }, + { + x: 16, + y: 4.785736211865901, + g: 0, + }, + { + x: 17, + y: 3.3296638770667366, + g: 0, + }, + { + x: 18, + y: 5.896450770695756, + g: 0, + }, + { + x: 19, + y: 6.64614655271855, + g: 0, + }, + { + x: 20, + y: 3.4704425535092964, + g: 0, + }, + { + x: 21, + y: 7.305623563920735, + g: 0, + }, + { + x: 22, + y: 6.886365541869464, + g: 0, + }, + { + x: 23, + y: 4.693312767286267, + g: 0, + }, + { + x: 24, + y: 5.242193735602951, + g: 0, + }, + { + x: 25, + y: 7.231731024961228, + g: 0, + }, + { + x: 26, + y: 5.162517614459241, + g: 0, + }, + { + x: 27, + y: 4.331045596580639, + g: 0, + }, + { + x: 28, + y: 7.330614167663976, + g: 0, + }, + { + x: 29, + y: 6.168395207037538, + g: 0, + }, + { + x: 30, + y: 3.990490431889054, + g: 0, + }, + { + x: 31, + y: 4.665850352371473, + g: 0, + }, + { + x: 32, + y: 4.553555090636245, + g: 0, + }, + { + x: 33, + y: 3.572894589531606, + g: 0, + }, + { + x: 34, + y: 4.890569189439778, + g: 0, + }, + { + x: 35, + y: 3.9125868633580074, + g: 0, + }, + { + x: 36, + y: 7.267444727537624, + g: 0, + }, + { + x: 37, + y: 3.4872349959836413, + g: 0, + }, + { + x: 38, + y: 5.876459163716127, + g: 0, + }, + { + x: 39, + y: 4.409497201311777, + g: 0, + }, + { + x: 40, + y: 5.292344549279131, + g: 0, + }, + { + x: 41, + y: 4.787980500620099, + g: 0, + }, + { + x: 42, + y: 3.780799679518473, + g: 0, + }, + { + x: 43, + y: 4.208518928111873, + g: 0, + }, + { + x: 44, + y: 4.929162762926002, + g: 0, + }, + { + x: 45, + y: 7.516488713070287, + g: 0, + }, + { + x: 46, + y: 5.170915061096178, + g: 0, + }, + { + x: 47, + y: 4.230718829595173, + g: 0, + }, + { + x: 48, + y: 7.027299967902843, + g: 0, + }, + { + x: 49, + y: 3.684224297190106, + g: 0, + }, + { + x: 0, + y: 5.158858398273338, + g: 1, + }, + { + x: 1, + y: 5.825307406999084, + g: 1, + }, + { + x: 2, + y: 6.731433393148501, + g: 1, + }, + { + x: 3, + y: 6.385077132293345, + g: 1, + }, + { + x: 4, + y: 5.30739929091002, + g: 1, + }, + { + x: 5, + y: 5.562341605806733, + g: 1, + }, + { + x: 6, + y: 7.177878922999808, + g: 1, + }, + { + x: 7, + y: 4.094646262106314, + g: 1, + }, + { + x: 8, + y: 6.251405390959935, + g: 1, + }, + { + x: 9, + y: 4.892587816041948, + g: 1, + }, + { + x: 10, + y: 5.152398470669144, + g: 1, + }, + { + x: 11, + y: 4.903262982811753, + g: 1, + }, + { + x: 12, + y: 3.6652086312113243, + g: 1, + }, + { + x: 13, + y: 7.162095714262813, + g: 1, + }, + { + x: 14, + y: 6.801033803640274, + g: 1, + }, + { + x: 15, + y: 3.789798798684692, + g: 1, + }, + { + x: 16, + y: 4.3894105431485135, + g: 1, + }, + { + x: 17, + y: 4.488862260209444, + g: 1, + }, + { + x: 18, + y: 4.859801693597622, + g: 1, + }, + { + x: 19, + y: 7.381516229848886, + g: 1, + }, + { + x: 20, + y: 7.487189792351412, + g: 1, + }, + { + x: 21, + y: 3.584291847877211, + g: 1, + }, + { + x: 22, + y: 3.541986413332105, + g: 1, + }, + { + x: 23, + y: 6.9336323025914, + g: 1, + }, + { + x: 24, + y: 5.298346065679133, + g: 1, + }, + { + x: 25, + y: 4.2339142803773715, + g: 1, + }, + { + x: 26, + y: 3.9804879265331645, + g: 1, + }, + { + x: 27, + y: 4.499871689649284, + g: 1, + }, + { + x: 28, + y: 6.81727462740658, + g: 1, + }, + { + x: 29, + y: 5.250076673298942, + g: 1, + }, + { + x: 30, + y: 4.940770954313113, + g: 1, + }, + { + x: 31, + y: 6.374738479570077, + g: 1, + }, + { + x: 32, + y: 6.283596236876132, + g: 1, + }, + { + x: 33, + y: 5.8457101714766715, + g: 1, + }, + { + x: 34, + y: 4.684371878306671, + g: 1, + }, + { + x: 35, + y: 5.208338893583481, + g: 1, + }, + { + x: 36, + y: 5.124452484451909, + g: 1, + }, + { + x: 37, + y: 5.038808234604569, + g: 1, + }, + { + x: 38, + y: 4.9490119117824705, + g: 1, + }, + { + x: 39, + y: 4.600413173605566, + g: 1, + }, + { + x: 40, + y: 7.921347037457185, + g: 1, + }, + { + x: 41, + y: 4.28252759028966, + g: 1, + }, + { + x: 42, + y: 6.780849625067404, + g: 1, + }, + { + x: 43, + y: 6.81376283232035, + g: 1, + }, + { + x: 44, + y: 4.897646941624636, + g: 1, + }, + { + x: 45, + y: 6.668978089890552, + g: 1, + }, + { + x: 46, + y: 7.490998943148998, + g: 1, + }, + { + x: 47, + y: 4.379737550640408, + g: 1, + }, + { + x: 48, + y: 5.086746805487076, + g: 1, + }, + { + x: 49, + y: 6.549400675200497, + g: 1, + }, +]; diff --git a/packages/osd-charts/src/utils/data_samples/test_dataset_tsvb.ts b/packages/osd-charts/src/utils/data_samples/test_dataset_tsvb.ts new file mode 100644 index 000000000000..88aa8efbe744 --- /dev/null +++ b/packages/osd-charts/src/utils/data_samples/test_dataset_tsvb.ts @@ -0,0 +1,1075 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const TSVB_DATASET = { + id: '61ca57f0-469d-11e7-af02-69e470af7417', + series: [ + { + id: '61ca57f1-469d-11e7-af02-69e470af7417:jpg', + label: 'jpg', + color: 'rgb(104, 188, 0)', + data: [ + [1552830600000, 0], + [1552831200000, 0], + [1552831800000, 0], + [1552832400000, 0], + [1552833000000, 0], + [1552833600000, 0], + [1552834200000, 0], + [1552834800000, 0], + [1552835400000, 0], + [1552836000000, 0], + [1552836600000, 0], + [1552837200000, 0], + [1552837800000, 0], + [1552838400000, 0], + [1552839000000, 0], + [1552839600000, 0], + [1552840200000, 0], + [1552840800000, 0], + [1552841400000, 0], + [1552842000000, 0], + [1552842600000, 0], + [1552843200000, 0], + [1552843800000, 0], + [1552844400000, 0], + [1552845000000, 0], + [1552845600000, 0], + [1552846200000, 0], + [1552846800000, 0], + [1552847400000, 0], + [1552848000000, 0], + [1552848600000, 0], + [1552849200000, 0], + [1552849800000, 0], + [1552850400000, 0], + [1552851000000, 0], + [1552851600000, 0], + [1552852200000, 0], + [1552852800000, 0], + [1552853400000, 0], + [1552854000000, 0], + [1552854600000, 0], + [1552855200000, 0], + [1552855800000, 0], + [1552856400000, 6], + [1552857000000, 0], + [1552857600000, 0], + [1552858200000, 0], + [1552858800000, 1], + [1552859400000, 1], + [1552860000000, 0], + [1552860600000, 1], + [1552861200000, 1], + [1552861800000, 1], + [1552862400000, 0], + [1552863000000, 2], + [1552863600000, 1], + [1552864200000, 0], + [1552864800000, 1], + [1552865400000, 4], + [1552866000000, 6], + [1552866600000, 6], + [1552867200000, 4], + [1552867800000, 7], + [1552868400000, 3], + [1552869000000, 20], + [1552869600000, 30], + [1552870200000, 40], + [1552870800000, null], + [1552871400000, 5], + [1552872000000, 6], + [1552872600000, 12], + [1552873200000, 0], + [1552873800000, 10], + [1552874400000, 10], + [1552875000000, 13], + [1552875600000, 8], + [1552876200000, 13], + [1552876800000, 10], + [1552877400000, 10], + [1552878000000, 20], + [1552878600000, 16], + [1552879200000, 16], + [1552879800000, 15], + [1552880400000, 21], + [1552881000000, 19], + [1552881600000, 29], + [1552882200000, 31], + [1552882800000, 28], + [1552883400000, 33], + [1552884000000, 30], + [1552884600000, 29], + [1552885200000, 24], + [1552885800000, 35], + [1552886400000, 33], + [1552887000000, 32], + [1552887600000, 28], + [1552888200000, 48], + [1552888800000, 44], + [1552889400000, 26], + [1552890000000, 44], + [1552890600000, 65], + [1552891200000, 42], + [1552891800000, 49], + [1552892400000, 51], + [1552893000000, 59], + [1552893600000, 47], + [1552894200000, 47], + [1552894800000, 54], + [1552895400000, 48], + [1552896000000, 61], + [1552896600000, 63], + [1552897200000, 59], + [1552897800000, 58], + [1552898400000, 52], + [1552899000000, 61], + [1552899600000, 42], + [1552900200000, 48], + [1552900800000, 47], + [1552901400000, 52], + [1552902000000, 67], + [1552902600000, 49], + [1552903200000, 56], + [1552903800000, 36], + [1552904400000, 38], + [1552905000000, 42], + [1552905600000, 59], + [1552906200000, 41], + [1552906800000, 32], + [1552907400000, 48], + [1552908000000, 41], + [1552908600000, 41], + [1552909200000, 44], + [1552909800000, 46], + [1552910400000, 32], + [1552911000000, 40], + [1552911600000, 33], + [1552912200000, 30], + [1552912800000, 33], + [1552913400000, 31], + [1552914000000, 35], + [1552914600000, 22], + [1552915200000, 26], + [1552915800000, 22], + [1552916400000, 14], + [1552917000000, 22], + [1552917600000, 20], + [1552918200000, 13], + [1552918800000, 21], + [1552919400000, 14], + [1552920000000, 15], + [1552920600000, 13], + [1552921200000, 16], + [1552921800000, 14], + [1552922400000, 12], + [1552923000000, 10], + [1552923600000, 7], + [1552924200000, 12], + [1552924800000, 3], + [1552925400000, 0], + [1552926000000, 3], + [1552926600000, 6], + [1552927200000, 6], + [1552927800000, 7], + [1552928400000, 10], + [1552929000000, 4], + [1552929600000, 6], + [1552930200000, 9], + [1552930800000, 4], + [1552931400000, 2], + [1552932000000, 1], + [1552932600000, 3], + [1552933200000, 4], + [1552933800000, 6], + [1552934400000, 1], + [1552935000000, 0], + [1552935600000, 0], + [1552936200000, 0], + [1552936800000, 1], + [1552937400000, 1], + [1552938000000, 0], + [1552938600000, 1], + [1552939200000, 0], + [1552939800000, 0], + [1552940400000, 1], + [1552941000000, 0], + [1552941600000, 0], + [1552942200000, 0], + [1552942800000, 2], + [1552943400000, 0], + [1552944000000, 0], + [1552944600000, 1], + [1552945200000, 0], + [1552945800000, 2], + [1552946400000, 1], + [1552947000000, 0], + [1552947600000, 1], + [1552948200000, 1], + [1552948800000, 1], + [1552949400000, 2], + ], + stack: false, + lines: { show: true, fill: 0.5, lineWidth: 1, steps: false }, + points: { show: true, radius: 1, lineWidth: 1 }, + bars: { show: false, fill: 0.5, lineWidth: 1 }, + }, + { + id: '61ca57f1-469d-11e7-af02-69e470af7417:css', + label: 'css', + color: 'rgb(77, 138, 0)', + data: [ + [1552830600000, 0], + [1552831200000, 0], + [1552831800000, 0], + [1552832400000, 0], + [1552833000000, 0], + [1552833600000, 0], + [1552834200000, 0], + [1552834800000, 0], + [1552835400000, 0], + [1552836000000, 0], + [1552836600000, 0], + [1552837200000, 0], + [1552837800000, 0], + [1552838400000, 0], + [1552839000000, 0], + [1552839600000, 0], + [1552840200000, 0], + [1552840800000, 0], + [1552841400000, 0], + [1552842000000, 0], + [1552842600000, 0], + [1552843200000, 0], + [1552843800000, 0], + [1552844400000, 0], + [1552845000000, 0], + [1552845600000, 0], + [1552846200000, 0], + [1552846800000, 0], + [1552847400000, 0], + [1552848000000, 0], + [1552848600000, 0], + [1552849200000, 0], + [1552849800000, 0], + [1552850400000, 0], + [1552851000000, 0], + [1552851600000, 0], + [1552852200000, 0], + [1552852800000, 0], + [1552853400000, 0], + [1552854000000, 0], + [1552854600000, 0], + [1552855200000, 0], + [1552855800000, 0], + [1552856400000, 0], + [1552857000000, 0], + [1552857600000, 0], + [1552858200000, 1], + [1552858800000, 0], + [1552859400000, 0], + [1552860000000, 1], + [1552860600000, 0], + [1552861200000, 0], + [1552861800000, 2], + [1552862400000, 0], + [1552863000000, 0], + [1552863600000, 1], + [1552864200000, 0], + [1552864800000, 1], + [1552865400000, 1], + [1552866000000, 0], + [1552866600000, 0], + [1552867200000, 2], + [1552867800000, 0], + [1552868400000, 4], + [1552869000000, 0], + [1552869600000, 1], + [1552870200000, 0], + [1552870800000, 2], + [1552871400000, 2], + [1552872000000, 2], + [1552872600000, 1], + [1552873200000, 0], + [1552873800000, 2], + [1552874400000, 1], + [1552875000000, 2], + [1552875600000, 4], + [1552876200000, 4], + [1552876800000, 4], + [1552877400000, 3], + [1552878000000, 3], + [1552878600000, 3], + [1552879200000, 0], + [1552879800000, 3], + [1552880400000, 4], + [1552881000000, 4], + [1552881600000, 4], + [1552882200000, 8], + [1552882800000, 7], + [1552883400000, 7], + [1552884000000, 3], + [1552884600000, 8], + [1552885200000, 6], + [1552885800000, 6], + [1552886400000, 8], + [1552887000000, 11], + [1552887600000, 7], + [1552888200000, 8], + [1552888800000, 7], + [1552889400000, 11], + [1552890000000, 16], + [1552890600000, 14], + [1552891200000, 10], + [1552891800000, 12], + [1552892400000, 7], + [1552893000000, 15], + [1552893600000, 13], + [1552894200000, 9], + [1552894800000, 16], + [1552895400000, 15], + [1552896000000, 14], + [1552896600000, 9], + [1552897200000, 10], + [1552897800000, 15], + [1552898400000, 15], + [1552899000000, 13], + [1552899600000, 18], + [1552900200000, 17], + [1552900800000, 18], + [1552901400000, 18], + [1552902000000, 6], + [1552902600000, 19], + [1552903200000, 20], + [1552903800000, 14], + [1552904400000, 11], + [1552905000000, 7], + [1552905600000, 20], + [1552906200000, 11], + [1552906800000, 13], + [1552907400000, 8], + [1552908000000, 15], + [1552908600000, 10], + [1552909200000, 13], + [1552909800000, 16], + [1552910400000, 11], + [1552911000000, 11], + [1552911600000, 11], + [1552912200000, 7], + [1552912800000, 10], + [1552913400000, 6], + [1552914000000, 4], + [1552914600000, 6], + [1552915200000, 4], + [1552915800000, 5], + [1552916400000, 7], + [1552917000000, 7], + [1552917600000, 5], + [1552918200000, 2], + [1552918800000, 3], + [1552919400000, 4], + [1552920000000, 3], + [1552920600000, 4], + [1552921200000, 1], + [1552921800000, 4], + [1552922400000, 4], + [1552923000000, 2], + [1552923600000, 3], + [1552924200000, 3], + [1552924800000, 2], + [1552925400000, 0], + [1552926000000, 1], + [1552926600000, 0], + [1552927200000, 2], + [1552927800000, 4], + [1552928400000, 2], + [1552929000000, 2], + [1552929600000, 0], + [1552930200000, 0], + [1552930800000, 0], + [1552931400000, 2], + [1552932000000, 0], + [1552932600000, 1], + [1552933200000, 1], + [1552933800000, 0], + [1552934400000, 1], + [1552935000000, 0], + [1552935600000, 0], + [1552936200000, 0], + [1552936800000, 2], + [1552937400000, 0], + [1552938000000, 0], + [1552938600000, 0], + [1552939200000, 0], + [1552939800000, 0], + [1552940400000, 0], + [1552941000000, 0], + [1552941600000, 1], + [1552942200000, 0], + [1552942800000, 1], + [1552943400000, 0], + [1552944000000, 0], + [1552944600000, 0], + [1552945200000, 0], + [1552945800000, 0], + [1552946400000, 1], + [1552947000000, 1], + [1552947600000, 0], + [1552948200000, 0], + [1552948800000, 1], + [1552949400000, 0], + ], + stack: false, + lines: { show: true, fill: 0.5, lineWidth: 1, steps: false }, + points: { show: true, radius: 1, lineWidth: 1 }, + bars: { show: false, fill: 0.5, lineWidth: 1 }, + }, + { + id: '61ca57f1-469d-11e7-af02-69e470af7417:png', + label: 'png', + color: 'rgb(49, 89, 0)', + data: [ + [1552830600000, 0], + [1552831200000, 0], + [1552831800000, 0], + [1552832400000, 0], + [1552833000000, 0], + [1552833600000, 0], + [1552834200000, 0], + [1552834800000, 0], + [1552835400000, 0], + [1552836000000, 0], + [1552836600000, 0], + [1552837200000, 0], + [1552837800000, 0], + [1552838400000, 0], + [1552839000000, 0], + [1552839600000, 0], + [1552840200000, 0], + [1552840800000, 0], + [1552841400000, 0], + [1552842000000, 0], + [1552842600000, 0], + [1552843200000, 0], + [1552843800000, 0], + [1552844400000, 0], + [1552845000000, 0], + [1552845600000, 0], + [1552846200000, 0], + [1552846800000, 0], + [1552847400000, 0], + [1552848000000, 0], + [1552848600000, 0], + [1552849200000, 0], + [1552849800000, 0], + [1552850400000, 0], + [1552851000000, 0], + [1552851600000, 0], + [1552852200000, 0], + [1552852800000, 0], + [1552853400000, 0], + [1552854000000, 0], + [1552854600000, 0], + [1552855200000, 0], + [1552855800000, 0], + [1552856400000, 2], + [1552857000000, 0], + [1552857600000, 0], + [1552858200000, 0], + [1552858800000, 0], + [1552859400000, 0], + [1552860000000, 1], + [1552860600000, 1], + [1552861200000, 0], + [1552861800000, 0], + [1552862400000, 1], + [1552863000000, 0], + [1552863600000, 1], + [1552864200000, 1], + [1552864800000, 0], + [1552865400000, 0], + [1552866000000, 0], + [1552866600000, 1], + [1552867200000, 0], + [1552867800000, 2], + [1552868400000, 0], + [1552869000000, 0], + [1552869600000, 0], + [1552870200000, 0], + [1552870800000, 2], + [1552871400000, 3], + [1552872000000, 0], + [1552872600000, 0], + [1552873200000, 0], + [1552873800000, 0], + [1552874400000, 0], + [1552875000000, 2], + [1552875600000, 4], + [1552876200000, 0], + [1552876800000, 7], + [1552877400000, 0], + [1552878000000, 2], + [1552878600000, 0], + [1552879200000, 2], + [1552879800000, 4], + [1552880400000, 0], + [1552881000000, 2], + [1552881600000, 7], + [1552882200000, 4], + [1552882800000, 2], + [1552883400000, 4], + [1552884000000, 2], + [1552884600000, 6], + [1552885200000, 4], + [1552885800000, 5], + [1552886400000, 2], + [1552887000000, 7], + [1552887600000, 7], + [1552888200000, 5], + [1552888800000, 2], + [1552889400000, 1], + [1552890000000, 10], + [1552890600000, 9], + [1552891200000, 5], + [1552891800000, 10], + [1552892400000, 5], + [1552893000000, 7], + [1552893600000, 14], + [1552894200000, 10], + [1552894800000, 3], + [1552895400000, 11], + [1552896000000, 4], + [1552896600000, 15], + [1552897200000, 13], + [1552897800000, 15], + [1552898400000, 10], + [1552899000000, 4], + [1552899600000, 7], + [1552900200000, 12], + [1552900800000, 6], + [1552901400000, 9], + [1552902000000, 12], + [1552902600000, 3], + [1552903200000, 11], + [1552903800000, 10], + [1552904400000, 9], + [1552905000000, 6], + [1552905600000, 10], + [1552906200000, 5], + [1552906800000, 7], + [1552907400000, 9], + [1552908000000, 6], + [1552908600000, 7], + [1552909200000, 2], + [1552909800000, 3], + [1552910400000, 8], + [1552911000000, 2], + [1552911600000, 4], + [1552912200000, 6], + [1552912800000, 1], + [1552913400000, 6], + [1552914000000, 3], + [1552914600000, 2], + [1552915200000, 6], + [1552915800000, 6], + [1552916400000, 4], + [1552917000000, 3], + [1552917600000, 3], + [1552918200000, 1], + [1552918800000, 2], + [1552919400000, 4], + [1552920000000, 4], + [1552920600000, 2], + [1552921200000, 3], + [1552921800000, 1], + [1552922400000, 4], + [1552923000000, 1], + [1552923600000, 3], + [1552924200000, 1], + [1552924800000, 0], + [1552925400000, 0], + [1552926000000, 1], + [1552926600000, 1], + [1552927200000, 0], + [1552927800000, 0], + [1552928400000, 1], + [1552929000000, 1], + [1552929600000, 1], + [1552930200000, 0], + [1552930800000, 0], + [1552931400000, 0], + [1552932000000, 0], + [1552932600000, 0], + [1552933200000, 0], + [1552933800000, 0], + [1552934400000, 0], + [1552935000000, 0], + [1552935600000, 0], + [1552936200000, 0], + [1552936800000, 0], + [1552937400000, 0], + [1552938000000, 0], + [1552938600000, 0], + [1552939200000, 0], + [1552939800000, 0], + [1552940400000, 0], + [1552941000000, 0], + [1552941600000, 0], + [1552942200000, 0], + [1552942800000, 0], + [1552943400000, 0], + [1552944000000, 0], + [1552944600000, 0], + [1552945200000, 0], + [1552945800000, 0], + [1552946400000, 0], + [1552947000000, 1], + [1552947600000, 0], + [1552948200000, 1], + [1552948800000, 0], + [1552949400000, 1], + ], + stack: false, + lines: { show: true, fill: 0.5, lineWidth: 1, steps: false }, + points: { show: true, radius: 1, lineWidth: 1 }, + bars: { show: false, fill: 0.5, lineWidth: 1 }, + }, + { + id: '61ca57f1-469d-11e7-af02-69e470af7417:gif', + label: 'gif', + color: 'rgb(22, 39, 0)', + data: [ + [1552830600000, 0], + [1552831200000, 0], + [1552831800000, 0], + [1552832400000, 0], + [1552833000000, 0], + [1552833600000, 0], + [1552834200000, 0], + [1552834800000, 0], + [1552835400000, 0], + [1552836000000, 0], + [1552836600000, 0], + [1552837200000, 0], + [1552837800000, 0], + [1552838400000, 0], + [1552839000000, 0], + [1552839600000, 0], + [1552840200000, 0], + [1552840800000, 0], + [1552841400000, 0], + [1552842000000, 0], + [1552842600000, 0], + [1552843200000, 0], + [1552843800000, 0], + [1552844400000, 0], + [1552845000000, 0], + [1552845600000, 0], + [1552846200000, 0], + [1552846800000, 0], + [1552847400000, 0], + [1552848000000, 0], + [1552848600000, 0], + [1552849200000, 0], + [1552849800000, 0], + [1552850400000, 0], + [1552851000000, 0], + [1552851600000, 0], + [1552852200000, 0], + [1552852800000, 0], + [1552853400000, 0], + [1552854000000, 0], + [1552854600000, 0], + [1552855200000, 0], + [1552855800000, 0], + [1552856400000, 0], + [1552857000000, 0], + [1552857600000, 0], + [1552858200000, 0], + [1552858800000, 0], + [1552859400000, 0], + [1552860000000, 0], + [1552860600000, 0], + [1552861200000, 0], + [1552861800000, 0], + [1552862400000, 0], + [1552863000000, 0], + [1552863600000, 0], + [1552864200000, 0], + [1552864800000, 0], + [1552865400000, 0], + [1552866000000, 0], + [1552866600000, 0], + [1552867200000, 1], + [1552867800000, 0], + [1552868400000, 2], + [1552869000000, 1], + [1552869600000, 0], + [1552870200000, 1], + [1552870800000, 1], + [1552871400000, 2], + [1552872000000, 1], + [1552872600000, 2], + [1552873200000, 0], + [1552873800000, 1], + [1552874400000, 0], + [1552875000000, 0], + [1552875600000, 1], + [1552876200000, 1], + [1552876800000, 1], + [1552877400000, 2], + [1552878000000, 0], + [1552878600000, 2], + [1552879200000, 1], + [1552879800000, 2], + [1552880400000, 3], + [1552881000000, 2], + [1552881600000, 1], + [1552882200000, 2], + [1552882800000, 6], + [1552883400000, 4], + [1552884000000, 3], + [1552884600000, 5], + [1552885200000, 3], + [1552885800000, 3], + [1552886400000, 3], + [1552887000000, 3], + [1552887600000, 3], + [1552888200000, 3], + [1552888800000, 5], + [1552889400000, 1], + [1552890000000, 3], + [1552890600000, 4], + [1552891200000, 5], + [1552891800000, 4], + [1552892400000, 6], + [1552893000000, 4], + [1552893600000, 5], + [1552894200000, 5], + [1552894800000, 12], + [1552895400000, 7], + [1552896000000, 2], + [1552896600000, 8], + [1552897200000, 8], + [1552897800000, 5], + [1552898400000, 6], + [1552899000000, 8], + [1552899600000, 6], + [1552900200000, 3], + [1552900800000, 8], + [1552901400000, 4], + [1552902000000, 3], + [1552902600000, 9], + [1552903200000, 6], + [1552903800000, 14], + [1552904400000, 3], + [1552905000000, 7], + [1552905600000, 3], + [1552906200000, 4], + [1552906800000, 3], + [1552907400000, 5], + [1552908000000, 2], + [1552908600000, 3], + [1552909200000, 1], + [1552909800000, 2], + [1552910400000, 2], + [1552911000000, 5], + [1552911600000, 2], + [1552912200000, 1], + [1552912800000, 4], + [1552913400000, 3], + [1552914000000, 3], + [1552914600000, 4], + [1552915200000, 1], + [1552915800000, 4], + [1552916400000, 2], + [1552917000000, 3], + [1552917600000, 2], + [1552918200000, 2], + [1552918800000, 2], + [1552919400000, 0], + [1552920000000, 1], + [1552920600000, 2], + [1552921200000, 1], + [1552921800000, 4], + [1552922400000, 0], + [1552923000000, 0], + [1552923600000, 0], + [1552924200000, 0], + [1552924800000, 0], + [1552925400000, 0], + [1552926000000, 1], + [1552926600000, 2], + [1552927200000, 0], + [1552927800000, 0], + [1552928400000, 0], + [1552929000000, 0], + [1552929600000, 1], + [1552930200000, 0], + [1552930800000, 0], + [1552931400000, 0], + [1552932000000, 1], + [1552932600000, 1], + [1552933200000, 0], + [1552933800000, 0], + [1552934400000, 0], + [1552935000000, 0], + [1552935600000, 0], + [1552936200000, 0], + [1552936800000, 0], + [1552937400000, 0], + [1552938000000, 0], + [1552938600000, 0], + [1552939200000, 0], + [1552939800000, 0], + [1552940400000, 0], + [1552941000000, 0], + [1552941600000, 0], + [1552942200000, 0], + [1552942800000, 0], + [1552943400000, 0], + [1552944000000, 0], + [1552944600000, 0], + [1552945200000, 0], + [1552945800000, 0], + [1552946400000, 0], + [1552947000000, 0], + [1552947600000, 0], + [1552948200000, 0], + [1552948800000, 0], + [1552949400000, 0], + ], + stack: false, + lines: { show: true, fill: 0.5, lineWidth: 1, steps: false }, + points: { show: true, radius: 1, lineWidth: 1 }, + bars: { show: false, fill: 0.5, lineWidth: 1 }, + }, + { + id: '61ca57f1-469d-11e7-af02-69e470af7417:php', + label: 'php', + color: 'rgb(0, 0, 0)', + data: [ + [1552830600000, 0], + [1552831200000, 0], + [1552831800000, 0], + [1552832400000, 0], + [1552833000000, 0], + [1552833600000, 0], + [1552834200000, 0], + [1552834800000, 0], + [1552835400000, 0], + [1552836000000, 0], + [1552836600000, 0], + [1552837200000, 0], + [1552837800000, 0], + [1552838400000, 0], + [1552839000000, 0], + [1552839600000, 0], + [1552840200000, 0], + [1552840800000, 0], + [1552841400000, 0], + [1552842000000, 0], + [1552842600000, 0], + [1552843200000, 0], + [1552843800000, 0], + [1552844400000, 0], + [1552845000000, 0], + [1552845600000, 0], + [1552846200000, 0], + [1552846800000, 0], + [1552847400000, 0], + [1552848000000, 0], + [1552848600000, 0], + [1552849200000, 0], + [1552849800000, 0], + [1552850400000, 0], + [1552851000000, 0], + [1552851600000, 0], + [1552852200000, 0], + [1552852800000, 0], + [1552853400000, 0], + [1552854000000, 0], + [1552854600000, 0], + [1552855200000, 0], + [1552855800000, 0], + [1552856400000, 0], + [1552857000000, 0], + [1552857600000, 0], + [1552858200000, 0], + [1552858800000, 0], + [1552859400000, 0], + [1552860000000, 0], + [1552860600000, 0], + [1552861200000, 0], + [1552861800000, 0], + [1552862400000, 0], + [1552863000000, 0], + [1552863600000, 0], + [1552864200000, 0], + [1552864800000, 1], + [1552865400000, 0], + [1552866000000, 0], + [1552866600000, 0], + [1552867200000, 0], + [1552867800000, 0], + [1552868400000, 0], + [1552869000000, 0], + [1552869600000, 0], + [1552870200000, 0], + [1552870800000, 0], + [1552871400000, 1], + [1552872000000, 1], + [1552872600000, 1], + [1552873200000, 0], + [1552873800000, 1], + [1552874400000, 0], + [1552875000000, 0], + [1552875600000, 0], + [1552876200000, 0], + [1552876800000, 0], + [1552877400000, 3], + [1552878000000, 0], + [1552878600000, 0], + [1552879200000, 0], + [1552879800000, 0], + [1552880400000, 1], + [1552881000000, 1], + [1552881600000, 1], + [1552882200000, 1], + [1552882800000, 1], + [1552883400000, 2], + [1552884000000, 0], + [1552884600000, 1], + [1552885200000, 1], + [1552885800000, 1], + [1552886400000, 0], + [1552887000000, 1], + [1552887600000, 5], + [1552888200000, 2], + [1552888800000, 1], + [1552889400000, 4], + [1552890000000, 1], + [1552890600000, 2], + [1552891200000, 4], + [1552891800000, 8], + [1552892400000, 3], + [1552893000000, 4], + [1552893600000, 3], + [1552894200000, 2], + [1552894800000, 0], + [1552895400000, 5], + [1552896000000, 3], + [1552896600000, 4], + [1552897200000, 2], + [1552897800000, 1], + [1552898400000, 2], + [1552899000000, 2], + [1552899600000, 1], + [1552900200000, 3], + [1552900800000, 1], + [1552901400000, 1], + [1552902000000, 3], + [1552902600000, 4], + [1552903200000, 2], + [1552903800000, 4], + [1552904400000, 4], + [1552905000000, 4], + [1552905600000, 0], + [1552906200000, 5], + [1552906800000, 2], + [1552907400000, 5], + [1552908000000, 3], + [1552908600000, 0], + [1552909200000, 3], + [1552909800000, 0], + [1552910400000, 4], + [1552911000000, 1], + [1552911600000, 1], + [1552912200000, 0], + [1552912800000, 0], + [1552913400000, 2], + [1552914000000, 1], + [1552914600000, 2], + [1552915200000, 0], + [1552915800000, 0], + [1552916400000, 0], + [1552917000000, 1], + [1552917600000, 0], + [1552918200000, 1], + [1552918800000, 0], + [1552919400000, 2], + [1552920000000, 1], + [1552920600000, 0], + [1552921200000, 0], + [1552921800000, 0], + [1552922400000, 3], + [1552923000000, 0], + [1552923600000, 0], + [1552924200000, 0], + [1552924800000, 0], + [1552925400000, 0], + [1552926000000, 0], + [1552926600000, 0], + [1552927200000, 0], + [1552927800000, 0], + [1552928400000, 0], + [1552929000000, 0], + [1552929600000, 1], + [1552930200000, 0], + [1552930800000, 0], + [1552931400000, 0], + [1552932000000, 0], + [1552932600000, 0], + [1552933200000, 0], + [1552933800000, 0], + [1552934400000, 0], + [1552935000000, 0], + [1552935600000, 0], + [1552936200000, 1], + [1552936800000, 0], + [1552937400000, 0], + [1552938000000, 0], + [1552938600000, 0], + [1552939200000, 0], + [1552939800000, 0], + [1552940400000, 0], + [1552941000000, 0], + [1552941600000, 0], + [1552942200000, 0], + [1552942800000, 0], + [1552943400000, 0], + [1552944000000, 0], + [1552944600000, 0], + [1552945200000, 0], + [1552945800000, 0], + [1552946400000, 0], + [1552947000000, 0], + [1552947600000, 0], + [1552948200000, 0], + [1552948800000, 0], + [1552949400000, 1], + ], + stack: false, + lines: { show: true, fill: 0.5, lineWidth: 1, steps: false }, + points: { show: true, radius: 1, lineWidth: 1 }, + bars: { show: false, fill: 0.5, lineWidth: 1 }, + }, + ], +}; diff --git a/packages/osd-charts/src/utils/dimensions.ts b/packages/osd-charts/src/utils/dimensions.ts new file mode 100644 index 000000000000..5baf88c9540c --- /dev/null +++ b/packages/osd-charts/src/utils/dimensions.ts @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export interface Dimensions { + top: number; + left: number; + width: number; + height: number; +} + +/** @internal */ +export interface Size { + width: number; + height: number; +} + +/** + * fixme consider switching from `number` to `Pixels` or similar, once nominal typing is added + * @public + */ +export interface PerSideDistance { + top: number; + bottom: number; + left: number; + right: number; +} + +/** + * fixme consider deactivating @typescript-eslint/no-empty-interface + * see https://github.com/elastic/elastic-charts/pull/660#discussion_r419474171 + * @public + */ +export type Margins = PerSideDistance; + +/** @public */ +export type Padding = PerSideDistance; + +/** + * Simple padding declaration + * @public + */ +export interface SimplePadding { + outer: number; + inner: number; +} + +/** + * Computes padding from number or `SimplePadding` with optional `minPadding` + * + * @param padding + * @param minPadding should be at least one to avoid browser measureText inconsistencies + * @internal + */ +export function getSimplePadding(padding: number | Partial, minPadding = 0): SimplePadding { + if (typeof padding === 'number') { + const adjustedPadding = Math.max(minPadding, padding); + + return { + inner: adjustedPadding, + outer: adjustedPadding, + }; + } + + return { + inner: Math.max(minPadding, padding?.inner ?? 0), + outer: Math.max(minPadding, padding?.outer ?? 0), + }; +} diff --git a/packages/osd-charts/src/utils/domain.test.ts b/packages/osd-charts/src/utils/domain.test.ts new file mode 100644 index 000000000000..c19a121547d8 --- /dev/null +++ b/packages/osd-charts/src/utils/domain.test.ts @@ -0,0 +1,244 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ScaleType } from '../scales/constants'; +import { DomainPaddingUnit } from '../specs'; +import { AccessorFn } from './accessor'; +import { identity } from './common'; +import { computeContinuousDataDomain, computeDomainExtent, computeOrdinalDataDomain } from './domain'; + +describe('utils/domain', () => { + test('should return [0] domain if no data', () => { + const data: any[] = []; + const accessor: AccessorFn = (datum: any) => datum.x; + const isSorted = true; + const removeNull = true; + + const ordinalDataDomain = computeOrdinalDataDomain(data, accessor, isSorted, removeNull); + + expect(ordinalDataDomain).toEqual([0]); + }); + + test('should compute ordinal data domain: sort & remove nulls', () => { + const data = [{ x: 'd' }, { x: 'a' }, { x: null }, { x: 'b' }]; + const accessor: AccessorFn = (datum: any) => datum.x; + const isSorted = true; + const removeNull = true; + + const ordinalDataDomain = computeOrdinalDataDomain(data, accessor, isSorted, removeNull); + + const expectedOrdinalDomain = ['a', 'b', 'd']; + + expect(ordinalDataDomain).toEqual(expectedOrdinalDomain); + }); + + test('should compute ordinal data domain: unsorted and remove nulls', () => { + const data = [{ x: 'd' }, { x: 'a' }, { x: null }, { x: 'b' }]; + const accessor: AccessorFn = (datum: any) => datum.x; + const isSorted = false; + const removeNull = true; + + const ordinalDataDomain = computeOrdinalDataDomain(data, accessor, isSorted, removeNull); + + const expectedOrdinalDomain = ['d', 'a', 'b']; + + expect(ordinalDataDomain).toEqual(expectedOrdinalDomain); + }); + + test('should compute ordinal data domain: sorted and keep nulls', () => { + const data = [{ x: 'd' }, { x: 'a' }, { x: null }, { x: 'b' }]; + const accessor: AccessorFn = (datum: any) => datum.x; + const isSorted = true; + const removeNull = false; + + const ordinalDataDomain = computeOrdinalDataDomain(data, accessor, isSorted, removeNull); + + const expectedOrdinalDomain = ['a', 'b', 'd', null]; + + expect(ordinalDataDomain).toEqual(expectedOrdinalDomain); + }); + + test('should compute ordinal data domain: unsorted and keep nulls', () => { + const data = [{ x: 'd' }, { x: 'a' }, { x: null }, { x: 'b' }]; + const accessor: AccessorFn = (datum: any) => datum.x; + const isSorted = false; + const removeNull = false; + + const ordinalDataDomain = computeOrdinalDataDomain(data, accessor, isSorted, removeNull); + + const expectedOrdinalDomain = ['d', 'a', null, 'b']; + + expect(ordinalDataDomain).toEqual(expectedOrdinalDomain); + }); + + test('should compute continuous data domain: data scaled to extent', () => { + const data = [{ x: 12 }, { x: 6 }, { x: 8 }]; + const accessor = (datum: any) => datum.x; + const continuousDataDomain = computeContinuousDataDomain(data, accessor, ScaleType.Linear, { fit: true }); + const expectedContinuousDomain = [6, 12]; + + expect(continuousDataDomain).toEqual(expectedContinuousDomain); + }); + + test('should compute continuous data domain: data not scaled to extent', () => { + const data = [{ x: 12 }, { x: 6 }, { x: 8 }]; + const accessor = (datum: any) => datum.x; + + const continuousDataDomain = computeContinuousDataDomain(data, accessor, ScaleType.Linear); + + const expectedContinuousDomain = [0, 12]; + + expect(continuousDataDomain).toEqual(expectedContinuousDomain); + }); + + test('should compute continuous data domain: empty data not scaled to extent', () => { + const data: any[] = []; + const accessor = (datum: any) => datum.x; + + const continuousDataDomain = computeContinuousDataDomain(data, accessor, ScaleType.Linear); + + const expectedContinuousDomain = [0, 0]; + + expect(continuousDataDomain).toEqual(expectedContinuousDomain); + }); + + test('should filter zeros on log scale domain when fit is true', () => { + const data: number[] = [0.0001, 0, 1, 0, 10, 0, 100, 0, 0, 1000]; + const continuousDataDomain = computeContinuousDataDomain(data, identity, ScaleType.Log, { fit: true }); + + expect(continuousDataDomain).toEqual([0.0001, 1000]); + }); + + test('should not filter zeros on log scale domain when fit is false', () => { + const data: number[] = [0.0001, 0, 1, 0, 10, 0, 100, 0, 0, 1000]; + const continuousDataDomain = computeContinuousDataDomain(data, identity, ScaleType.Log, { fit: false }); + + expect(continuousDataDomain).toEqual([0, 1000]); + }); + + describe('YDomainOptions', () => { + it('should not effect domain when domain.fit is true', () => { + expect(computeDomainExtent([5, 10], { fit: true })).toEqual([5, 10]); + }); + + // Note: padded domains are possible with log scale but not very practical + it('should not effect positive domain if log scale with padding', () => { + expect(computeDomainExtent([0.001, 10], { padding: 5 })).toEqual([0, 15]); + }); + + it('should not effect negative domain if log scale with padding', () => { + expect(computeDomainExtent([-10, -0.001], { padding: 5 })).toEqual([-15, 0]); + }); + + describe('domain.fit is true', () => { + it('should find domain when start & end are positive', () => { + expect(computeDomainExtent([5, 10], { fit: true })).toEqual([5, 10]); + }); + + it('should find domain when start & end are negative', () => { + expect(computeDomainExtent([-15, -10], { fit: true })).toEqual([-15, -10]); + }); + + it('should find domain when start is negative, end is positive', () => { + expect(computeDomainExtent([-15, 10], { fit: true })).toEqual([-15, 10]); + }); + }); + describe('domain.fit is false', () => { + it('should find domain when start & end are positive', () => { + expect(computeDomainExtent([5, 10])).toEqual([0, 10]); + }); + + it('should find domain when start & end are negative', () => { + expect(computeDomainExtent([-15, -10])).toEqual([-15, 0]); + }); + + it('should find domain when start is negative, end is positive', () => { + expect(computeDomainExtent([-15, 10])).toEqual([-15, 10]); + }); + }); + + describe('padding does NOT cause domain to cross zero baseline', () => { + it('should get domain from positive domain', () => { + expect(computeDomainExtent([10, 70], { fit: true, padding: 5 })).toEqual([5, 75]); + }); + + it('should get domain from positive & negative domain', () => { + expect(computeDomainExtent([-30, 30], { fit: true, padding: 5 })).toEqual([-35, 35]); + }); + + it('should get domain from negative domain', () => { + expect(computeDomainExtent([-70, -10], { fit: true, padding: 5 })).toEqual([-75, -5]); + }); + + it('should use absolute padding value', () => { + expect(computeDomainExtent([10, 70], { fit: true, padding: -5 })).toEqual([5, 75]); + }); + }); + + describe('padding caused domain to cross zero baseline', () => { + describe('constrainPadding true - default', () => { + it('should set min baseline as 0 if original domain is less than zero', () => { + expect(computeDomainExtent([5, 65], { fit: true, padding: 15 })).toEqual([0, 80]); + }); + + it('should set max baseline as 0 if original domain is less than zero', () => { + expect(computeDomainExtent([-65, -5], { fit: true, padding: 15 })).toEqual([-80, 0]); + }); + }); + + describe('constrainPadding false', () => { + it('should allow min past baseline as 0, even if original domain is less than zero', () => { + expect(computeDomainExtent([5, 65], { fit: true, padding: 15, constrainPadding: false })).toEqual([-10, 80]); + }); + + it('should allow max past baseline as 0, even if original domain is less than zero', () => { + expect(computeDomainExtent([-65, -5], { fit: true, padding: 15, constrainPadding: false })).toEqual([ + -80, + 10, + ]); + }); + }); + }); + + describe('padding units', () => { + // Note: domain pixel padding computed in continuous scale + it('should not change domain when using Pixel padding unit', () => { + expect(computeDomainExtent([5, 65], { fit: true, padding: 15, paddingUnit: DomainPaddingUnit.Pixel })).toEqual([ + 5, + 65, + ]); + }); + it('should handle DomainRatio padding unit', () => { + expect( + computeDomainExtent([50, 60], { fit: true, padding: 0.5, paddingUnit: DomainPaddingUnit.DomainRatio }), + ).toEqual([45, 65]); + }); + it('should handle negative inverted DomainRatio padding unit', () => { + expect( + computeDomainExtent([-50, -60], { fit: true, padding: 0.5, paddingUnit: DomainPaddingUnit.DomainRatio }), + ).toEqual([-45, -65]); + }); + it('should handle negative inverted Domain padding unit', () => { + expect( + computeDomainExtent([-50, -60], { fit: true, padding: 10, paddingUnit: DomainPaddingUnit.Domain }), + ).toEqual([-40, -70]); + }); + }); + }); +}); diff --git a/packages/osd-charts/src/utils/domain.ts b/packages/osd-charts/src/utils/domain.ts new file mode 100644 index 000000000000..e71a255c132f --- /dev/null +++ b/packages/osd-charts/src/utils/domain.ts @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { extent } from 'd3-array'; + +import { ScaleType } from '../scales/constants'; +import { DomainPaddingUnit, YDomainRange } from '../specs'; +import { AccessorFn } from './accessor'; + +/** @public */ +export type OrdinalDomain = (number | string)[]; +/** @public */ +export type ContinuousDomain = [min: number, max: number]; +/** @public */ +export type Range = [min: number, max: number]; + +/** + * Returns padded domain given constrain + * @internal */ +export function constrainPadding( + start: number, + end: number, + newStart: number, + newEnd: number, + constrain: boolean = true, +): [number, number] { + if (constrain) { + if (start < end) { + return [start >= 0 && newStart < 0 ? 0 : newStart, end <= 0 && newEnd > 0 ? 0 : newEnd]; + } + + return [end >= 0 && newEnd < 0 ? 0 : newEnd, start <= 0 && newStart > 0 ? 0 : newStart]; + } + + return [newStart, newEnd]; +} + +/** @internal */ +export function computeOrdinalDataDomain( + data: any[], + accessor: AccessorFn, + sorted?: boolean, + removeNull?: boolean, +): string[] | number[] { + // TODO: Check for empty data before computing domain + if (data.length === 0) { + return [0]; + } + + const domain = data.map(accessor).filter((d) => (removeNull ? d !== null : true)); + const uniqueValues = [...new Set(domain)]; + return sorted ? uniqueValues.sort((a, b) => `${a}`.localeCompare(`${b}`)) : uniqueValues; +} + +function getPaddedDomain(start: number, end: number, domainOptions?: YDomainRange): [number, number] { + if (!domainOptions || !domainOptions.padding || domainOptions.paddingUnit === DomainPaddingUnit.Pixel) { + return [start, end]; + } + + const { padding, paddingUnit = DomainPaddingUnit.Domain } = domainOptions; + const absPadding = Math.abs(padding); + const computedPadding = paddingUnit === DomainPaddingUnit.Domain ? absPadding : absPadding * Math.abs(end - start); + + if (computedPadding === 0) { + return [start, end]; + } + + const newStart = start - computedPadding; + const newEnd = end + computedPadding; + + return constrainPadding(start, end, newStart, newEnd, domainOptions.constrainPadding); +} + +/** @internal */ +export function computeDomainExtent( + domain: [number, number] | [undefined, undefined], + domainOptions?: YDomainRange, +): [number, number] { + if (domain[0] == null || domain[1] == null) return [0, 0]; + + const inverted = domain[0] > domain[1]; + const paddedDomain = (([start, end]: Range): Range => { + const [paddedStart, paddedEnd] = getPaddedDomain(start, end, domainOptions); + + if (paddedStart >= 0 && paddedEnd >= 0) { + return domainOptions?.fit ? [paddedStart, paddedEnd] : [0, paddedEnd]; + } + if (paddedStart < 0 && paddedEnd < 0) { + return domainOptions?.fit ? [paddedStart, paddedEnd] : [paddedStart, 0]; + } + + return [paddedStart, paddedEnd]; + })(inverted ? (domain.slice().reverse() as Range) : domain); + + return inverted ? (paddedDomain.slice().reverse() as Range) : paddedDomain; +} + +/** + * Get Continuous domain from data. May alters domain to constrain to zero baseline. + * + * when `domainOptions` is null the domain will not be altered + * @internal + */ +export function computeContinuousDataDomain( + data: any[], + accessor: (n: any) => number, + scaleType: ScaleType, + domainOptions?: YDomainRange | null, +): ContinuousDomain { + const filteredData = domainOptions?.fit && scaleType === ScaleType.Log ? data.filter((d) => accessor(d) !== 0) : data; + const range = extent(filteredData, accessor); + + if (domainOptions === null) { + return [range[0] ?? 0, range[1] ?? 0]; + } + + return computeDomainExtent(range, domainOptions); +} diff --git a/packages/osd-charts/src/utils/events.ts b/packages/osd-charts/src/utils/events.ts new file mode 100644 index 000000000000..8290c0480fd8 --- /dev/null +++ b/packages/osd-charts/src/utils/events.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Scale } from '../scales'; +import { BrushEndListener, isPointerOverEvent, PointerEvent, PointerOverEvent, HeatmapSpec } from '../specs'; +import { DragState } from '../state/chart_state'; + +/** @internal */ +export function isValidPointerOverEvent( + mainScale: Scale, + event: PointerEvent | null | undefined, +): event is PointerOverEvent { + return isPointerOverEvent(event) && (event.unit === undefined || event.unit === mainScale.unit); +} + +/** @internal */ +export interface DragCheckProps { + onBrushEnd: BrushEndListener | HeatmapSpec['config']['onBrushEnd'] | undefined; + lastDrag: DragState | null; +} + +/** @internal */ +export function hasDragged(prevProps: DragCheckProps | null, nextProps: DragCheckProps | null) { + if (nextProps === null) { + return false; + } + if (!nextProps.onBrushEnd) { + return false; + } + const prevLastDrag = prevProps !== null ? prevProps.lastDrag : null; + const nextLastDrag = nextProps !== null ? nextProps.lastDrag : null; + + if (prevLastDrag === null && nextLastDrag !== null) { + return true; + } + if (prevLastDrag !== null && nextLastDrag !== null && prevLastDrag.end.time !== nextLastDrag.end.time) { + return true; + } + return false; +} diff --git a/packages/osd-charts/src/utils/fast_deep_equal.ts b/packages/osd-charts/src/utils/fast_deep_equal.ts new file mode 100644 index 000000000000..553b68575604 --- /dev/null +++ b/packages/osd-charts/src/utils/fast_deep_equal.ts @@ -0,0 +1,91 @@ +/* eslint-disable header/header, @typescript-eslint/explicit-module-boundary-types, prefer-destructuring, no-restricted-syntax, no-self-compare */ + +/** + * @notice + * This product includes code that is adapted from fast-deep-equal@3.1.1, + * which is available under a "MIT" license. + * + * MIT License + * + * Copyright (c) 2017 Evgeny Poberezkin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** @internal */ +export function deepEqual(a: any, b: any): boolean { + if (a === b) return true; + + if (a && b && typeof a === 'object' && typeof b === 'object') { + if (a.constructor !== b.constructor) return false; + + let length: number; + let i: any; + + if (Array.isArray(a)) { + length = a.length; + if (length != b.length) return false; + for (i = length; i-- !== 0; ) if (!deepEqual(a[i], b[i])) return false; + return true; + } + if (a instanceof Map && b instanceof Map) { + if (a.size !== b.size) return false; + // @ts-ignore + for (i of a.entries()) if (!b.has(i[0])) return false; + // @ts-ignore + for (i of a.entries()) if (!deepEqual(i[1], b.get(i[0]))) return false; + return true; + } + + if (a instanceof Set && b instanceof Set) { + if (a.size !== b.size) return false; + // @ts-ignore + for (i of a.entries()) if (!b.has(i[0])) return false; + return true; + } + + if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags; + if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf(); + if (a.toString !== Object.prototype.toString) return a.toString() === b.toString(); + + const keys = Object.keys(a); + length = keys.length; + if (length !== Object.keys(b).length) return false; + + for (i = length; i-- !== 0; ) if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false; + + for (i = length; i-- !== 0; ) { + const key = keys[i]; + if (key === '_owner' && a.$$typeof) { + // React-specific: avoid traversing React elements' _owner. + // _owner contains circular references + // and is not needed when comparing the actual elements (and not their owners) + continue; + } + if (!deepEqual(a[key], b[key])) return false; + } + + return true; + } + + // true if both NaN, false otherwise + return a !== a && b !== b; +} + +/* eslint-enable */ diff --git a/packages/osd-charts/src/utils/geometry.ts b/packages/osd-charts/src/utils/geometry.ts new file mode 100644 index 000000000000..c1ecf6f1f88e --- /dev/null +++ b/packages/osd-charts/src/utils/geometry.ts @@ -0,0 +1,176 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $Values } from 'utility-types'; + +import { XYChartSeriesIdentifier } from '../chart_types/xy_chart/utils/series'; +import { Fill, Stroke } from '../geoms/types'; +import { Color } from './common'; +import { Dimensions } from './dimensions'; +import { BarSeriesStyle, PointStyle, AreaStyle, LineStyle, PointShape } from './themes/theme'; + +/** + * The accessor type + * @public + */ +export const BandedAccessorType = Object.freeze({ + Y0: 'y0' as const, + Y1: 'y1' as const, +}); + +/** @public */ +export type BandedAccessorType = $Values; + +/** @public */ +export interface GeometryValue { + y: any; + x: any; + mark: number | null; + accessor: BandedAccessorType; + /** + * The original datum used for this geometry + */ + datum: any; +} + +/** @internal */ +export type IndexedGeometry = PointGeometry | BarGeometry; + +/** + * Array of **range** clippings [x1, x2] to be excluded during rendering + * + * Note: Must be scaled **range** values (i.e. pixel coordinates) **NOT** domain values + * @internal + */ +export type ClippedRanges = [number, number][]; + +/** @internal */ +export interface PointGeometry { + seriesIdentifier: XYChartSeriesIdentifier; + x: number; + y: number; + radius: number; + color: Color; + transform: { + x: number; + y: number; + }; + value: GeometryValue; + style: PointGeometryStyle; + panel: Dimensions; + orphan: boolean; +} +/** @internal */ +export interface PointGeometryStyle { + fill: Fill; + stroke: Stroke; + shape: PointShape; +} + +/** @internal */ +export interface PerPanel { + panel: Dimensions; + value: T; +} + +/** @internal */ +export interface BarGeometry { + x: number; + y: number; + width: number; + height: number; + transform: { + x: number; + y: number; + rotation?: number; + }; + color: Color; + displayValue?: { + fontScale?: number; + fontSize: number; + text: any; + width: number; + height: number; + hideClippedValue?: boolean; + isValueContainedInElement?: boolean; + }; + seriesIdentifier: XYChartSeriesIdentifier; + value: GeometryValue; + seriesStyle: BarSeriesStyle; + panel: Dimensions; +} + +/** @internal */ +export interface LineGeometry { + line: string; + points: PointGeometry[]; + color: Color; + transform: { + x: number; + y: number; + }; + seriesIdentifier: XYChartSeriesIdentifier; + seriesLineStyle: LineStyle; + seriesPointStyle: PointStyle; + /** + * Ranges of `[x0, x1]` pairs to clip from series + */ + clippedRanges: ClippedRanges; + hideClippedRanges?: boolean; +} + +/** @internal */ +export interface AreaGeometry { + area: string; + lines: string[]; + points: PointGeometry[]; + color: Color; + transform: { + x: number; + y: number; + }; + seriesIdentifier: XYChartSeriesIdentifier; + seriesAreaStyle: AreaStyle; + seriesAreaLineStyle: LineStyle; + seriesPointStyle: PointStyle; + isStacked: boolean; + /** + * Ranges of `[x0, x1]` pairs to clip from series + */ + clippedRanges: ClippedRanges; + hideClippedRanges?: boolean; +} + +/** @internal */ +export interface BubbleGeometry { + points: PointGeometry[]; + color: Color; + seriesIdentifier: XYChartSeriesIdentifier; + seriesPointStyle: PointStyle; +} + +/** @internal */ +export function isPointGeometry(ig: IndexedGeometry): ig is PointGeometry { + return ig.hasOwnProperty('radius'); +} + +/** @internal */ +export function isBarGeometry(ig: IndexedGeometry): ig is BarGeometry { + return ig.hasOwnProperty('width') && ig.hasOwnProperty('height'); +} diff --git a/packages/osd-charts/src/utils/ids.test.ts b/packages/osd-charts/src/utils/ids.test.ts new file mode 100644 index 000000000000..a3fb08b58489 --- /dev/null +++ b/packages/osd-charts/src/utils/ids.test.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AnnotationId, AxisId, GroupId } from './ids'; + +describe('IDs', () => { + test('ids should differ depending on entity', () => { + const axisId1 = 'axisId1'; + const axisId2 = 'axisId2'; + const groupId1 = 'groupId'; + const groupId2 = 'groupId'; + const axisSeries: Map = new Map(); + axisSeries.set(axisId1, 'data1'); + axisSeries.set(axisId2, 'data2'); + + const groupSeries: Map = new Map(); + groupSeries.set(groupId1, 'data1'); + groupSeries.set(groupId2, 'data2'); + const expectedAxisSeries = [ + ['axisId1', 'data1'], + ['axisId2', 'data2'], + ]; + const expectedGroupSeries = [['groupId', 'data2']]; + expect(expectedAxisSeries).toEqual([...axisSeries]); + expect(expectedGroupSeries).toEqual([...groupSeries]); + }); + test('should be able to identify annotations', () => { + const annotationId1 = 'anno1'; + const annotationId2 = 'anno2'; + + const annotations = new Map(); + annotations.set(annotationId1, 'annotations 1'); + annotations.set(annotationId2, 'annotations 2'); + + const expectedAnnotations = [ + ['anno1', 'annotations 1'], + ['anno2', 'annotations 2'], + ]; + expect(expectedAnnotations).toEqual([...annotations]); + }); +}); diff --git a/packages/osd-charts/src/utils/ids.ts b/packages/osd-charts/src/utils/ids.ts new file mode 100644 index 000000000000..54f244bb6542 --- /dev/null +++ b/packages/osd-charts/src/utils/ids.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @public */ +export type GroupId = string; +/** @public */ +export type AxisId = string; +/** @public */ +export type SpecId = string; +/** @public */ +export type AnnotationId = string; diff --git a/packages/osd-charts/src/utils/legend.ts b/packages/osd-charts/src/utils/legend.ts new file mode 100644 index 000000000000..7b77742452bd --- /dev/null +++ b/packages/osd-charts/src/utils/legend.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegendPositionConfig } from '../specs/settings'; +import { LayoutDirection } from './common'; + +/** @internal */ +export const isHorizontalLegend = (legendPosition: LegendPositionConfig) => + legendPosition.direction === LayoutDirection.Horizontal; + +/** @internal */ +export const isHierarchicalLegend = (flatLegend: boolean | undefined, legendPosition: LegendPositionConfig) => + !flatLegend && !isHorizontalLegend(legendPosition); diff --git a/packages/osd-charts/src/utils/logger.ts b/packages/osd-charts/src/utils/logger.ts new file mode 100644 index 000000000000..c1ab696cd3db --- /dev/null +++ b/packages/osd-charts/src/utils/logger.ts @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable no-console */ + +/** + * Helper class to assist with logging warnings + * + * @internal + * @todo Add more helpful messages in dev for configuration errors + */ +export class Logger { + static namespace = '[@elastic/charts]'; + + /** + * Log warning to console + * + * @param message + * @param optionalParams + */ + static warn(message?: any, ...optionalParams: any[]) { + if (Logger.isDevelopment() && !Logger.isTest()) { + console.warn(`${Logger.namespace} ${message}`, ...optionalParams); + } + } + + /** + * Log expected value warning to console + */ + static expected(message: any, expected: any, received: any) { + if (Logger.isDevelopment() && !Logger.isTest()) { + console.warn( + `${Logger.namespace} ${message}`, + `\n + Expected: ${expected} + Received: ${received} +`, + ); + } + } + + /** + * Log error to console + * + * @param message + * @param optionalParams + */ + static error(message?: any, ...optionalParams: any[]) { + if (Logger.isDevelopment() && !Logger.isTest()) { + console.warn(`${Logger.namespace} ${message}`, ...optionalParams); + } + } + + /** + * Determined development env + * + * @todo confirm this logic works + * @todo add more robust logic + */ + private static isDevelopment(): boolean { + return process.env.NODE_ENV !== 'production'; + } + + /** + * Determined development env + * + * @todo confirm this logic works + * @todo add more robust logic + */ + private static isTest(): boolean { + return process.env.NODE_ENV === 'test'; + } +} + +/* eslint-enable */ diff --git a/packages/osd-charts/src/utils/point.ts b/packages/osd-charts/src/utils/point.ts new file mode 100644 index 000000000000..0772b7a20033 --- /dev/null +++ b/packages/osd-charts/src/utils/point.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @public */ +export interface Point { + x: number; + y: number; +} + +/** @internal * */ +export function getDelta(start: Point, end: Point) { + return Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2)); +} diff --git a/packages/osd-charts/src/utils/series_sort.ts b/packages/osd-charts/src/utils/series_sort.ts new file mode 100644 index 000000000000..ed6341859861 --- /dev/null +++ b/packages/osd-charts/src/utils/series_sort.ts @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SeriesIdentifier } from '../common/series_id'; +import { SettingsSpec, SortSeriesByConfig } from '../specs/settings'; + +/** + * A compare function used to determine the order of the elements. It is expected to return + * a negative value if first argument is less than second argument, zero if they're equal and a positive + * value otherwise. + * @public + */ +export type SeriesCompareFn = (siA: SeriesIdentifier, siB: SeriesIdentifier) => number; + +/** @internal */ +export const DEFAULT_SORTING_FN = () => { + return 0; +}; + +/** @internal */ +export function getRenderingCompareFn( + // @ts-ignore + sortSeriesBy: SettingsSpec['sortSeriesBy'], + defaultSortFn?: SeriesCompareFn, +): SeriesCompareFn { + return getCompareFn('rendering', sortSeriesBy, defaultSortFn); +} + +/** @internal */ +export function getLegendCompareFn( + // @ts-ignore + sortSeriesBy: SettingsSpec['sortSeriesBy'], + defaultSortFn?: SeriesCompareFn, +): SeriesCompareFn { + return getCompareFn('legend', sortSeriesBy, defaultSortFn); +} + +/** @internal */ +export function getTooltipCompareFn( + // @ts-ignore + sortSeriesBy: SettingsSpec['sortSeriesBy'], + defaultSortFn?: SeriesCompareFn, +): SeriesCompareFn { + return getCompareFn('tooltip', sortSeriesBy, defaultSortFn); +} + +function getCompareFn( + aspect: keyof SortSeriesByConfig, + // @ts-ignore + sortSeriesBy: SettingsSpec['sortSeriesBy'], + defaultSortFn: SeriesCompareFn = DEFAULT_SORTING_FN, +): SeriesCompareFn { + if (typeof sortSeriesBy === 'object') { + return sortSeriesBy[aspect] ?? sortSeriesBy.default ?? defaultSortFn; + } + return sortSeriesBy ?? defaultSortFn; +} diff --git a/packages/osd-charts/src/utils/themes/colors.ts b/packages/osd-charts/src/utils/themes/colors.ts new file mode 100644 index 000000000000..1f2f1112ded5 --- /dev/null +++ b/packages/osd-charts/src/utils/themes/colors.ts @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export interface ColorScales { + [key: string]: string; +} + +interface EchPalette { + colors: string[]; +} + +const echPaletteColorBlind: EchPalette = { + colors: [ + '#1EA593', + '#2B70F7', + '#CE0060', + '#38007E', + '#FCA5D3', + '#F37020', + '#E49E29', + '#B0916F', + '#7B000B', + '#34130C', + ], +}; + +const echPaletteForLightBackground: EchPalette = { + colors: ['#006BB4', '#017D73', '#F5A700', '#BD271E', '#DD0A73'], +}; + +const echPaletteForDarkBackground: EchPalette = { + colors: ['#1BA9F5', '#7DE2D1', '#F990C0', '#F66', '#FFCE7A'], +}; + +const echPaletteForStatus: EchPalette = { + colors: [ + '#58BA6D', + '#6ECE67', + '#A5E26A', + '#D2E26A', + '#EBDF61', + '#EBD361', + '#EBC461', + '#D99D4C', + '#D97E4C', + '#D75949', + ], +}; + +/** @internal */ +export const palettes = { + echPaletteColorBlind, + echPaletteForLightBackground, + echPaletteForDarkBackground, + echPaletteForStatus, +}; diff --git a/packages/osd-charts/src/utils/themes/dark_theme.ts b/packages/osd-charts/src/utils/themes/dark_theme.ts new file mode 100644 index 000000000000..79bb60b4b295 --- /dev/null +++ b/packages/osd-charts/src/utils/themes/dark_theme.ts @@ -0,0 +1,208 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { palettes } from './colors'; +import { PointShape, Theme } from './theme'; +import { + DEFAULT_CHART_MARGINS, + DEFAULT_CHART_PADDING, + DEFAULT_GEOMETRY_STYLES, + DEFAULT_MISSING_COLOR, +} from './theme_common'; + +/** @public */ +export const DARK_THEME: Theme = { + chartPaddings: DEFAULT_CHART_PADDING, + chartMargins: DEFAULT_CHART_MARGINS, + lineSeriesStyle: { + line: { + visible: true, + strokeWidth: 1, + opacity: 1, + }, + point: { + visible: true, + strokeWidth: 1, + fill: 'black', + radius: 2, + opacity: 1, + shape: PointShape.Circle, + }, + }, + bubbleSeriesStyle: { + point: { + visible: true, + strokeWidth: 1, + fill: 'black', + radius: 2, + opacity: 1, + shape: PointShape.Circle, + }, + }, + areaSeriesStyle: { + area: { + visible: true, + opacity: 0.3, + }, + line: { + visible: true, + strokeWidth: 1, + opacity: 1, + }, + point: { + visible: false, + fill: 'black', + strokeWidth: 0.5, + radius: 1, + opacity: 1, + shape: PointShape.Circle, + }, + }, + barSeriesStyle: { + rect: { + opacity: 1, + }, + rectBorder: { + visible: false, + strokeWidth: 1, + }, + displayValue: { + fontSize: 8, + fontStyle: 'normal', + fontFamily: 'sans-serif', + padding: 0, + fill: '#999', + offsetX: 0, + offsetY: 0, + }, + }, + arcSeriesStyle: { + arc: { + visible: true, + stroke: 'white', + strokeWidth: 1, + opacity: 1, + }, + }, + sharedStyle: DEFAULT_GEOMETRY_STYLES, + scales: { + barsPadding: 0.25, + histogramPadding: 0.05, + }, + axes: { + axisTitle: { + fontSize: 12, + fontStyle: 'bold', + fontFamily: 'sans-serif', + padding: { + inner: 8, + outer: 0, + }, + fill: '#D4D4D4', + visible: true, + }, + axisPanelTitle: { + fontSize: 10, + fontStyle: 'bold', + fontFamily: 'sans-serif', + padding: { + inner: 8, + outer: 0, + }, + fill: '#D4D4D4', + visible: true, + }, + axisLine: { + visible: true, + stroke: '#444', + strokeWidth: 1, + }, + tickLabel: { + visible: true, + fontSize: 10, + fontFamily: 'sans-serif', + fontStyle: 'normal', + fill: '#999', + padding: 0, + rotation: 0, + offset: { + x: 0, + y: 0, + reference: 'local', + }, + alignment: { + vertical: 'near', + horizontal: 'near', + }, + }, + tickLine: { + visible: true, + stroke: '#444', + strokeWidth: 1, + size: 10, + padding: 10, + }, + gridLine: { + horizontal: { + visible: false, + stroke: '#D3DAE6', + strokeWidth: 1, + opacity: 1, + dash: [0, 0], + }, + vertical: { + visible: false, + stroke: '#D3DAE6', + strokeWidth: 1, + opacity: 1, + dash: [0, 0], + }, + }, + }, + colors: { + vizColors: palettes.echPaletteColorBlind.colors, + defaultVizColor: DEFAULT_MISSING_COLOR, + }, + legend: { + verticalWidth: 200, + horizontalHeight: 64, + spacingBuffer: 10, + margin: 0, + }, + crosshair: { + band: { + fill: '#2A2A2A', + visible: true, + }, + line: { + stroke: '#999', + strokeWidth: 1, + visible: true, + }, + crossLine: { + stroke: '#999', + strokeWidth: 1, + dash: [5, 5], + visible: true, + }, + }, + background: { + color: 'transparent', + }, +}; diff --git a/packages/osd-charts/src/utils/themes/light_theme.ts b/packages/osd-charts/src/utils/themes/light_theme.ts new file mode 100644 index 000000000000..a3a2f73929d8 --- /dev/null +++ b/packages/osd-charts/src/utils/themes/light_theme.ts @@ -0,0 +1,208 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { palettes } from './colors'; +import { PointShape, Theme } from './theme'; +import { + DEFAULT_CHART_MARGINS, + DEFAULT_CHART_PADDING, + DEFAULT_GEOMETRY_STYLES, + DEFAULT_MISSING_COLOR, +} from './theme_common'; + +/** @public */ +export const LIGHT_THEME: Theme = { + chartPaddings: DEFAULT_CHART_PADDING, + chartMargins: DEFAULT_CHART_MARGINS, + lineSeriesStyle: { + line: { + visible: true, + strokeWidth: 1, + opacity: 1, + }, + point: { + visible: true, + strokeWidth: 1, + fill: 'white', + radius: 2, + opacity: 1, + shape: PointShape.Circle, + }, + }, + bubbleSeriesStyle: { + point: { + visible: true, + strokeWidth: 1, + fill: 'white', + radius: 2, + opacity: 1, + shape: PointShape.Circle, + }, + }, + areaSeriesStyle: { + area: { + visible: true, + opacity: 0.3, + }, + line: { + visible: true, + strokeWidth: 1, + opacity: 1, + }, + point: { + visible: false, + strokeWidth: 1, + fill: 'white', + radius: 2, + opacity: 1, + shape: PointShape.Circle, + }, + }, + barSeriesStyle: { + rect: { + opacity: 1, + }, + rectBorder: { + visible: false, + strokeWidth: 1, + }, + displayValue: { + fontSize: 8, + fontStyle: 'normal', + fontFamily: 'sans-serif', + padding: 0, + fill: '#777', + offsetX: 0, + offsetY: 0, + }, + }, + arcSeriesStyle: { + arc: { + visible: true, + stroke: 'black', + strokeWidth: 1, + opacity: 1, + }, + }, + sharedStyle: DEFAULT_GEOMETRY_STYLES, + scales: { + barsPadding: 0.25, + histogramPadding: 0.05, + }, + axes: { + axisTitle: { + visible: true, + fontSize: 12, + fontStyle: 'bold', + fontFamily: 'sans-serif', + padding: { + inner: 8, + outer: 0, + }, + fill: '#333', + }, + axisPanelTitle: { + visible: true, + fontSize: 10, + fontStyle: 'bold', + fontFamily: 'sans-serif', + padding: { + inner: 8, + outer: 0, + }, + fill: '#333', + }, + axisLine: { + visible: true, + stroke: '#eaeaea', + strokeWidth: 1, + }, + tickLabel: { + visible: true, + fontSize: 10, + fontFamily: 'sans-serif', + fontStyle: 'normal', + fill: '#777', + padding: 0, + rotation: 0, + offset: { + x: 0, + y: 0, + reference: 'local', + }, + alignment: { + vertical: 'near', + horizontal: 'near', + }, + }, + tickLine: { + visible: true, + stroke: '#eaeaea', + strokeWidth: 1, + size: 10, + padding: 10, + }, + gridLine: { + horizontal: { + visible: false, + stroke: '#D3DAE6', + strokeWidth: 1, + opacity: 1, + dash: [0, 0], + }, + vertical: { + visible: false, + stroke: '#D3DAE6', + strokeWidth: 1, + opacity: 1, + dash: [0, 0], + }, + }, + }, + colors: { + vizColors: palettes.echPaletteColorBlind.colors, + defaultVizColor: DEFAULT_MISSING_COLOR, + }, + legend: { + verticalWidth: 200, + horizontalHeight: 64, + spacingBuffer: 10, + margin: 0, + }, + crosshair: { + band: { + fill: '#F5F5F5', + visible: true, + }, + line: { + stroke: '#98A2B3', + strokeWidth: 1, + visible: true, + }, + crossLine: { + stroke: '#98A2B3', + strokeWidth: 1, + dash: [5, 5], + visible: true, + }, + }, + background: { + color: 'transparent', + }, +}; diff --git a/packages/osd-charts/src/utils/themes/merge_utils.ts b/packages/osd-charts/src/utils/themes/merge_utils.ts new file mode 100644 index 000000000000..5b102a776a85 --- /dev/null +++ b/packages/osd-charts/src/utils/themes/merge_utils.ts @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mergePartial } from '../common'; +import { LIGHT_THEME } from './light_theme'; +import { LineAnnotationStyle, PartialTheme, RectAnnotationStyle, Theme } from './theme'; + +/** @public */ +export const DEFAULT_ANNOTATION_LINE_STYLE: LineAnnotationStyle = { + line: { + stroke: '#777', + strokeWidth: 1, + opacity: 1, + }, + details: { + fontSize: 10, + fontFamily: 'sans-serif', + fontStyle: 'normal', + fill: '#777', + padding: 0, + }, +}; +/** @public */ +export const DEFAULT_ANNOTATION_RECT_STYLE: RectAnnotationStyle = { + stroke: '#FFEEBC', + strokeWidth: 0, + opacity: 0.25, + fill: '#FFEEBC', +}; + +/** @public */ +export function mergeWithDefaultAnnotationLine(config?: Partial): LineAnnotationStyle { + const defaultLine = DEFAULT_ANNOTATION_LINE_STYLE.line; + const defaultDetails = DEFAULT_ANNOTATION_LINE_STYLE.details; + const mergedConfig: LineAnnotationStyle = { ...DEFAULT_ANNOTATION_LINE_STYLE }; + + if (!config) { + return mergedConfig; + } + + if (config.line) { + mergedConfig.line = { + ...defaultLine, + ...config.line, + }; + } + + if (config.details) { + mergedConfig.details = { + ...defaultDetails, + ...config.details, + }; + } + + return mergedConfig; +} + +/** @public */ +export function mergeWithDefaultAnnotationRect(config?: Partial): RectAnnotationStyle { + if (!config) { + return DEFAULT_ANNOTATION_RECT_STYLE; + } + + return { + ...DEFAULT_ANNOTATION_RECT_STYLE, + ...config, + }; +} + +/** + * Merge theme or themes with a base theme + * + * priority is based on spatial order + * + * @param theme - primary partial theme + * @param defaultTheme - base theme + * @param axillaryThemes - additional themes to be merged + * + * @public + */ +export function mergeWithDefaultTheme( + theme: PartialTheme, + defaultTheme: Theme = LIGHT_THEME, + axillaryThemes: PartialTheme[] = [], +): Theme { + return mergePartial(defaultTheme, theme, { mergeOptionalPartialValues: true }, axillaryThemes); +} diff --git a/packages/osd-charts/src/utils/themes/theme.test.ts b/packages/osd-charts/src/utils/themes/theme.test.ts new file mode 100644 index 000000000000..f3ffcede48a9 --- /dev/null +++ b/packages/osd-charts/src/utils/themes/theme.test.ts @@ -0,0 +1,463 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Margins } from '../dimensions'; +import { DARK_THEME } from './dark_theme'; +import { LIGHT_THEME } from './light_theme'; +import { + DEFAULT_ANNOTATION_LINE_STYLE, + DEFAULT_ANNOTATION_RECT_STYLE, + mergeWithDefaultAnnotationLine, + mergeWithDefaultAnnotationRect, + mergeWithDefaultTheme, +} from './merge_utils'; +import { AreaSeriesStyle, LineSeriesStyle, PartialTheme, PointShape, Theme } from './theme'; + +describe('Theme', () => { + let CLONED_LIGHT_THEME: Theme; + let CLONED_DARK_THEME: Theme; + + beforeEach(() => { + CLONED_LIGHT_THEME = JSON.parse(JSON.stringify(LIGHT_THEME)); + CLONED_DARK_THEME = JSON.parse(JSON.stringify(DARK_THEME)); + }); + + afterEach(() => { + // check default immutability + expect(LIGHT_THEME).toEqual(CLONED_LIGHT_THEME); + expect(DARK_THEME).toEqual(CLONED_DARK_THEME); + }); + + describe('mergeWithDefaultAnnotationLine', () => { + it('should merge custom and default annotation line configs', () => { + expect(mergeWithDefaultAnnotationLine()).toEqual(DEFAULT_ANNOTATION_LINE_STYLE); + + const customLineConfig = { + stroke: 'foo', + strokeWidth: 50, + opacity: 1, + }; + + const defaultLineConfig = { + stroke: '#777', + strokeWidth: 1, + opacity: 1, + }; + + const customDetailsConfig = { + fontSize: 50, + fontFamily: 'custom-font-family', + fontStyle: 'custom-font-style', + fill: 'custom-fill', + padding: 20, + }; + + const defaultDetailsConfig = { + fontSize: 10, + fill: '#777', + fontFamily: 'sans-serif', + fontStyle: 'normal', + padding: 0, + }; + + const expectedMergedCustomLineConfig = { line: customLineConfig, details: defaultDetailsConfig }; + const mergedCustomLineConfig = mergeWithDefaultAnnotationLine({ line: customLineConfig }); + expect(mergedCustomLineConfig).toEqual(expectedMergedCustomLineConfig); + + const expectedMergedCustomDetailsConfig = { line: defaultLineConfig, details: customDetailsConfig }; + const mergedCustomDetailsConfig = mergeWithDefaultAnnotationLine({ details: customDetailsConfig }); + expect(mergedCustomDetailsConfig).toEqual(expectedMergedCustomDetailsConfig); + }); + }); + + describe('mergeWithDefaultAnnotationRect', () => { + it('should merge custom and default rect annotation style', () => { + expect(mergeWithDefaultAnnotationRect()).toEqual(DEFAULT_ANNOTATION_RECT_STYLE); + + const customConfig = { + stroke: 'customStroke', + fill: 'customFill', + }; + + const expectedMergedConfig = { + stroke: 'customStroke', + fill: 'customFill', + opacity: 0.25, + strokeWidth: 0, + }; + + expect(mergeWithDefaultAnnotationRect(customConfig)).toEqual(expectedMergedConfig); + }); + }); + + describe('mergeWithDefaultTheme', () => { + it('should default to LIGHT_THEME', () => { + const partialTheme: PartialTheme = {}; + const mergedTheme = mergeWithDefaultTheme(partialTheme); + expect(mergedTheme).toEqual(LIGHT_THEME); + }); + + it('should merge partial theme: margins', () => { + const customTheme = mergeWithDefaultTheme({ + chartMargins: { + bottom: 314571, + top: 314571, + left: 314571, + right: 314571, + }, + }); + expect(customTheme.chartMargins).toBeDefined(); + expect(customTheme.chartMargins.bottom).toBe(314571); + expect(customTheme.chartMargins.left).toBe(314571); + expect(customTheme.chartMargins.right).toBe(314571); + expect(customTheme.chartMargins.top).toBe(314571); + }); + + it('should merge partial theme: paddings', () => { + const chartPaddings: Margins = { + bottom: 314571, + top: 314571, + left: 314571, + right: 314571, + }; + const customTheme = mergeWithDefaultTheme({ + chartPaddings, + }); + expect(customTheme.chartPaddings).toBeDefined(); + expect(customTheme.chartPaddings.bottom).toBe(314571); + expect(customTheme.chartPaddings.left).toBe(314571); + expect(customTheme.chartPaddings.right).toBe(314571); + expect(customTheme.chartPaddings.top).toBe(314571); + const customDarkTheme = mergeWithDefaultTheme( + { + chartPaddings, + }, + DARK_THEME, + ); + expect(customDarkTheme.chartPaddings).toEqual(chartPaddings); + }); + + it('should merge partial theme: lineSeriesStyle', () => { + const lineSeriesStyle: LineSeriesStyle = { + line: { + stroke: 'elastic_charts', + strokeWidth: 314571, + visible: true, + opacity: 1, + }, + point: { + fill: 'white', + radius: 314571, + stroke: 'elastic_charts', + strokeWidth: 314571, + visible: true, + opacity: 314571, + }, + }; + const customTheme = mergeWithDefaultTheme({ + lineSeriesStyle, + }); + expect(customTheme.lineSeriesStyle).toEqual({ + ...lineSeriesStyle, + point: { + ...lineSeriesStyle.point, + shape: PointShape.Circle, + }, + }); + const customDarkTheme = mergeWithDefaultTheme( + { + lineSeriesStyle, + }, + DARK_THEME, + ); + expect(customDarkTheme.lineSeriesStyle).toEqual({ + ...lineSeriesStyle, + point: { + ...lineSeriesStyle.point, + shape: PointShape.Circle, + }, + }); + }); + + it('should merge partial theme: areaSeriesStyle', () => { + const areaSeriesStyle: AreaSeriesStyle = { + area: { + fill: 'elastic_charts', + visible: true, + opacity: 314571, + }, + line: { + stroke: 'elastic_charts', + strokeWidth: 314571, + visible: true, + opacity: 1, + }, + point: { + fill: 'white', + visible: true, + radius: 314571, + stroke: 'elastic_charts', + strokeWidth: 314571, + opacity: 314571, + }, + }; + const customTheme = mergeWithDefaultTheme({ + areaSeriesStyle, + }); + expect(customTheme.areaSeriesStyle).toEqual({ + ...areaSeriesStyle, + point: { + ...areaSeriesStyle.point, + shape: PointShape.Circle, + }, + }); + const customDarkTheme = mergeWithDefaultTheme( + { + areaSeriesStyle, + }, + DARK_THEME, + ); + expect(customDarkTheme.areaSeriesStyle).toEqual({ + ...areaSeriesStyle, + point: { + ...areaSeriesStyle.point, + shape: PointShape.Circle, + }, + }); + }); + + it('should merge partial theme: barSeriesStyle', () => { + const partialTheme: PartialTheme = { + barSeriesStyle: { + rectBorder: { + stroke: 'elastic_charts', + }, + displayValue: { + fontSize: 10, + fontStyle: 'custom-font-style', + }, + }, + }; + const mergedTheme = mergeWithDefaultTheme(partialTheme, DARK_THEME); + expect(mergedTheme).toEqual({ + ...DARK_THEME, + barSeriesStyle: { + rect: { + opacity: 1, + }, + rectBorder: { + ...DARK_THEME.barSeriesStyle.rectBorder, + ...partialTheme.barSeriesStyle!.rectBorder, + }, + displayValue: { + ...DARK_THEME.barSeriesStyle.displayValue, + ...partialTheme.barSeriesStyle!.displayValue, + }, + }, + }); + }); + + it('should merge partial theme: sharedStyle', () => { + const partialTheme: PartialTheme = { + sharedStyle: { + highlighted: { + opacity: 100, + }, + }, + }; + const mergedTheme = mergeWithDefaultTheme(partialTheme, DARK_THEME); + expect(mergedTheme).toEqual({ + ...DARK_THEME, + sharedStyle: { + ...DARK_THEME.sharedStyle, + highlighted: { + ...DARK_THEME.sharedStyle.highlighted, + ...partialTheme.sharedStyle!.highlighted, + }, + }, + }); + }); + + it('should merge partial theme: scales', () => { + const partialTheme: PartialTheme = { + scales: { + barsPadding: 314571, + }, + }; + const mergedTheme = mergeWithDefaultTheme(partialTheme, DARK_THEME); + expect(mergedTheme).toEqual({ + ...DARK_THEME, + scales: { + ...DARK_THEME.scales, + ...partialTheme.scales, + }, + }); + }); + + it('should merge partial theme: axes', () => { + const partialTheme: PartialTheme = { + axes: { + axisTitle: { + fontStyle: 'elastic_charts', + }, + axisLine: { + stroke: 'elastic_charts', + }, + }, + }; + const mergedTheme = mergeWithDefaultTheme(partialTheme, DARK_THEME); + expect(mergedTheme).toEqual({ + ...DARK_THEME, + axes: { + ...DARK_THEME.axes, + axisTitle: { + ...DARK_THEME.axes.axisTitle, + ...partialTheme.axes!.axisTitle, + }, + axisLine: { + ...DARK_THEME.axes.axisLine, + ...partialTheme.axes!.axisLine, + }, + }, + }); + }); + + it('should merge partial theme: colors', () => { + const partialTheme: PartialTheme = { + colors: { + vizColors: ['elastic_charts_c1', 'elastic_charts_c2'], + }, + }; + const mergedTheme = mergeWithDefaultTheme(partialTheme, DARK_THEME); + expect(mergedTheme).toEqual({ + ...DARK_THEME, + colors: { + ...DARK_THEME.colors, + ...partialTheme.colors, + }, + }); + }); + + it('should merge partial theme: legend', () => { + const partialTheme: PartialTheme = { + legend: { + horizontalHeight: 314571, + }, + }; + const mergedTheme = mergeWithDefaultTheme(partialTheme, DARK_THEME); + expect(mergedTheme).toEqual({ + ...DARK_THEME, + legend: { + ...DARK_THEME.legend, + ...partialTheme.legend, + }, + }); + }); + + it('should merge partial theme: crosshair', () => { + const partialTheme: PartialTheme = { + crosshair: { + band: { + fill: 'elastic_charts_c1', + }, + line: { + strokeWidth: 314571, + }, + }, + }; + const mergedTheme = mergeWithDefaultTheme(partialTheme, DARK_THEME); + expect(mergedTheme).toEqual({ + ...DARK_THEME, + crosshair: { + ...DARK_THEME.crosshair, + band: { + ...DARK_THEME.crosshair.band, + ...partialTheme.crosshair!.band, + }, + line: { + ...DARK_THEME.crosshair.line, + ...partialTheme.crosshair!.line, + }, + }, + }); + }); + + it('should override all values if provided', () => { + const mergedTheme = mergeWithDefaultTheme(LIGHT_THEME, DARK_THEME); + expect(mergedTheme).toEqual(LIGHT_THEME); + }); + + it('should merge partial theme with axillaryThemes', () => { + const customTheme = mergeWithDefaultTheme( + { + chartMargins: { + bottom: 123, + }, + }, + LIGHT_THEME, + [ + { + chartMargins: { + top: 123, + }, + }, + { + chartMargins: { + left: 123, + }, + }, + ], + ); + expect(customTheme.chartMargins).toBeDefined(); + expect(customTheme.chartMargins.bottom).toBe(123); + expect(customTheme.chartMargins.top).toBe(123); + expect(customTheme.chartMargins.left).toBe(123); + }); + + it('should merge theme with axillaryThemes in spatial order priority', () => { + const customTheme = mergeWithDefaultTheme( + { + chartMargins: { + bottom: 1, + }, + }, + LIGHT_THEME, + [ + { + chartMargins: { + top: 2, + bottom: 2, + }, + }, + { + chartMargins: { + top: 3, + left: 3, + bottom: 3, + }, + }, + ], + ); + expect(customTheme.chartMargins).toBeDefined(); + expect(customTheme.chartMargins.bottom).toBe(1); + expect(customTheme.chartMargins.top).toBe(2); + expect(customTheme.chartMargins.left).toBe(3); + }); + }); +}); diff --git a/packages/osd-charts/src/utils/themes/theme.ts b/packages/osd-charts/src/utils/themes/theme.ts new file mode 100644 index 000000000000..5cb83434cf00 --- /dev/null +++ b/packages/osd-charts/src/utils/themes/theme.ts @@ -0,0 +1,549 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $Values } from 'utility-types'; + +import { Pixels, Ratio } from '../../common/geometry'; +import { Color, ColorVariant, HorizontalAlignment, RecursivePartial, VerticalAlignment } from '../common'; +import { Margins, SimplePadding } from '../dimensions'; +import { Point } from '../point'; + +/** @public */ +export interface Visible { + visible: boolean; +} + +/** @public */ +export interface TextStyle { + fontSize: number; + fontFamily: string; + fontStyle?: string; + fill: Color; + padding: number | SimplePadding; +} + +/** + * Offset in pixels + * @public + */ +export interface TextOffset { + /** + * X offset of tick in px or string with % of height + */ + x: number | string; + /** + * X offset of tick in px or string with % of height + */ + y: number | string; + /** + * Offset coordinate system reference + * + * - `global` - aligns offset coordinate system to global (non-rotated) coordinate system + * - `local` - aligns offset coordinate system to local rotated coordinate system + */ + reference: 'global' | 'local'; +} + +/** + * Text alignment + * @public + */ +export interface TextAlignment { + horizontal: HorizontalAlignment; + vertical: VerticalAlignment; +} + +/** + * Shared style properties for varies geometries + * @public + */ +export interface GeometryStyle { + /** + * Opacity multiplier + * + * if set to `0.5` all given opacities will be halfed + */ + opacity: number; +} + +/** + * Shared style properties for varies geometries + * @public + */ +export interface GeometryStateStyle { + /** + * Opacity multiplier + * + * if set to `0.5` all given opacities will be halfed + */ + opacity: number; +} + +/** @public */ +export interface SharedGeometryStateStyle { + default: GeometryStateStyle; + highlighted: GeometryStateStyle; + unhighlighted: GeometryStateStyle; +} + +/** + * The stroke color style + * @public + */ +export interface StrokeStyle { + /** The stroke color in hex, rgba, hsl */ + stroke: C; + /** The stroke width in pixel */ + strokeWidth: number; +} + +/** @public */ +export type TickStyle = StrokeStyle & + Visible & { + /** + * Amount of padding between tick line and labels + */ + padding: number; + /** + * length of tick line + */ + size: number; + }; + +/** + * The dash array for a stroke + * @public + */ +export interface StrokeDashArray { + /** The dash array for dashed strokes */ + dash: number[]; +} +/** @public */ +export interface FillStyle { + /** The fill color in hex, rgba, hsl */ + fill: Color; +} +/** @public */ +export interface Opacity { + /** The opacity value from 0 to 1 */ + opacity: number; +} + +/** @public */ +export interface AxisStyle { + axisTitle: TextStyle & Visible; + axisPanelTitle: TextStyle & Visible; + axisLine: StrokeStyle & Visible; + tickLabel: TextStyle & + Visible & { + /** The degrees of rotation of the tick labels */ + rotation: number; + /** + * Offset in pixels to render text relative to anchor + * + * **Note:** rotation aligns to global cartesian coordinates + */ + offset: TextOffset; + alignment: TextAlignment; + }; + tickLine: TickStyle; + gridLine: { + horizontal: GridLineStyle; + vertical: GridLineStyle; + }; +} + +/** + * @public + */ +export interface GridLineStyle { + visible: boolean; + stroke: Color; + strokeWidth: number; + opacity: number; + dash: number[]; +} +/** @public */ +export interface ScalesConfig { + /** + * The proportion of the range that is reserved for blank space between bands. + * A value of 0 means no blank space between bands, and a value of 1 means a bandwidth of zero. + * A number between 0 and 1. + */ + barsPadding: number; + /** + * The proportion of the range that is reserved for blank space between bands in histogramMode. + * A value of 0 means no blank space between bands, and a value of 1 means a bandwidth of zero. + * A number between 0 and 1. + */ + histogramPadding: number; +} +/** @public */ +export interface ColorConfig { + vizColors: Color[]; + defaultVizColor: Color; +} +/** + * The background style applied to the chart. + * This is used to coordinate adequate contrast of the text in partition and treemap charts. + * @public + */ +export interface BackgroundStyle { + /** + * The background color + */ + color: string; +} +/** @public */ +export interface LegendStyle { + /** + * Max width used for left/right legend + * + * or + * + * Width of `LegendItem` for top/bottom legend + */ + verticalWidth: number; + /** + * Max height used for top/bottom legend + */ + horizontalHeight: number; + /** + * Added buffer between label and value. + * + * Smaller values render a more compact legend + */ + spacingBuffer: number; + /** + * Legend padding. The Chart margins are independent of the legend. + * + * TODO: make SimplePadding when after axis changes are added + */ + margin: number; +} +/** @public */ +export interface Theme { + /** + * Space btw parent DOM element and first available element of the chart (axis if exists, else the chart itself) + */ + chartMargins: Margins; + /** + * Space btw the chart geometries and axis; if no axis, pads space btw chart & container + */ + chartPaddings: Margins; + /** + * Global line styles. + * + * __Note:__ This is not used to set the color of a specific series. As such, any changes to the styles will not be reflected in the tooltip, legend, etc.. + * + * You may use `SeriesColorAccessor` to assign colors to a given series or replace the `theme.colors.vizColors` colors to your desired colors. + */ + lineSeriesStyle: LineSeriesStyle; + /** + * Global area styles. + * + * __Note:__ This is not used to set the color of a specific series. As such, any changes to the styles will not be reflected in the tooltip, legend, etc.. + * + * You may use `SeriesColorAccessor` to assign colors to a given series or replace the `theme.colors.vizColors` colors to your desired colors. + */ + areaSeriesStyle: AreaSeriesStyle; + /** + * Global bar styles. + * + * __Note:__ This is not used to set the color of a specific series. As such, any changes to the styles will not be reflected in the tooltip, legend, etc.. + * + * You may use `SeriesColorAccessor` to assign colors to a given series or replace the `theme.colors.vizColors` colors to your desired colors. + */ + barSeriesStyle: BarSeriesStyle; + /** + * Global bubble styles. + * + * __Note:__ This is not used to set the color of a specific series. As such, any changes to the styles will not be reflected in the tooltip, legend, etc.. + * + * You may use `SeriesColorAccessor` to assign colors to a given series or replace the `theme.colors.vizColors` colors to your desired colors. + */ + bubbleSeriesStyle: BubbleSeriesStyle; + arcSeriesStyle: ArcSeriesStyle; + sharedStyle: SharedGeometryStateStyle; + axes: AxisStyle; + scales: ScalesConfig; + colors: ColorConfig; + legend: LegendStyle; + crosshair: CrosshairStyle; + /** + * Used to scale radius with `markSizeAccessor` + * + * value from 1 to 100 + */ + markSizeRatio?: number; + /** + * The background allows the consumer to provide a color of the background container of the chart. + * This can then be used to calculate the contrast of the text for partition charts. + */ + background: BackgroundStyle; +} + +/** @public */ +export type PartialTheme = RecursivePartial; + +/** @public */ +export type DisplayValueStyle = Omit & { + offsetX: number; + offsetY: number; + fontSize: + | number + | { + min: number; + max: number; + }; + fill: + | Color + | { color: Color; borderColor?: Color; borderWidth?: number } + | { + textInvertible: boolean; + textContrast?: number | boolean; + textBorder?: number; + }; + alignment?: { + horizontal: Exclude; + vertical: Exclude; + }; +}; + +/** @public */ +export const PointShape = Object.freeze({ + Circle: 'circle' as const, + Square: 'square' as const, + Diamond: 'diamond' as const, + Plus: 'plus' as const, + X: 'x' as const, + Triangle: 'triangle' as const, +}); +/** @public */ +export type PointShape = $Values; + +/** @public */ +export interface PointStyle { + /** is the point visible or hidden */ + visible: boolean; + /** a static stroke color if defined, if not it will use the color of the series */ + stroke?: Color | ColorVariant; + /** the stroke width of the point */ + strokeWidth: number; + /** a static fill color if defined, if not it will use the color of the series */ + fill?: Color | ColorVariant; + /** the opacity of each point on the theme/series */ + opacity: number; + /** the radius of each point of the theme/series */ + radius: number; + /** shape for the point, default to circle */ + shape?: PointShape; +} + +/** @public */ +export interface LineStyle { + /** is the line visible or hidden ? */ + visible: boolean; + /** a static stroke color if defined, if not it will use the color of the series */ + stroke?: Color | ColorVariant; + /** the stroke width of the line */ + strokeWidth: number; + /** the opacity of each line on the theme/series */ + opacity: number; + /** the dash array */ + dash?: number[]; +} + +/** @public */ +export const TextureShape = Object.freeze({ + ...PointShape, + Line: 'line' as const, +}); +/** @public */ +export type TextureShape = $Values; + +/** @public */ +export interface TexturedStylesBase { + /** polygon fill color for texture */ + fill?: Color | ColorVariant; + /** polygon stroke color for texture */ + stroke?: Color | ColorVariant; + /** polygon stroke width for texture */ + strokeWidth?: number; + /** polygon opacity for texture */ + opacity?: number; + /** polygon opacity for texture */ + dash?: number[]; + /** polygon opacity for texture */ + size?: number; + /** + * The angle of rotation for entire texture + * in degrees + */ + rotation?: number; + /** + * The angle of rotation for polygons + * in degrees + */ + shapeRotation?: number; + /** texture spacing between polygons */ + spacing?: Partial | number; + /** overall origin offset of pattern */ + offset?: Partial & { + /** apply offset along global coordinate axes */ + global?: boolean; + }; +} + +/** @public */ +export interface TexturedShapeStyles extends TexturedStylesBase { + /** typed of texture designs currently supported */ + shape: TextureShape; +} + +/** @public */ +export interface TexturedPathStyles extends TexturedStylesBase { + /** path for polygon texture */ + path: string | Path2D; +} + +/** + * @public + * + * Texture style config for area spec + */ +export type TexturedStyles = TexturedPathStyles | TexturedShapeStyles; + +/** @public */ +export interface AreaStyle { + /** applying textures to the area on the theme/series */ + texture?: TexturedStyles; + /** is the area is visible or hidden ? */ + visible: boolean; + /** a static fill color if defined, if not it will use the color of the series */ + fill?: Color | ColorVariant; + /** the opacity of each area on the theme/series */ + opacity: number; +} + +/** @public */ +export interface ArcStyle { + /** is the arc is visible or hidden ? */ + visible: boolean; + /** a static fill color if defined, if not it will use the color of the series */ + fill?: Color | ColorVariant; + /** a static stroke color if defined, if not it will use the color of the series */ + stroke?: Color | ColorVariant; + /** the stroke width of the line */ + strokeWidth: number; + /** the opacity of each arc on the theme/series */ + opacity: number; +} + +/** @public */ +export interface RectStyle { + /** a static fill color if defined, if not it will use the color of the series */ + fill?: Color | ColorVariant; + /** the opacity of each rect on the theme/series */ + opacity: number; + /** The width of the rect in pixel. If expressed together with `widthRatio` then the `widthRatio` + * will express the max available size, where the `widthPixel` express the derived/min width. */ + widthPixel?: Pixels; + /** The ratio of the width limited to [0,1]. If expressed together with `widthPixel` then the `widthRatio` + * will express the max available size, where the `widthPixel` express the derived/min width. */ + widthRatio?: Ratio; + /** applying textures to the bar on the theme/series */ + texture?: TexturedStyles; +} + +/** @public */ +export interface RectBorderStyle { + /** + * Border visibility + */ + visible: boolean; + /** + * Border stroke color + */ + stroke?: Color | ColorVariant; + /** + * Border stroke width + */ + strokeWidth: number; + /** + * Border stroke opacity + */ + strokeOpacity?: number; +} +/** @public */ +export interface BarSeriesStyle { + rect: RectStyle; + rectBorder: RectBorderStyle; + displayValue: DisplayValueStyle; +} + +/** @public */ +export interface BubbleSeriesStyle { + point: PointStyle; +} + +/** @public */ +export interface LineSeriesStyle { + line: LineStyle; + point: PointStyle; +} + +/** @public */ +export interface AreaSeriesStyle { + area: AreaStyle; + line: LineStyle; + point: PointStyle; +} + +/** @public */ +export interface ArcSeriesStyle { + arc: ArcStyle; +} + +/** @public */ +export interface CrosshairStyle { + band: FillStyle & Visible; + line: StrokeStyle & Visible & Partial; + crossLine: StrokeStyle & Visible & Partial; +} + +/** + * The style for a linear annotation + * @public + */ +export interface LineAnnotationStyle { + /** + * The style for the line geometry + */ + line: StrokeStyle & Opacity & Partial; + /** + * The style for the text shown on the tooltip. + * @deprecated This style is not currently used and will + * soon be removed. + */ + details: TextStyle; +} + +/** @public */ +export type RectAnnotationStyle = StrokeStyle & FillStyle & Opacity & Partial; diff --git a/packages/osd-charts/src/utils/themes/theme_common.ts b/packages/osd-charts/src/utils/themes/theme_common.ts new file mode 100644 index 000000000000..f3f9f4c68c4a --- /dev/null +++ b/packages/osd-charts/src/utils/themes/theme_common.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Margins } from '../dimensions'; +import { SharedGeometryStateStyle } from './theme'; + +/** @public */ +export const DEFAULT_MISSING_COLOR = 'red'; + +/** @public */ +export const DEFAULT_CHART_PADDING: Margins = { + left: 0, + right: 0, + top: 0, + bottom: 0, +}; +/** @public */ +export const DEFAULT_CHART_MARGINS: Margins = { + left: 10, + right: 10, + top: 10, + bottom: 10, +}; + +/** @public */ +export const DEFAULT_GEOMETRY_STYLES: SharedGeometryStateStyle = { + default: { + opacity: 1, + }, + highlighted: { + opacity: 1, + }, + unhighlighted: { + opacity: 0.25, + }, +}; diff --git a/packages/osd-charts/tsconfig.check.json b/packages/osd-charts/tsconfig.check.json new file mode 100644 index 000000000000..1346a420c59c --- /dev/null +++ b/packages/osd-charts/tsconfig.check.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "noUnusedLocals": true, + "removeComments": true, + "noEmit": true, + "skipLibCheck": false, + "rootDir": "dist" + }, + "files": [], + "include": ["dist"] +} diff --git a/packages/osd-charts/tsconfig.json b/packages/osd-charts/tsconfig.json new file mode 100644 index 000000000000..4bc17507a534 --- /dev/null +++ b/packages/osd-charts/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "outDir": "dist", + "noUnusedLocals": true, + "removeComments": false + }, + "files": [], + "include": ["src/**/*"], + "exclude": ["**/*.test.*", "**/__mocks__", "src/mocks/**/*", "src/utils/data_samples/**/*"] +} diff --git a/packages/osd-charts/tsconfig.nocomments.json b/packages/osd-charts/tsconfig.nocomments.json new file mode 100644 index 000000000000..d35b77b18e1c --- /dev/null +++ b/packages/osd-charts/tsconfig.nocomments.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "removeComments": true, + "declaration": false, + "declarationMap": false + }, + "files": [] +}