diff --git a/src/i18n/keysets/en.json b/src/i18n/keysets/en.json index eaf2cea9..3fdb3704 100644 --- a/src/i18n/keysets/en.json +++ b/src/i18n/keysets/en.json @@ -36,7 +36,8 @@ "label_invalid-series-type": "It seems you haven't defined \"series.type\" property, or defined it incorrectly. Available values: [{{types}}].", "label_invalid-series-property": "It seems you are trying to use inappropriate value for \"{{key}}\", or defined it incorrectly. Available values: [{{values}}].", "label_invalid-treemap-redundant-value": "It seems you are trying to set \"value\" for container node. Check node with this properties: { id: \"{{id}}\", name: \"{{name}}\" }", - "label_invalid-treemap-missing-value": "It seems you are trying to use node without \"value\". Check node with this properties: { id: \"{{id}}\", name: \"{{name}}\" }" + "label_invalid-treemap-missing-value": "It seems you are trying to use node without \"value\". Check node with this properties: { id: \"{{id}}\", name: \"{{name}}\" }", + "label_invalid-y-axis-index": "It seems you are trying to use inappropriate index for Y axis: \"{{index}}\"" }, "highcharts": { "reset-zoom-title": "Reset zoom", diff --git a/src/i18n/keysets/ru.json b/src/i18n/keysets/ru.json index fa6ce219..9693b140 100644 --- a/src/i18n/keysets/ru.json +++ b/src/i18n/keysets/ru.json @@ -7,13 +7,10 @@ "error": "Ошибка", "legend-series-hide": "Скрыть все линии", "legend-series-show": "Показать все линии", - "loading": "Загрузка", - "tooltip-point-format-size": "Размер", "tooltip-sum": "Сумма", "tooltip-rest": "Остальные", - "error-incorrect-key-value-intro": "Некорректный формат объекта переданного как значение в", "error-incorrect-key": ", ключи объекта должны быть преобразуемы в целое число", "error-incorrect-value": ", значением объекта может быть либо строка, либо функция возвращающая строку" @@ -38,7 +35,8 @@ "label_invalid-series-type": "Похоже, что вы не указали значение \"series.type\" или указали его неверно. Доступные значения: [{{types}}].", "label_invalid-series-property": "Похоже, что вы пытаетесь использовать недопустимое значение для \"{{key}}\", или указали его неверно. Доступные значения: [{{values}}].", "label_invalid-treemap-redundant-value": "Похоже, что вы пытаетесь установить значение \"value\" для узла, используемого в качестве контейнера. Проверьте узел с этими свойствами: { id: \"{{id}}\", name: \"{{name}}\" }", - "label_invalid-treemap-missing-value": "Похоже, что вы пытаетесь использовать узел без значения \"value\". Проверьте узел с этими свойствами: { id: \"{{id}}\", name: \"{{name}}\" }" + "label_invalid-treemap-missing-value": "Похоже, что вы пытаетесь использовать узел без значения \"value\". Проверьте узел с этими свойствами: { id: \"{{id}}\", name: \"{{name}}\" }", + "label_invalid-y-axis-index": "Похоже, что вы пытаетесь использовать некорректный индекс для оси Y: \"{{index}}\"" }, "highcharts": { "reset-zoom-title": "Сбросить увеличение", diff --git a/src/plugins/d3/__stories__/Showcase.stories.tsx b/src/plugins/d3/__stories__/Showcase.stories.tsx index 5a7c8882..48945fcc 100644 --- a/src/plugins/d3/__stories__/Showcase.stories.tsx +++ b/src/plugins/d3/__stories__/Showcase.stories.tsx @@ -9,11 +9,13 @@ import {settings} from '../../../libs'; import {Basic as BasicArea} from '../examples/area/Basic'; import {PercentStackingArea} from '../examples/area/PercentStacking'; import {StackedArea} from '../examples/area/StackedArea'; +import {TwoYAxis as AreaTwoYAxis} from '../examples/area/TwoYAxis'; import {BasicBarXChart} from '../examples/bar-x/Basic'; import {DataLabels as BarXDataLabels} from '../examples/bar-x/DataLabels'; import {GroupedColumns} from '../examples/bar-x/GroupedColumns'; import {PercentStackColumns} from '../examples/bar-x/PercentStack'; import {StackedColumns} from '../examples/bar-x/StackedColumns'; +import {TwoYAxis as BarXTwoYAxis} from '../examples/bar-x/TwoYAxis'; import {Basic as BasicBarY} from '../examples/bar-y/Basic'; import {GroupedColumns as GroupedColumnsBarY} from '../examples/bar-y/GroupedColumns'; import {PercentStackingBars} from '../examples/bar-y/PercentStacking'; @@ -23,9 +25,11 @@ import {Basic as BasicLine} from '../examples/line/Basic'; import {DataLabels as LineWithDataLabels} from '../examples/line/DataLabels'; import {LineWithMarkers} from '../examples/line/LineWithMarkers'; import {LinesWithShapes} from '../examples/line/Shapes'; +import {TwoYAxis as LineTwoYAxis} from '../examples/line/TwoYAxis'; import {BasicPie} from '../examples/pie/Basic'; import {Donut} from '../examples/pie/Donut'; import {Basic as BasicScatter} from '../examples/scatter/Basic'; +import {TwoYAxis as ScatterTwoYAxis} from '../examples/scatter/TwoYAxis'; import {D3Plugin} from '../index'; const ShowcaseStory = () => { @@ -62,6 +66,10 @@ const ShowcaseStory = () => { Lines with different shapes + + Line with two Y axis + + Area charts @@ -79,6 +87,10 @@ const ShowcaseStory = () => { Stacked percentage areas + + Dual Y axis + + Bar-x charts @@ -104,6 +116,10 @@ const ShowcaseStory = () => { Bar-x chart with data labels + + Dual Y axis + + Bar-y charts @@ -143,10 +159,14 @@ const ShowcaseStory = () => { Scatter charts - + Basic scatter + + Scatter chart with two Y axis + + Combined charts diff --git a/src/plugins/d3/examples/area/TwoYAxis.tsx b/src/plugins/d3/examples/area/TwoYAxis.tsx new file mode 100644 index 00000000..e96dcfd4 --- /dev/null +++ b/src/plugins/d3/examples/area/TwoYAxis.tsx @@ -0,0 +1,68 @@ +import React from 'react'; + +import {dateTime} from '@gravity-ui/date-utils'; + +import {ChartKit} from '../../../../components/ChartKit'; +import type {ChartKitWidgetData} from '../../../../types'; +import {ExampleWrapper} from '../ExampleWrapper'; +import marsWeatherData from '../mars-weather'; + +export const TwoYAxis = () => { + const data = marsWeatherData as any[]; + const pressureData = data.map((d) => ({ + x: dateTime({input: d.terrestrial_date, format: 'YYYY-MM-DD'}).valueOf(), + y: d.pressure, + })); + + const tempData = data.map((d) => ({ + x: dateTime({input: d.terrestrial_date, format: 'YYYY-MM-DD'}).valueOf(), + y: d.max_temp - d.min_temp, + })); + + const widgetData: ChartKitWidgetData = { + series: { + data: [ + { + type: 'area', + data: pressureData, + name: 'Pressure', + yAxis: 0, + }, + { + type: 'area', + data: tempData, + name: 'Temperature range', + yAxis: 1, + }, + ], + }, + yAxis: [ + { + title: { + text: 'Pressure', + }, + }, + { + title: { + text: 'Temperature range', + }, + }, + ], + xAxis: { + type: 'datetime', + title: { + text: 'Terrestrial date', + }, + ticks: {pixelInterval: 200}, + }, + title: { + text: 'Mars weather', + }, + }; + + return ( + + + + ); +}; diff --git a/src/plugins/d3/examples/bar-x/TwoYAxis.tsx b/src/plugins/d3/examples/bar-x/TwoYAxis.tsx new file mode 100644 index 00000000..ab6d9e64 --- /dev/null +++ b/src/plugins/d3/examples/bar-x/TwoYAxis.tsx @@ -0,0 +1,68 @@ +import React from 'react'; + +import {dateTime} from '@gravity-ui/date-utils'; + +import {ChartKit} from '../../../../components/ChartKit'; +import type {ChartKitWidgetData} from '../../../../types'; +import {ExampleWrapper} from '../ExampleWrapper'; +import marsWeatherData from '../mars-weather'; + +export const TwoYAxis = () => { + const data = marsWeatherData as any[]; + const pressureData = data.map((d) => ({ + x: dateTime({input: d.terrestrial_date, format: 'YYYY-MM-DD'}).valueOf(), + y: d.pressure, + })); + + const tempData = data.map((d) => ({ + x: dateTime({input: d.terrestrial_date, format: 'YYYY-MM-DD'}).valueOf(), + y: d.max_temp - d.min_temp, + })); + + const widgetData: ChartKitWidgetData = { + series: { + data: [ + { + type: 'bar-x', + data: pressureData, + name: 'Pressure', + yAxis: 0, + }, + { + type: 'bar-x', + data: tempData, + name: 'Temperature range', + yAxis: 1, + }, + ], + }, + yAxis: [ + { + title: { + text: 'Pressure', + }, + }, + { + title: { + text: 'Temperature range', + }, + }, + ], + xAxis: { + type: 'datetime', + title: { + text: 'Terrestrial date', + }, + ticks: {pixelInterval: 200}, + }, + title: { + text: 'Mars weather', + }, + }; + + return ( + + + + ); +}; diff --git a/src/plugins/d3/examples/line/TwoYAxis.tsx b/src/plugins/d3/examples/line/TwoYAxis.tsx new file mode 100644 index 00000000..6545bb9c --- /dev/null +++ b/src/plugins/d3/examples/line/TwoYAxis.tsx @@ -0,0 +1,68 @@ +import React from 'react'; + +import {dateTime} from '@gravity-ui/date-utils'; + +import {ChartKit} from '../../../../components/ChartKit'; +import type {ChartKitWidgetData} from '../../../../types'; +import {ExampleWrapper} from '../ExampleWrapper'; +import marsWeatherData from '../mars-weather'; + +export const TwoYAxis = () => { + const data = marsWeatherData as any[]; + const minTempData = data.map((d) => ({ + x: dateTime({input: d.terrestrial_date, format: 'YYYY-MM-DD'}).valueOf(), + y: d.min_temp, + })); + + const maxTempData = data.map((d) => ({ + x: dateTime({input: d.terrestrial_date, format: 'YYYY-MM-DD'}).valueOf(), + y: d.max_temp, + })); + + const widgetData: ChartKitWidgetData = { + series: { + data: [ + { + type: 'line', + data: minTempData, + name: 'Min Temperature', + yAxis: 0, + }, + { + type: 'line', + data: maxTempData, + name: 'Max Temperature', + yAxis: 1, + }, + ], + }, + yAxis: [ + { + title: { + text: 'Min', + }, + }, + { + title: { + text: 'Max', + }, + }, + ], + xAxis: { + type: 'datetime', + title: { + text: 'Terrestrial date', + }, + ticks: {pixelInterval: 200}, + }, + title: { + text: 'Mars weather', + }, + }; + + return ( + + + + ); +}; diff --git a/src/plugins/d3/examples/mars-weather.js b/src/plugins/d3/examples/mars-weather.js new file mode 100644 index 00000000..28f074ff --- /dev/null +++ b/src/plugins/d3/examples/mars-weather.js @@ -0,0 +1,1203 @@ +// source: https://www.kaggle.com/datasets/thedevastator/mars-weather-data-from-2012-to-2018 +export default [ + { + id: '541', + terrestrial_date: '2014-04-14', + sol: 600, + ls: 116, + season: 'Month 4', + min_temp: -81, + max_temp: -25, + pressure: 787, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '543', + terrestrial_date: '2014-04-13', + sol: 599, + ls: 115, + season: 'Month 4', + min_temp: -84, + max_temp: -20, + pressure: 788, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '542', + terrestrial_date: '2014-04-12', + sol: 598, + ls: 115, + season: 'Month 4', + min_temp: -84, + max_temp: -25, + pressure: 787, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '540', + terrestrial_date: '2014-04-11', + sol: 597, + ls: 114, + season: 'Month 4', + min_temp: -84, + max_temp: -26, + pressure: 790, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '539', + terrestrial_date: '2014-04-10', + sol: 596, + ls: 114, + season: 'Month 4', + min_temp: -82, + max_temp: -24, + pressure: 791, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '538', + terrestrial_date: '2014-04-09', + sol: 595, + ls: 113, + season: 'Month 4', + min_temp: -83, + max_temp: -25, + pressure: 792, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '537', + terrestrial_date: '2014-04-08', + sol: 594, + ls: 113, + season: 'Month 4', + min_temp: -83, + max_temp: -22, + pressure: 793, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '536', + terrestrial_date: '2014-04-07', + sol: 593, + ls: 112, + season: 'Month 4', + min_temp: -83, + max_temp: -23, + pressure: 795, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '534', + terrestrial_date: '2014-04-06', + sol: 592, + ls: 112, + season: 'Month 4', + min_temp: -83, + max_temp: -26, + pressure: 795, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '533', + terrestrial_date: '2014-04-05', + sol: 591, + ls: 111, + season: 'Month 4', + min_temp: -82, + max_temp: -26, + pressure: 795, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '535', + terrestrial_date: '2014-04-04', + sol: 590, + ls: 111, + season: 'Month 4', + min_temp: -82, + max_temp: -26, + pressure: 797, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '531', + terrestrial_date: '2014-04-03', + sol: 589, + ls: 110, + season: 'Month 4', + min_temp: -85, + max_temp: -27, + pressure: 798, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '532', + terrestrial_date: '2014-04-02', + sol: 588, + ls: 110, + season: 'Month 4', + min_temp: -84, + max_temp: -24, + pressure: 799, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '530', + terrestrial_date: '2014-04-01', + sol: 587, + ls: 109, + season: 'Month 4', + min_temp: -85, + max_temp: -28, + pressure: 801, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '529', + terrestrial_date: '2014-03-31', + sol: 586, + ls: 109, + season: 'Month 4', + min_temp: -82, + max_temp: -24, + pressure: 802, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '527', + terrestrial_date: '2014-03-30', + sol: 585, + ls: 109, + season: 'Month 4', + min_temp: -83, + max_temp: -26, + pressure: 802, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '528', + terrestrial_date: '2014-03-29', + sol: 584, + ls: 108, + season: 'Month 4', + min_temp: -82, + max_temp: -25, + pressure: 804, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '526', + terrestrial_date: '2014-03-28', + sol: 583, + ls: 108, + season: 'Month 4', + min_temp: -82, + max_temp: -27, + pressure: 806, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '525', + terrestrial_date: '2014-03-27', + sol: 582, + ls: 107, + season: 'Month 4', + min_temp: -84, + max_temp: -27, + pressure: 807, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '524', + terrestrial_date: '2014-03-26', + sol: 581, + ls: 107, + season: 'Month 4', + min_temp: -84, + max_temp: -28, + pressure: 808, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '523', + terrestrial_date: '2014-03-25', + sol: 580, + ls: 106, + season: 'Month 4', + min_temp: -83, + max_temp: -24, + pressure: 810, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '522', + terrestrial_date: '2014-03-24', + sol: 579, + ls: 106, + season: 'Month 4', + min_temp: -82, + max_temp: -22, + pressure: 811, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '521', + terrestrial_date: '2014-03-22', + sol: 578, + ls: 105, + season: 'Month 4', + min_temp: -83, + max_temp: -23, + pressure: 812, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '519', + terrestrial_date: '2014-03-21', + sol: 577, + ls: 105, + season: 'Month 4', + min_temp: -84, + max_temp: -27, + pressure: 813, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '520', + terrestrial_date: '2014-03-20', + sol: 576, + ls: 104, + season: 'Month 4', + min_temp: -84, + max_temp: -26, + pressure: 815, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '518', + terrestrial_date: '2014-03-19', + sol: 575, + ls: 104, + season: 'Month 4', + min_temp: -82, + max_temp: -26, + pressure: 816, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '517', + terrestrial_date: '2014-03-18', + sol: 574, + ls: 103, + season: 'Month 4', + min_temp: -85, + max_temp: -23, + pressure: 817, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '516', + terrestrial_date: '2014-03-17', + sol: 573, + ls: 103, + season: 'Month 4', + min_temp: -84, + max_temp: -27, + pressure: 819, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '514', + terrestrial_date: '2014-03-16', + sol: 572, + ls: 102, + season: 'Month 4', + min_temp: -85, + max_temp: -27, + pressure: 820, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '515', + terrestrial_date: '2014-03-15', + sol: 571, + ls: 102, + season: 'Month 4', + min_temp: -84, + max_temp: -26, + pressure: 821, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '513', + terrestrial_date: '2014-03-14', + sol: 570, + ls: 102, + season: 'Month 4', + min_temp: -84, + max_temp: -26, + pressure: 823, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '512', + terrestrial_date: '2014-03-13', + sol: 569, + ls: 101, + season: 'Month 4', + min_temp: -85, + max_temp: -27, + pressure: 825, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '511', + terrestrial_date: '2014-03-12', + sol: 568, + ls: 101, + season: 'Month 4', + min_temp: -86, + max_temp: -28, + pressure: 825, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '510', + terrestrial_date: '2014-03-11', + sol: 567, + ls: 100, + season: 'Month 4', + min_temp: -86, + max_temp: -28, + pressure: 827, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '507', + terrestrial_date: '2014-03-10', + sol: 566, + ls: 100, + season: 'Month 4', + min_temp: -86, + max_temp: -27, + pressure: 829, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '508', + terrestrial_date: '2014-03-09', + sol: 565, + ls: 99, + season: 'Month 4', + min_temp: -85, + max_temp: -27, + pressure: 830, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '509', + terrestrial_date: '2014-03-08', + sol: 564, + ls: 99, + season: 'Month 4', + min_temp: -86, + max_temp: -27, + pressure: 831, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '505', + terrestrial_date: '2014-03-07', + sol: 563, + ls: 98, + season: 'Month 4', + min_temp: -87, + max_temp: -31, + pressure: 833, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '506', + terrestrial_date: '2014-03-06', + sol: 562, + ls: 98, + season: 'Month 4', + min_temp: -85, + max_temp: -23, + pressure: 834, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '504', + terrestrial_date: '2014-03-05', + sol: 561, + ls: 97, + season: 'Month 4', + min_temp: -85, + max_temp: -23, + pressure: 835, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '503', + terrestrial_date: '2014-03-04', + sol: 560, + ls: 97, + season: 'Month 4', + min_temp: -86, + max_temp: -23, + pressure: 836, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '502', + terrestrial_date: '2014-03-03', + sol: 559, + ls: 96, + season: 'Month 4', + min_temp: -85, + max_temp: -27, + pressure: 838, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '500', + terrestrial_date: '2014-03-02', + sol: 558, + ls: 96, + season: 'Month 4', + min_temp: -85, + max_temp: -26, + pressure: 839, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '498', + terrestrial_date: '2014-03-01', + sol: 557, + ls: 96, + season: 'Month 4', + min_temp: -85, + max_temp: -29, + pressure: 840, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '501', + terrestrial_date: '2014-02-28', + sol: 556, + ls: 95, + season: 'Month 4', + min_temp: -85, + max_temp: -29, + pressure: 842, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '499', + terrestrial_date: '2014-02-27', + sol: 555, + ls: 95, + season: 'Month 4', + min_temp: -85, + max_temp: -31, + pressure: 843, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '497', + terrestrial_date: '2014-02-26', + sol: 554, + ls: 94, + season: 'Month 4', + min_temp: -84, + max_temp: -22, + pressure: 843, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '496', + terrestrial_date: '2014-02-25', + sol: 553, + ls: 94, + season: 'Month 4', + min_temp: -84, + max_temp: -26, + pressure: 845, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '493', + terrestrial_date: '2014-02-24', + sol: 552, + ls: 93, + season: 'Month 4', + min_temp: -86, + max_temp: -29, + pressure: 847, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '495', + terrestrial_date: '2014-02-23', + sol: 551, + ls: 93, + season: 'Month 4', + min_temp: -85, + max_temp: -28, + pressure: 848, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '494', + terrestrial_date: '2014-02-22', + sol: 550, + ls: 92, + season: 'Month 4', + min_temp: -85, + max_temp: -27, + pressure: 850, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '492', + terrestrial_date: '2014-02-21', + sol: 549, + ls: 92, + season: 'Month 4', + min_temp: -87, + max_temp: -23, + pressure: 851, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '491', + terrestrial_date: '2014-02-20', + sol: 548, + ls: 91, + season: 'Month 4', + min_temp: -86, + max_temp: -28, + pressure: 852, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '488', + terrestrial_date: '2014-02-19', + sol: 547, + ls: 91, + season: 'Month 4', + min_temp: -85, + max_temp: -29, + pressure: 853, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '489', + terrestrial_date: '2014-02-18', + sol: 546, + ls: 91, + season: 'Month 4', + min_temp: -85, + max_temp: -34, + pressure: 855, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '490', + terrestrial_date: '2014-02-17', + sol: 545, + ls: 90, + season: 'Month 4', + min_temp: -85, + max_temp: -29, + pressure: 856, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '487', + terrestrial_date: '2014-02-16', + sol: 544, + ls: 90, + season: 'Month 4', + min_temp: -86, + max_temp: -27, + pressure: 857, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '486', + terrestrial_date: '2014-02-15', + sol: 543, + ls: 89, + season: 'Month 3', + min_temp: -84, + max_temp: -26, + pressure: 858, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '477', + terrestrial_date: '2014-02-13', + sol: 542, + ls: 89, + season: 'Month 3', + min_temp: -85, + max_temp: -28, + pressure: 859, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '482', + terrestrial_date: '2014-02-12', + sol: 541, + ls: 88, + season: 'Month 3', + min_temp: -84, + max_temp: -27, + pressure: 861, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '481', + terrestrial_date: '2014-02-11', + sol: 540, + ls: 88, + season: 'Month 3', + min_temp: -84, + max_temp: -29, + pressure: 862, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '476', + terrestrial_date: '2014-02-10', + sol: 539, + ls: 87, + season: 'Month 3', + min_temp: -85, + max_temp: -23, + pressure: 864, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '472', + terrestrial_date: '2014-02-09', + sol: 538, + ls: 87, + season: 'Month 3', + min_temp: -85, + max_temp: -25, + pressure: 865, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '469', + terrestrial_date: '2014-02-08', + sol: 537, + ls: 86, + season: 'Month 3', + min_temp: -83, + max_temp: -28, + pressure: 865, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '470', + terrestrial_date: '2014-02-07', + sol: 536, + ls: 86, + season: 'Month 3', + min_temp: -83, + max_temp: -29, + pressure: 867, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '468', + terrestrial_date: '2014-02-06', + sol: 535, + ls: 86, + season: 'Month 3', + min_temp: -88, + max_temp: -29, + pressure: 868, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '467', + terrestrial_date: '2014-02-05', + sol: 534, + ls: 85, + season: 'Month 3', + min_temp: -86, + max_temp: -29, + pressure: 869, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '466', + terrestrial_date: '2014-02-04', + sol: 533, + ls: 85, + season: 'Month 3', + min_temp: -87, + max_temp: -30, + pressure: 871, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '464', + terrestrial_date: '2014-02-03', + sol: 532, + ls: 84, + season: 'Month 3', + min_temp: -88, + max_temp: -23, + pressure: 872, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '465', + terrestrial_date: '2014-02-02', + sol: 531, + ls: 84, + season: 'Month 3', + min_temp: -87, + max_temp: -22, + pressure: 872, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '463', + terrestrial_date: '2014-02-01', + sol: 530, + ls: 83, + season: 'Month 3', + min_temp: -87, + max_temp: -28, + pressure: 873, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '462', + terrestrial_date: '2014-01-31', + sol: 529, + ls: 83, + season: 'Month 3', + min_temp: -87, + max_temp: -23, + pressure: 875, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '461', + terrestrial_date: '2014-01-30', + sol: 528, + ls: 82, + season: 'Month 3', + min_temp: -86, + max_temp: -26, + pressure: 876, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '460', + terrestrial_date: '2014-01-29', + sol: 527, + ls: 82, + season: 'Month 3', + min_temp: -86, + max_temp: -23, + pressure: 877, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '459', + terrestrial_date: '2014-01-28', + sol: 526, + ls: 82, + season: 'Month 3', + min_temp: -87, + max_temp: -24, + pressure: 878, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '456', + terrestrial_date: '2014-01-27', + sol: 525, + ls: 81, + season: 'Month 3', + min_temp: -87, + max_temp: -29, + pressure: 878, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '457', + terrestrial_date: '2014-01-26', + sol: 524, + ls: 81, + season: 'Month 3', + min_temp: -85, + max_temp: -27, + pressure: 880, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '458', + terrestrial_date: '2014-01-25', + sol: 523, + ls: 80, + season: 'Month 3', + min_temp: -85, + max_temp: -25, + pressure: 881, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '455', + terrestrial_date: '2014-01-24', + sol: 522, + ls: 80, + season: 'Month 3', + min_temp: -85, + max_temp: -26, + pressure: 881, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '454', + terrestrial_date: '2014-01-23', + sol: 521, + ls: 79, + season: 'Month 3', + min_temp: -87, + max_temp: -26, + pressure: 882, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '453', + terrestrial_date: '2014-01-22', + sol: 520, + ls: 79, + season: 'Month 3', + min_temp: -86, + max_temp: -24, + pressure: 884, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '452', + terrestrial_date: '2014-01-21', + sol: 519, + ls: 78, + season: 'Month 3', + min_temp: -86, + max_temp: -25, + pressure: 884, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '451', + terrestrial_date: '2014-01-20', + sol: 518, + ls: 78, + season: 'Month 3', + min_temp: -85, + max_temp: -29, + pressure: 885, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '448', + terrestrial_date: '2014-01-19', + sol: 517, + ls: 77, + season: 'Month 3', + min_temp: -86, + max_temp: -27, + pressure: 885, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '449', + terrestrial_date: '2014-01-18', + sol: 516, + ls: 77, + season: 'Month 3', + min_temp: -86, + max_temp: -25, + pressure: 888, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '450', + terrestrial_date: '2014-01-17', + sol: 515, + ls: 77, + season: 'Month 3', + min_temp: -86, + max_temp: -23, + pressure: 888, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '447', + terrestrial_date: '2014-01-16', + sol: 514, + ls: 76, + season: 'Month 3', + min_temp: -86, + max_temp: -29, + pressure: 888, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '445', + terrestrial_date: '2014-01-15', + sol: 513, + ls: 76, + season: 'Month 3', + min_temp: -86, + max_temp: -29, + pressure: 889, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '444', + terrestrial_date: '2014-01-14', + sol: 512, + ls: 75, + season: 'Month 3', + min_temp: -86, + max_temp: -24, + pressure: 890, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '446', + terrestrial_date: '2014-01-13', + sol: 511, + ls: 75, + season: 'Month 3', + min_temp: -85, + max_temp: -31, + pressure: 892, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '441', + terrestrial_date: '2014-01-12', + sol: 510, + ls: 74, + season: 'Month 3', + min_temp: -85, + max_temp: -31, + pressure: 892, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '443', + terrestrial_date: '2014-01-11', + sol: 509, + ls: 74, + season: 'Month 3', + min_temp: -86, + max_temp: -30, + pressure: 895, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '442', + terrestrial_date: '2014-01-10', + sol: 508, + ls: 73, + season: 'Month 3', + min_temp: -83, + max_temp: -29, + pressure: 893, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '440', + terrestrial_date: '2014-01-09', + sol: 507, + ls: 73, + season: 'Month 3', + min_temp: -85, + max_temp: -25, + pressure: 894, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '439', + terrestrial_date: '2014-01-08', + sol: 506, + ls: 73, + season: 'Month 3', + min_temp: -86, + max_temp: -27, + pressure: 894, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '438', + terrestrial_date: '2014-01-06', + sol: 505, + ls: 72, + season: 'Month 3', + min_temp: -85, + max_temp: -29, + pressure: 895, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '436', + terrestrial_date: '2014-01-05', + sol: 504, + ls: 72, + season: 'Month 3', + min_temp: -85, + max_temp: -29, + pressure: 895, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '483', + terrestrial_date: '2014-01-04', + sol: 503, + ls: 71, + season: 'Month 3', + min_temp: -86, + max_temp: -28, + pressure: 897, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '437', + terrestrial_date: '2014-01-03', + sol: 502, + ls: 71, + season: 'Month 3', + min_temp: -87, + max_temp: -30, + pressure: 898, + wind_speed: null, + atmo_opacity: 'Sunny', + }, + { + id: '435', + terrestrial_date: '2014-01-02', + sol: 501, + ls: 70, + season: 'Month 3', + min_temp: -86, + max_temp: -28, + pressure: 898, + wind_speed: null, + atmo_opacity: 'Sunny', + }, +]; diff --git a/src/plugins/d3/examples/scatter/TwoYAxis.tsx b/src/plugins/d3/examples/scatter/TwoYAxis.tsx new file mode 100644 index 00000000..248aac5c --- /dev/null +++ b/src/plugins/d3/examples/scatter/TwoYAxis.tsx @@ -0,0 +1,68 @@ +import React from 'react'; + +import {dateTime} from '@gravity-ui/date-utils'; + +import {ChartKit} from '../../../../components/ChartKit'; +import type {ChartKitWidgetData} from '../../../../types'; +import {ExampleWrapper} from '../ExampleWrapper'; +import marsWeatherData from '../mars-weather'; + +export const TwoYAxis = () => { + const data = marsWeatherData as any[]; + const minTempData = data.map((d) => ({ + x: dateTime({input: d.terrestrial_date, format: 'YYYY-MM-DD'}).valueOf(), + y: d.min_temp, + })); + + const maxTempData = data.map((d) => ({ + x: dateTime({input: d.terrestrial_date, format: 'YYYY-MM-DD'}).valueOf(), + y: d.max_temp, + })); + + const widgetData: ChartKitWidgetData = { + series: { + data: [ + { + type: 'scatter', + data: minTempData, + name: 'Min Temperature', + yAxis: 0, + }, + { + type: 'scatter', + data: maxTempData, + name: 'Max Temperature', + yAxis: 1, + }, + ], + }, + yAxis: [ + { + title: { + text: 'Min', + }, + }, + { + title: { + text: 'Max', + }, + }, + ], + xAxis: { + type: 'datetime', + title: { + text: 'Terrestrial date', + }, + ticks: {pixelInterval: 200}, + }, + title: { + text: 'Mars weather', + }, + }; + + return ( + + + + ); +}; diff --git a/src/plugins/d3/renderer/components/AxisY.tsx b/src/plugins/d3/renderer/components/AxisY.tsx index b7c34b75..f872d1ed 100644 --- a/src/plugins/d3/renderer/components/AxisY.tsx +++ b/src/plugins/d3/renderer/components/AxisY.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import {axisLeft, select} from 'd3'; -import type {AxisDomain, AxisScale} from 'd3'; +import {axisLeft, axisRight, line, select} from 'd3'; +import type {Axis, AxisDomain, AxisScale, Selection} from 'd3'; import {block} from '../../../../utils/cn'; import type {ChartScale, PreparedAxis} from '../hooks'; @@ -21,14 +21,20 @@ const b = block('d3-axis'); type Props = { axises: PreparedAxis[]; + scale: ChartScale[]; width: number; height: number; - scale: ChartScale; }; -function transformLabel(node: Element, axis: PreparedAxis) { +function transformLabel(args: {node: Element; axis: PreparedAxis}) { + const {node, axis} = args; let topOffset = axis.labels.lineHeight / 2; - let leftOffset = -axis.labels.margin; + let leftOffset = axis.labels.margin; + + if (axis.position === 'left') { + leftOffset = leftOffset * -1; + } + if (axis.labels.rotation) { if (axis.labels.rotation > 0) { leftOffset -= axis.labels.lineHeight * calculateSin(axis.labels.rotation); @@ -51,107 +57,155 @@ function transformLabel(node: Element, axis: PreparedAxis) { return `translate(${leftOffset}px, ${topOffset}px)`; } +function getAxisGenerator(args: { + preparedAxis: PreparedAxis; + axisGenerator: Axis; + width: number; + height: number; + scale: ChartScale; +}) { + const {preparedAxis, axisGenerator: generator, width, height, scale} = args; + const tickSize = preparedAxis.grid.enabled ? width * -1 : 0; + const step = getClosestPointsRange(preparedAxis, getScaleTicks(scale as AxisScale)); + + let axisGenerator = generator + .tickSize(tickSize) + .tickPadding(preparedAxis.labels.margin) + .tickFormat((value) => { + if (!preparedAxis.labels.enabled) { + return ''; + } + + return formatAxisTickLabel({ + axis: preparedAxis, + value, + step, + }); + }); + + const ticksCount = getTicksCount({axis: preparedAxis, range: height}); + if (ticksCount) { + axisGenerator = axisGenerator.ticks(ticksCount); + } + + return axisGenerator; +} + export const AxisY = ({axises, width, height, scale}: Props) => { - const ref = React.useRef(null); + const ref = React.useRef(null); React.useEffect(() => { if (!ref.current) { return; } - const axis = axises[0]; const svgElement = select(ref.current); svgElement.selectAll('*').remove(); - const tickSize = axis.grid.enabled ? width * -1 : 0; - const step = getClosestPointsRange(axis, getScaleTicks(scale as AxisScale)); - - let yAxisGenerator = axisLeft(scale as AxisScale) - .tickSize(tickSize) - .tickPadding(axis.labels.margin) - .tickFormat((value) => { - if (!axis.labels.enabled) { - return ''; - } - - return formatAxisTickLabel({ - axis, - value, - step, - }); - }); - - const ticksCount = getTicksCount({axis, range: height}); - if (ticksCount) { - yAxisGenerator = yAxisGenerator.ticks(ticksCount); - } - - svgElement.call(yAxisGenerator).attr('class', b()); - svgElement - .select('.domain') - .attr('d', `M0,${height}H0V0`) - .style('stroke', axis.lineColor || ''); - - if (axis.labels.enabled) { - const tickTexts = svgElement - .selectAll('.tick text') - // The offset must be applied before the labels are rotated. - // Therefore, we reset the values and make an offset in transform attribute. - // FIXME: give up axisLeft(d3) and switch to our own generation method - .attr('x', null) - .attr('dy', null) - .style('font-size', axis.labels.style.fontSize) - .style('transform', function () { - return transformLabel(this, axis); - }); - const textMaxWidth = - !axis.labels.rotation || Math.abs(axis.labels.rotation) % 360 !== 90 - ? axis.labels.maxWidth - : (height - axis.labels.padding * (tickTexts.size() - 1)) / tickTexts.size(); - tickTexts.call(setEllipsisForOverflowTexts, textMaxWidth); - } - const transformStyle = svgElement.select('.tick').attr('transform'); - const {y} = parseTransformStyle(transformStyle); + const axisSelection = svgElement + .selectAll('axis') + .data(axises) + .join('g') + .attr('class', b()) + .style('transform', (_d, index) => (index === 0 ? '' : `translate(${width}px, 0)`)); + + axisSelection.each((d, index, node) => { + const seriesScale = scale[index]; + const axisItem = select(node[index]) as Selection< + SVGGElement, + PreparedAxis, + any, + unknown + >; + const yAxisGenerator = getAxisGenerator({ + axisGenerator: + index === 0 + ? axisLeft(seriesScale as AxisScale) + : axisRight(seriesScale as AxisScale), + preparedAxis: d, + height, + width, + scale: seriesScale, + }); + yAxisGenerator(axisItem); + + if (d.labels.enabled) { + const tickTexts = axisItem + .selectAll('.tick text') + // The offset must be applied before the labels are rotated. + // Therefore, we reset the values and make an offset in transform attribute. + // FIXME: give up axisLeft(d3) and switch to our own generation method + .attr('x', null) + .attr('dy', null) + .style('font-size', d.labels.style.fontSize) + .style('transform', function () { + return transformLabel({node: this, axis: d}); + }); + const textMaxWidth = + !d.labels.rotation || Math.abs(d.labels.rotation) % 360 !== 90 + ? d.labels.maxWidth + : (height - d.labels.padding * (tickTexts.size() - 1)) / tickTexts.size(); + tickTexts.call(setEllipsisForOverflowTexts, textMaxWidth); + } - if (y === height) { - // Remove stroke from tick that has the same y coordinate like domain - svgElement.select('.tick line').style('stroke', 'none'); - } + // remove overlapping ticks + // Note: this method do not prepared for rotated labels + if (!d.labels.rotation) { + let elementY = 0; + axisItem + .selectAll('.tick') + .filter(function (_d, tickIndex) { + const tickNode = this as unknown as Element; + const r = tickNode.getBoundingClientRect(); + + if (r.bottom > elementY && tickIndex !== 0) { + return true; + } + elementY = r.top - d.labels.padding; + return false; + }) + .remove(); + } - // remove overlapping ticks - // Note: this method do not prepared for rotated labels - if (!axis.labels.rotation) { - let elementY = 0; - svgElement - .selectAll('.tick') - .filter(function (_d, index) { - const node = this as unknown as Element; - const r = node.getBoundingClientRect(); - - if (r.bottom > elementY && index !== 0) { - return true; - } - elementY = r.top - axis.labels.padding; - return false; - }) - .remove(); - } + return axisItem; + }); - if (axis.title.text) { - const textY = axis.title.margin + axis.labels.margin + axis.labels.width; - - svgElement - .append('text') - .attr('class', b('title')) - .attr('text-anchor', 'middle') - .attr('dy', -textY) - .attr('dx', -height / 2) - .attr('font-size', axis.title.style.fontSize) - .attr('transform', 'rotate(-90)') - .text(axis.title.text) - .call(setEllipsisForOverflowText, height); - } + axisSelection + .select('.domain') + .attr('d', () => { + const points: [number, number][] = [ + [0, 0], + [0, height], + ]; + + return line()(points); + }) + .style('stroke', (d) => d.lineColor || ''); + + svgElement.selectAll('.tick').each((_d, index, nodes) => { + const tickNode = select(nodes[index]); + if (parseTransformStyle(tickNode.attr('transform')).y === height) { + // Remove stroke from tick that has the same y coordinate like domain + tickNode.select('line').style('stroke', 'none'); + } + }); + + axisSelection + .append('text') + .attr('class', b('title')) + .attr('text-anchor', 'middle') + .attr('dy', (d) => -(d.title.margin + d.labels.margin + d.labels.width)) + .attr('dx', (_d, index) => (index === 0 ? -height / 2 : height / 2)) + .attr('font-size', (d) => d.title.style.fontSize) + .attr('transform', (_d, index) => (index === 0 ? 'rotate(-90)' : 'rotate(90)')) + .text((d) => d.title.text) + .each((_d, index, node) => { + return setEllipsisForOverflowText( + select(node[index]) as Selection, + height, + ); + }); }, [axises, width, height, scale]); - return ; + return ; }; diff --git a/src/plugins/d3/renderer/components/Chart.tsx b/src/plugins/d3/renderer/components/Chart.tsx index 2f434d09..19d83102 100644 --- a/src/plugins/d3/renderer/components/Chart.tsx +++ b/src/plugins/d3/renderer/components/Chart.tsx @@ -7,7 +7,7 @@ import type {ChartKitWidgetData} from '../../../../types'; import {block} from '../../../../utils/cn'; import {getD3Dispatcher} from '../d3-dispatcher'; import {useAxisScales, useChartDimensions, useChartOptions, useSeries, useShapes} from '../hooks'; -import {getWidthOccupiedByYAxis} from '../hooks/useChartDimensions/utils'; +import {getYAxisWidth} from '../hooks/useChartDimensions/utils'; import {getPreparedXAxis} from '../hooks/useChartOptions/x-axis'; import {getPreparedYAxis} from '../hooks/useChartOptions/y-axis'; import {getClosestPoints} from '../utils/get-closest-data'; @@ -103,7 +103,8 @@ export const Chart = (props: Props) => { }, [dispatcher, clickHandler]); const boundsOffsetTop = chart.margin.top; - const boundsOffsetLeft = chart.margin.left + getWidthOccupiedByYAxis({preparedAxis: yAxis}); + // We only need to consider the width of the first left axis + const boundsOffsetLeft = chart.margin.left + getYAxisWidth(yAxis[0]); const handleMouseMove: MouseEventHandler = (event) => { const [pointerX, pointerY] = pointer(event, svgRef.current); @@ -143,7 +144,7 @@ export const Chart = (props: Props) => { height={boundsHeight} transform={`translate(${[boundsOffsetLeft, boundsOffsetTop].join(',')})`} > - {xScale && yScale && ( + {xScale && yScale?.length && ( { @@ -202,7 +202,18 @@ const createScales = (args: Args) => { return { xScale: createXScale(xAxis, visibleSeries, boundsWidth), - yScale: createYScale(yAxis[0], visibleSeries, boundsHeight), + yScale: yAxis.map((axis, index) => { + const axisSeries = series.filter((s) => { + const seriesAxisIndex = get(s, 'yAxis', 0); + return seriesAxisIndex === index; + }); + const visibleAxisSeries = getOnlyVisibleSeries(axisSeries); + return createYScale( + axis, + visibleAxisSeries.length ? visibleAxisSeries : axisSeries, + boundsHeight, + ); + }), }; }; @@ -213,7 +224,7 @@ export const useAxisScales = (args: Args): ReturnValue => { const {boundsWidth, boundsHeight, series, xAxis, yAxis} = args; const scales = React.useMemo(() => { let xScale: ChartScale | undefined; - let yScale: ChartScale | undefined; + let yScale: ChartScale[] | undefined; const hasAxisRelatedSeries = series.some(isAxisRelatedSeries); if (hasAxisRelatedSeries) { diff --git a/src/plugins/d3/renderer/hooks/useChartDimensions/utils.ts b/src/plugins/d3/renderer/hooks/useChartDimensions/utils.ts index 48211654..05bbe6c0 100644 --- a/src/plugins/d3/renderer/hooks/useChartDimensions/utils.ts +++ b/src/plugins/d3/renderer/hooks/useChartDimensions/utils.ts @@ -15,19 +15,20 @@ export const getBoundsWidth = (args: { ); }; -export function getWidthOccupiedByYAxis(args: {preparedAxis: PreparedAxis[]}) { - const {preparedAxis} = args; +export function getYAxisWidth(axis: PreparedAxis | undefined) { let result = 0; + if (axis?.title.text) { + result += axis.title.height + axis.title.margin; + } - preparedAxis.forEach((axis) => { - if (axis.title.text) { - result += axis.title.height + axis.title.margin; - } - - if (axis.labels.enabled) { - result += axis.labels.margin + axis.labels.width; - } - }); + if (axis?.labels.enabled) { + result += axis.labels.margin + axis.labels.width; + } return result; } + +export function getWidthOccupiedByYAxis(args: {preparedAxis: PreparedAxis[]}) { + const {preparedAxis = []} = args; + return preparedAxis.reduce((sum, axis) => sum + getYAxisWidth(axis), 0); +} diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/types.ts b/src/plugins/d3/renderer/hooks/useChartOptions/types.ts index 84357986..68065924 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/types.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/types.ts @@ -41,6 +41,7 @@ export type PreparedAxis = Omit & { ticks: { pixelInterval?: number; }; + position: 'left' | 'right' | 'top' | 'bottom'; }; export type PreparedTitle = ChartKitWidgetData['title'] & { diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/x-axis.ts b/src/plugins/d3/renderer/hooks/useChartOptions/x-axis.ts index 35abd156..02b8ddaf 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/x-axis.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/x-axis.ts @@ -120,6 +120,7 @@ export const getPreparedXAxis = ({ ticks: { pixelInterval: get(xAxis, 'ticks.pixelInterval'), }, + position: 'bottom', }; const {height, rotation} = getLabelSettings({ diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts b/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts index 36831b78..04608e13 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts @@ -84,57 +84,63 @@ export const getPreparedYAxis = ({ series: ChartKitWidgetSeries[]; yAxis: ChartKitWidgetData['yAxis']; }): PreparedAxis[] => { - // FIXME: add support for n axises - const yAxis1 = yAxis?.[0]; - const labelsEnabled = get(yAxis1, 'labels.enabled', true); + return (yAxis || [{}]).map((axisItem, index) => { + const axisPosition = index === 0 ? 'left' : 'right'; + const labelsEnabled = get(axisItem, 'labels.enabled', true); - const y1LabelsStyle: BaseTextStyle = { - fontSize: get(yAxis1, 'labels.style.fontSize', DEFAULT_AXIS_LABEL_FONT_SIZE), - }; - const y1TitleText = get(yAxis1, 'title.text', ''); - const y1TitleStyle: BaseTextStyle = { - fontSize: get(yAxis1, 'title.style.fontSize', yAxisTitleDefaults.fontSize), - }; - const axisType = get(yAxis1, 'type', 'linear'); - const preparedY1Axis: PreparedAxis = { - type: axisType, - labels: { - enabled: labelsEnabled, - margin: labelsEnabled ? get(yAxis1, 'labels.margin', axisLabelsDefaults.margin) : 0, - padding: labelsEnabled ? get(yAxis1, 'labels.padding', axisLabelsDefaults.padding) : 0, - dateFormat: get(yAxis1, 'labels.dateFormat'), - numberFormat: get(yAxis1, 'labels.numberFormat'), - style: y1LabelsStyle, - rotation: get(yAxis1, 'labels.rotation', 0), - width: 0, - height: 0, - lineHeight: getHorisontalSvgTextHeight({text: 'TmpLabel', style: y1LabelsStyle}), - maxWidth: get(yAxis1, 'labels.maxWidth', axisLabelsDefaults.maxWidth), - }, - lineColor: get(yAxis1, 'lineColor'), - categories: get(yAxis1, 'categories'), - timestamps: get(yAxis1, 'timestamps'), - title: { - text: y1TitleText, - margin: get(yAxis1, 'title.margin', yAxisTitleDefaults.margin), - style: y1TitleStyle, - height: y1TitleText - ? getHorisontalSvgTextHeight({text: y1TitleText, style: y1TitleStyle}) - : 0, - }, - min: getAxisMin(yAxis1, series), - maxPadding: get(yAxis1, 'maxPadding', 0.05), - grid: { - enabled: get(yAxis1, 'grid.enabled', true), - }, - ticks: { - pixelInterval: get(yAxis1, 'ticks.pixelInterval'), - }, - }; + const labelsStyle: BaseTextStyle = { + fontSize: get(axisItem, 'labels.style.fontSize', DEFAULT_AXIS_LABEL_FONT_SIZE), + }; + const titleText = get(axisItem, 'title.text', ''); + const titleStyle: BaseTextStyle = { + fontSize: get(axisItem, 'title.style.fontSize', yAxisTitleDefaults.fontSize), + }; + const axisType = get(axisItem, 'type', 'linear'); + const preparedAxis: PreparedAxis = { + type: axisType, + labels: { + enabled: labelsEnabled, + margin: labelsEnabled + ? get(axisItem, 'labels.margin', axisLabelsDefaults.margin) + : 0, + padding: labelsEnabled + ? get(axisItem, 'labels.padding', axisLabelsDefaults.padding) + : 0, + dateFormat: get(axisItem, 'labels.dateFormat'), + numberFormat: get(axisItem, 'labels.numberFormat'), + style: labelsStyle, + rotation: get(axisItem, 'labels.rotation', 0), + width: 0, + height: 0, + lineHeight: getHorisontalSvgTextHeight({text: 'TmpLabel', style: labelsStyle}), + maxWidth: get(axisItem, 'labels.maxWidth', axisLabelsDefaults.maxWidth), + }, + lineColor: get(axisItem, 'lineColor'), + categories: get(axisItem, 'categories'), + timestamps: get(axisItem, 'timestamps'), + title: { + text: titleText, + margin: get(axisItem, 'title.margin', yAxisTitleDefaults.margin), + style: titleStyle, + height: titleText + ? getHorisontalSvgTextHeight({text: titleText, style: titleStyle}) + : 0, + }, + min: getAxisMin(axisItem, series), + maxPadding: get(axisItem, 'maxPadding', 0.05), + grid: { + enabled: get(axisItem, 'grid.enabled', index === 0), + }, + ticks: { + pixelInterval: get(axisItem, 'ticks.pixelInterval'), + }, + position: axisPosition, + }; - if (labelsEnabled) { - preparedY1Axis.labels.width = getAxisLabelMaxWidth({axis: preparedY1Axis, series}); - } + if (labelsEnabled) { + preparedAxis.labels.width = getAxisLabelMaxWidth({axis: preparedAxis, series}); + } - return [preparedY1Axis]; + return preparedAxis; + }); }; diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-area.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-area.ts index 9c00c84e..11a04efe 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-area.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-area.ts @@ -86,6 +86,7 @@ export function prepareArea(args: PrepareAreaSeriesArgs) { }, marker: prepareMarker(series, seriesOptions), cursor: get(series, 'cursor', null), + yAxis: get(series, 'yAxis', 0), }; return prepared; diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-bar-x.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-bar-x.ts index 0fa6ca9b..baa8669a 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-bar-x.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-bar-x.ts @@ -45,6 +45,7 @@ export function prepareBarXSeries(args: PrepareBarXSeriesArgs): PreparedSeries[] padding: get(series, 'dataLabels.padding', DEFAULT_DATALABELS_PADDING), }, cursor: get(series, 'cursor', null), + yAxis: get(series, 'yAxis', 0), }; }, []); } diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts index a2ee6216..c321f87f 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts @@ -7,7 +7,7 @@ import type {ChartKitWidgetData} from '../../../../../types'; import {legendDefaults} from '../../constants'; import {getHorisontalSvgTextHeight} from '../../utils'; import {getBoundsWidth} from '../useChartDimensions'; -import {getWidthOccupiedByYAxis} from '../useChartDimensions/utils'; +import {getYAxisWidth} from '../useChartDimensions/utils'; import type {PreparedAxis, PreparedChart} from '../useChartOptions/types'; import type {LegendConfig, LegendItem, PreparedLegend, PreparedSeries} from './types'; @@ -19,7 +19,9 @@ export const getPreparedLegend = (args: { series: ChartKitWidgetData['series']['data']; }): PreparedLegend => { const {legend, series} = args; - const enabled = typeof legend?.enabled === 'boolean' ? legend?.enabled : series.length > 1; + const enabled = Boolean( + typeof legend?.enabled === 'boolean' ? legend?.enabled : series.length > 1, + ); const defaultItemStyle = clone(legendDefaults.itemStyle); const itemStyle = get(legend, 'itemStyle'); const computedItemStyle = merge(defaultItemStyle, itemStyle); @@ -128,7 +130,7 @@ export const getLegendComponents = (args: { preparedLegend.height = legendHeight; const top = chartHeight - chartMargin.bottom - preparedLegend.height; const offset: LegendConfig['offset'] = { - left: chartMargin.left + getWidthOccupiedByYAxis({preparedAxis: preparedYAxis}), + left: chartMargin.left + getYAxisWidth(preparedYAxis[0]), top, }; diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-line.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-line.ts index eb65a5da..0997854b 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-line.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-line.ts @@ -121,6 +121,7 @@ export function prepareLineSeries(args: PrepareLineSeriesArgs) { linecap: prepareLinecap(dashStyle as DashStyle, series, seriesOptions) as LineCap, opacity: get(series, 'opacity', null), cursor: get(series, 'cursor', null), + yAxis: get(series, 'yAxis', 0), }; return prepared; diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-scatter.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-scatter.ts index 126abb5f..47a692cf 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-scatter.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-scatter.ts @@ -67,6 +67,7 @@ export function prepareScatterSeries(args: PrepareScatterSeriesArgs): PreparedSc data: s.data, marker: prepareMarker(s, seriesOptions, index), cursor: get(s, 'cursor', null), + yAxis: get(s, 'yAxis', 0), }; return prepared; diff --git a/src/plugins/d3/renderer/hooks/useSeries/types.ts b/src/plugins/d3/renderer/hooks/useSeries/types.ts index 2581c5dd..1022bb22 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/types.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/types.ts @@ -108,6 +108,7 @@ export type PreparedScatterSeries = { }; }; }; + yAxis: number; } & BasePreparedSeries; export type PreparedBarXSeries = { @@ -122,6 +123,7 @@ export type PreparedBarXSeries = { allowOverlap: boolean; padding: number; }; + yAxis: number; } & BasePreparedSeries; export type PreparedBarYSeries = { @@ -200,6 +202,7 @@ export type PreparedLineSeries = { dashStyle: DashStyle; linecap: LineCap; opacity: number | null; + yAxis: number; } & BasePreparedSeries; export type PreparedAreaSeries = { @@ -233,6 +236,7 @@ export type PreparedAreaSeries = { }; }; }; + yAxis: number; } & BasePreparedSeries; export type PreparedTreemapSeries = { diff --git a/src/plugins/d3/renderer/hooks/useShapes/area/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/area/prepare-data.ts index b12c21da..c76cc2f6 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/area/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/area/prepare-data.ts @@ -1,4 +1,3 @@ -import type {ScaleLinear} from 'd3'; import {group, sort} from 'd3'; import type {AreaSeriesData} from '../../../../../../types'; @@ -70,15 +69,12 @@ export const prepareAreaData = (args: { xAxis: PreparedAxis; xScale: ChartScale; yAxis: PreparedAxis[]; - yScale: ChartScale; + yScale: ChartScale[]; + boundsHeight: number; }): PreparedAreaData[] => { - const {series, xAxis, xScale, yScale} = args; - const yLinearScale = yScale as ScaleLinear; - const plotHeight = yLinearScale(yLinearScale.domain()[0]); - const yAxis = args.yAxis[0]; + const {series, xAxis, xScale, yAxis, yScale, boundsHeight: plotHeight} = args; const [_xMin, xRangeMax] = xScale.range(); const xMax = xRangeMax / (1 - xAxis.maxPadding); - const [yMin, _yMax] = yScale.range(); return Array.from(group(series, (s) => s.stackId)).reduce( (result, [_stackId, seriesStack]) => { @@ -90,6 +86,10 @@ export const prepareAreaData = (args: { }); const seriesStackData = seriesStack.reduce((acc, s) => { + const yAxisIndex = s.yAxis; + const seriesYAxis = yAxis[yAxisIndex]; + const seriesYScale = yScale[yAxisIndex]; + const [yMin, _yMax] = seriesYScale.range(); const seriesData = s.data.reduce>((m, d) => { return m.set(String(d.x), d); }, new Map()); @@ -102,7 +102,9 @@ export const prepareAreaData = (args: { // FIXME: think about how to break the series into separate areas(null Y values) y: 0, } as AreaSeriesData); - const yValue = getYValue({point: d, yAxis, yScale}) - accumulatedYValue; + const yValue = + getYValue({point: d, yAxis: seriesYAxis, yScale: seriesYScale}) - + accumulatedYValue; accumulatedYValues.set(x, yMin - yValue); pointsAcc.push({ diff --git a/src/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.ts index de6869ce..c1d229bb 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.ts @@ -43,11 +43,10 @@ export const prepareBarXData = (args: { xAxis: PreparedAxis; xScale: ChartScale; yAxis: PreparedAxis[]; - yScale: ChartScale; + yScale: ChartScale[]; + boundsHeight: number; }): PreparedBarXData[] => { - const {series, seriesOptions, xAxis, xScale, yScale} = args; - const yLinearScale = yScale as ScaleLinear; - const plotHeight = yLinearScale(yLinearScale.domain()[0]); + const {series, seriesOptions, xAxis, xScale, yScale, boundsHeight: plotHeight} = args; const categories = get(xAxis, 'categories', [] as string[]); const barMaxWidth = get(seriesOptions, 'bar-x.barMaxWidth'); const barPadding = get(seriesOptions, 'bar-x.barPadding'); @@ -139,6 +138,8 @@ export const prepareBarXData = (args: { ? sort(yValues, (a, b) => comparator(get(a, sortKey), get(b, sortKey))) : yValues; sortedData.forEach((yValue) => { + const yAxisIndex = yValue.series.yAxis; + const seriesYScale = yScale[yAxisIndex] as ScaleLinear; let xCenter; if (xAxis.type === 'category') { @@ -152,7 +153,7 @@ export const prepareBarXData = (args: { } const x = xCenter - currentGroupWidth / 2 + (rectWidth + rectGap) * groupItemIndex; - const y = yLinearScale(yValue.data.y as number); + const y = seriesYScale(yValue.data.y as number); const height = plotHeight - y; const barData: PreparedBarXData = { diff --git a/src/plugins/d3/renderer/hooks/useShapes/bar-y/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/bar-y/prepare-data.ts index aa40f379..820ae8ab 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/bar-y/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/bar-y/prepare-data.ts @@ -74,9 +74,15 @@ export const prepareBarYData = (args: { xAxis: PreparedAxis; xScale: ChartScale; yAxis: PreparedAxis[]; - yScale: ChartScale; + yScale: ChartScale[]; }): PreparedBarYData[] => { - const {series, seriesOptions, yAxis, xScale, yScale} = args; + const { + series, + seriesOptions, + yAxis, + xScale, + yScale: [yScale], + } = args; const xLinearScale = xScale as ScaleLinear; const plotWidth = xLinearScale(xLinearScale.domain()[1]); const barMaxWidth = get(seriesOptions, 'bar-y.barMaxWidth'); diff --git a/src/plugins/d3/renderer/hooks/useShapes/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/index.tsx index 0ceb4e02..80a5c71f 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/index.tsx @@ -59,7 +59,7 @@ type Args = { xAxis: PreparedAxis; yAxis: PreparedAxis[]; xScale?: ChartScale; - yScale?: ChartScale; + yScale?: ChartScale[]; }; export const useShapes = (args: Args) => { @@ -91,6 +91,7 @@ export const useShapes = (args: Args) => { xScale, yAxis, yScale, + boundsHeight, }); acc.push( { xScale, yAxis, yScale, + boundsHeight, }); acc.push( { series: chartSeries as PreparedScatterSeries[], xAxis, xScale, - yAxis: yAxis[0], + yAxis, yScale, }); acc.push( diff --git a/src/plugins/d3/renderer/hooks/useShapes/line/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/line/prepare-data.ts index 35edba0f..2fb70f6f 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/line/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/line/prepare-data.ts @@ -41,7 +41,7 @@ export const prepareLineData = (args: { xAxis: PreparedAxis; xScale: ChartScale; yAxis: PreparedAxis[]; - yScale: ChartScale; + yScale: ChartScale[]; }): PreparedLineData[] => { const {series, xAxis, xScale, yScale} = args; const yAxis = args.yAxis[0]; @@ -49,9 +49,10 @@ export const prepareLineData = (args: { const xMax = xRangeMax / (1 - xAxis.maxPadding); return series.reduce((acc, s) => { + const seriesYScale = yScale[s.yAxis]; const points = s.data.map((d) => ({ x: getXValue({point: d, xAxis, xScale}), - y: getYValue({point: d, yAxis, yScale}), + y: getYValue({point: d, yAxis, yScale: seriesYScale}), active: true, data: d, series: s, diff --git a/src/plugins/d3/renderer/hooks/useShapes/scatter/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/scatter/prepare-data.ts index 8d7b5a55..df76c256 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/scatter/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/scatter/prepare-data.ts @@ -16,14 +16,17 @@ export const prepareScatterData = (args: { series: PreparedScatterSeries[]; xAxis: PreparedAxis; xScale: ChartScale; - yAxis: PreparedAxis; - yScale: ChartScale; + yAxis: PreparedAxis[]; + yScale: ChartScale[]; }): PreparedScatterData[] => { const {series, xAxis, xScale, yAxis, yScale} = args; return series.reduce((acc, s) => { + const yAxisIndex = get(s, 'yAxis', 0); + const seriesYAxis = yAxis[yAxisIndex]; + const seriesYScale = yScale[yAxisIndex]; const filteredData = - xAxis.type === 'category' || yAxis.type === 'category' + xAxis.type === 'category' || seriesYAxis.type === 'category' ? s.data : getFilteredLinearScatterData(s.data); @@ -33,7 +36,7 @@ export const prepareScatterData = (args: { data: d, series: s, x: getXValue({point: d, xAxis, xScale}), - y: getYValue({point: d, yAxis, yScale}), + y: getYValue({point: d, yAxis: seriesYAxis, yScale: seriesYScale}), opacity: get(d, 'opacity', null), }, hovered: false, diff --git a/src/plugins/d3/renderer/hooks/useShapes/waterfall/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/waterfall/prepare-data.ts index efb8bdaf..48f3cf1b 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/waterfall/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/waterfall/prepare-data.ts @@ -82,7 +82,7 @@ export const prepareWaterfallData = (args: { xAxis: PreparedAxis; xScale: ChartScale; yAxis: PreparedAxis[]; - yScale: ChartScale; + yScale: ChartScale[]; }): PreparedWaterfallData[] => { const { series, @@ -90,7 +90,7 @@ export const prepareWaterfallData = (args: { xAxis, xScale, yAxis: [yAxis], - yScale, + yScale: [yScale], } = args; const yLinearScale = yScale as ScaleLinear; const plotHeight = yLinearScale(yLinearScale.domain()[0]); diff --git a/src/plugins/d3/renderer/validation/__tests__/validation.test.ts b/src/plugins/d3/renderer/validation/__tests__/validation.test.ts index 9e357dcb..91caafb6 100644 --- a/src/plugins/d3/renderer/validation/__tests__/validation.test.ts +++ b/src/plugins/d3/renderer/validation/__tests__/validation.test.ts @@ -130,4 +130,17 @@ describe('plugins/d3/validation', () => { expect(error?.code).toEqual(CHARTKIT_ERROR_CODE.INVALID_DATA); }, ); + + test('validateData should throw an error in case of invalid axis index', () => { + const data = {series: {data: [{type: 'line', yAxis: 5, data: [{x: 1, y: 1}]}]}}; + let error: ChartKitError | null = null; + + try { + validateData(data as ChartKitWidgetData); + } catch (e) { + error = e as ChartKitError; + } + + expect(error?.code).toEqual(CHARTKIT_ERROR_CODE.INVALID_DATA); + }); }); diff --git a/src/plugins/d3/renderer/validation/index.ts b/src/plugins/d3/renderer/validation/index.ts index 6c744c4c..79e7a45d 100644 --- a/src/plugins/d3/renderer/validation/index.ts +++ b/src/plugins/d3/renderer/validation/index.ts @@ -25,11 +25,23 @@ const AVAILABLE_SERIES_TYPES = Object.values(SeriesType); const validateXYSeries = (args: { series: XYSeries; xAxis?: ChartKitWidgetAxis; - yAxis?: ChartKitWidgetAxis; + yAxis?: ChartKitWidgetAxis[]; }) => { - const {series, xAxis, yAxis} = args; + const {series, xAxis, yAxis = []} = args; + + const yAxisIndex = get(series, 'yAxis', 0); + const seriesYAxis = yAxis[yAxisIndex]; + if (yAxisIndex !== 0 && typeof seriesYAxis === 'undefined') { + throw new ChartKitError({ + code: CHARTKIT_ERROR_CODE.INVALID_DATA, + message: i18n('error', 'label_invalid-y-axis-index', { + index: yAxisIndex, + }), + }); + } + const xType = get(xAxis, 'type', DEFAULT_AXIS_TYPE); - const yType = get(yAxis, 'type', DEFAULT_AXIS_TYPE); + const yType = get(seriesYAxis, 'type', DEFAULT_AXIS_TYPE); series.data.forEach(({x, y}) => { switch (xType) { case 'category': { @@ -172,7 +184,7 @@ const validateTreemapSeries = ({series}: {series: TreemapSeries}) => { const validateSeries = (args: { series: ChartKitWidgetSeries; xAxis?: ChartKitWidgetAxis; - yAxis?: ChartKitWidgetAxis; + yAxis?: ChartKitWidgetAxis[]; }) => { const {series, xAxis, yAxis} = args; @@ -252,6 +264,6 @@ export const validateData = (data?: ChartKitWidgetData) => { } data.series.data.forEach((series) => { - validateSeries({series, yAxis: data.yAxis?.[0], xAxis: data.xAxis}); + validateSeries({series, yAxis: data.yAxis, xAxis: data.xAxis}); }); }; diff --git a/src/types/widget-data/area.ts b/src/types/widget-data/area.ts index 04b8054b..9ac4c2df 100644 --- a/src/types/widget-data/area.ts +++ b/src/types/widget-data/area.ts @@ -75,4 +75,6 @@ export type AreaSeries = BaseSeries & { }; /** Options for the point markers of line in area series */ marker?: AreaMarkerOptions; + /** Y-axis index (when using two axes) */ + yAxis?: number; }; diff --git a/src/types/widget-data/bar-x.ts b/src/types/widget-data/bar-x.ts index 51ea3c6b..fd859e7f 100644 --- a/src/types/widget-data/bar-x.ts +++ b/src/types/widget-data/bar-x.ts @@ -64,4 +64,6 @@ export type BarXSeries = BaseSeries & { legend?: ChartKitWidgetLegend & { symbol?: RectLegendSymbolOptions; }; + /** Y-axis index (when using two axes) */ + yAxis?: number; }; diff --git a/src/types/widget-data/line.ts b/src/types/widget-data/line.ts index 6c3f0d01..d0324656 100644 --- a/src/types/widget-data/line.ts +++ b/src/types/widget-data/line.ts @@ -54,4 +54,6 @@ export type LineSeries = BaseSeries & { linecap?: `${LineCap}`; /** Individual opacity for the line. */ opacity?: number; + /** Y-axis index (when using two axes) */ + yAxis?: number; }; diff --git a/src/types/widget-data/scatter.ts b/src/types/widget-data/scatter.ts index 0386ee43..9708e803 100644 --- a/src/types/widget-data/scatter.ts +++ b/src/types/widget-data/scatter.ts @@ -39,10 +39,10 @@ export type ScatterSeries = BaseSeries & { color?: string; /** A predefined shape or symbol for the dot */ symbolType?: `${SymbolType}`; - // yAxisIndex?: number; - /** Individual series legend options. Has higher priority than legend options in widget data */ legend?: ChartKitWidgetLegend & { symbol?: RectLegendSymbolOptions; }; + /** Y-axis index (when using two axes) */ + yAxis?: number; };