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