diff --git a/client/app/components/sortable/index.jsx b/client/app/components/sortable/index.jsx index 7365e64fae..eecac56ac7 100644 --- a/client/app/components/sortable/index.jsx +++ b/client/app/components/sortable/index.jsx @@ -14,7 +14,7 @@ export const SortableContainerWrapper = sortableContainer(({ children }) => chil export const SortableElement = sortableElement(({ children }) => children); -export function SortableContainer({ disabled, containerProps, children, ...wrapperProps }) { +export function SortableContainer({ disabled, containerComponent, containerProps, children, ...wrapperProps }) { const containerRef = useRef(); const [isDragging, setIsDragging] = useState(false); @@ -59,22 +59,24 @@ export function SortableContainer({ disabled, containerProps, children, ...wrapp containerProps.ref = containerRef; } - // order of props matters - we override some of them + const ContainerComponent = containerComponent; return ( -
{children}
+ {children}
); } SortableContainer.propTypes = { disabled: PropTypes.bool, + containerComponent: PropTypes.elementType, containerProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types children: PropTypes.node, }; SortableContainer.defaultProps = { disabled: false, + containerComponent: 'div', containerProps: {}, children: null, }; diff --git a/client/app/visualizations/chart/Editor/AxisSettings.jsx b/client/app/visualizations/chart/Editor/AxisSettings.jsx new file mode 100644 index 0000000000..918b41a27a --- /dev/null +++ b/client/app/visualizations/chart/Editor/AxisSettings.jsx @@ -0,0 +1,106 @@ +import { isString, isObject, isFinite, isNumber, merge } from 'lodash'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { useDebouncedCallback } from 'use-debounce'; +import Select from 'antd/lib/select'; +import Input from 'antd/lib/input'; +import InputNumber from 'antd/lib/input-number'; +import * as Grid from 'antd/lib/grid'; + +function toNumber(value) { + value = isNumber(value) ? value : parseFloat(value); + return isFinite(value) ? value : null; +} + +export default function AxisSettings({ id, options, features, onChange }) { + function optionsChanged(newOptions) { + onChange(merge({}, options, newOptions)); + } + + const [handleNameChange] = useDebouncedCallback((text) => { + const title = isString(text) && (text !== '') ? { text } : null; + optionsChanged({ title }); + }, 200); + + const [handleMinMaxChange] = useDebouncedCallback(opts => optionsChanged(opts), 200); + + return ( + +
+ + +
+ +
+ + handleNameChange(event.target.value)} + /> +
+ + {features.range && ( + + + + handleMinMaxChange({ rangeMin: toNumber(value) })} + /> + + + + handleMinMaxChange({ rangeMax: toNumber(value) })} + /> + + + )} +
+ ); +} + +AxisSettings.propTypes = { + id: PropTypes.string.isRequired, + options: PropTypes.shape({ + type: PropTypes.string.isRequired, + title: PropTypes.shape({ + text: PropTypes.string, + }), + rangeMin: PropTypes.number, + rangeMax: PropTypes.number, + }).isRequired, + features: PropTypes.shape({ + autoDetectType: PropTypes.bool, + range: PropTypes.bool, + }), + onChange: PropTypes.func, +}; + +AxisSettings.defaultProps = { + features: {}, + onChange: () => {}, +}; diff --git a/client/app/visualizations/chart/Editor/ChartTypeSelect.jsx b/client/app/visualizations/chart/Editor/ChartTypeSelect.jsx new file mode 100644 index 0000000000..9931130d12 --- /dev/null +++ b/client/app/visualizations/chart/Editor/ChartTypeSelect.jsx @@ -0,0 +1,36 @@ +import { map } from 'lodash'; +import React, { useMemo } from 'react'; +import Select from 'antd/lib/select'; +import { clientConfig } from '@/services/auth'; + +export default function ChartTypeSelect(props) { + const chartTypes = useMemo(() => { + const result = [ + { type: 'line', name: 'Line', icon: 'line-chart' }, + { type: 'column', name: 'Bar', icon: 'bar-chart' }, + { type: 'area', name: 'Area', icon: 'area-chart' }, + { type: 'pie', name: 'Pie', icon: 'pie-chart' }, + { type: 'scatter', name: 'Scatter', icon: 'circle-o' }, + { type: 'bubble', name: 'Bubble', icon: 'circle-o' }, + { type: 'heatmap', name: 'Heatmap', icon: 'th' }, + { type: 'box', name: 'Box', icon: 'square-o' }, + ]; + + if (clientConfig.allowCustomJSVisualizations) { + result.push({ type: 'custom', name: 'Custom', icon: 'code' }); + } + + return result; + }, []); + + return ( + + ); +} diff --git a/client/app/visualizations/chart/Editor/ColorsSettings.jsx b/client/app/visualizations/chart/Editor/ColorsSettings.jsx new file mode 100644 index 0000000000..395ac8179c --- /dev/null +++ b/client/app/visualizations/chart/Editor/ColorsSettings.jsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { EditorPropTypes } from '@/visualizations'; + +import PieColorsSettings from './PieColorsSettings'; +import HeatmapColorsSettings from './HeatmapColorsSettings'; +import DefaultColorsSettings from './DefaultColorsSettings'; + +const components = { + pie: PieColorsSettings, + heatmap: HeatmapColorsSettings, +}; + +export default function ColorsSettings({ options, ...props }) { + const Component = components[options.globalSeriesType] || DefaultColorsSettings; + return ; +} + +ColorsSettings.propTypes = EditorPropTypes; diff --git a/client/app/visualizations/chart/Editor/ColorsSettings.test.js b/client/app/visualizations/chart/Editor/ColorsSettings.test.js new file mode 100644 index 0000000000..505f20d482 --- /dev/null +++ b/client/app/visualizations/chart/Editor/ColorsSettings.test.js @@ -0,0 +1,84 @@ +import { after } from 'lodash'; +import React from 'react'; +import enzyme from 'enzyme'; + +import getOptions from '../getOptions'; +import ColorsSettings from './ColorsSettings'; + +function findByTestID(wrapper, testId) { + return wrapper.find(`[data-test="${testId}"]`); +} + +function mount(options, done) { + options = getOptions(options); + return enzyme.mount(( + { + expect(changedOptions).toMatchSnapshot(); + done(); + }} + /> + )); +} + +describe('Visualizations -> Chart -> Editor -> Colors Settings', () => { + describe('for pie', () => { + test('Changes series color', (done) => { + const el = mount({ + globalSeriesType: 'pie', + columnMapping: { a: 'x', b: 'y' }, + }, done); + + findByTestID(el, 'Chart.Series.v.Color').first().simulate('click'); + findByTestID(el, 'ColorPicker').first().find('input') + .simulate('change', { target: { value: 'red' } }); + }); + }); + + describe('for heatmap', () => { + test('Changes color scheme', (done) => { + const el = mount({ + globalSeriesType: 'heatmap', + columnMapping: { a: 'x', b: 'y' }, + }, done); + + findByTestID(el, 'Chart.Colors.Heatmap.ColorScheme').first().simulate('click'); + findByTestID(el, 'Chart.Colors.Heatmap.ColorScheme.RdBu').first().simulate('click'); + }); + + test('Sets custom color scheme', async (done) => { + const el = mount({ + globalSeriesType: 'heatmap', + columnMapping: { a: 'x', b: 'y' }, + colorScheme: 'Custom...', + }, after(2, done)); // we will perform 2 actions, so call `done` after all of them completed + + findByTestID(el, 'Chart.Colors.Heatmap.MinColor').first().simulate('click'); + findByTestID(el, 'ColorPicker').first().find('input') + .simulate('change', { target: { value: 'yellow' } }); + + findByTestID(el, 'Chart.Colors.Heatmap.MaxColor').first().simulate('click'); + findByTestID(el, 'ColorPicker').first().find('input') + .simulate('change', { target: { value: 'red' } }); + }); + }); + + describe('for all except of pie and heatmap', () => { + test('Changes series color', (done) => { + const el = mount({ + globalSeriesType: 'column', + columnMapping: { a: 'x', b: 'y' }, + }, done); + + findByTestID(el, 'Chart.Series.b.Color').first().simulate('click'); + findByTestID(el, 'ColorPicker').first().find('input') + .simulate('change', { target: { value: 'red' } }); + }); + }); +}); diff --git a/client/app/visualizations/chart/Editor/ColumnMappingSelect.jsx b/client/app/visualizations/chart/Editor/ColumnMappingSelect.jsx new file mode 100644 index 0000000000..336c611b43 --- /dev/null +++ b/client/app/visualizations/chart/Editor/ColumnMappingSelect.jsx @@ -0,0 +1,60 @@ +import { isString, map, uniq, flatten, filter, sortBy, keys } from 'lodash'; +import React from 'react'; +import PropTypes from 'prop-types'; +import Select from 'antd/lib/select'; + +const MappingTypes = { + x: { label: 'X Column' }, + y: { label: 'Y Columns', multiple: true }, + series: { label: 'Group by' }, + yError: { label: 'Errors column' }, + size: { label: 'Bubble size column' }, + zVal: { label: 'Color Column' }, +}; + +export default function ColumnMappingSelect({ value, availableColumns, type, onChange }) { + const options = sortBy(filter( + uniq(flatten([availableColumns, value])), + v => isString(v) && (v !== ''), + )); + const { label, multiple } = MappingTypes[type]; + + return ( +
+ + +
+ ); +} + +ColumnMappingSelect.propTypes = { + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.arrayOf(PropTypes.string), + ]), + availableColumns: PropTypes.arrayOf(PropTypes.string), + type: PropTypes.oneOf(keys(MappingTypes)), + onChange: PropTypes.func, +}; + +ColumnMappingSelect.defaultProps = { + value: null, + availableColumns: [], + type: null, + onChange: () => {}, +}; + +ColumnMappingSelect.MappingTypes = MappingTypes; diff --git a/client/app/visualizations/chart/Editor/CustomChartSettings.jsx b/client/app/visualizations/chart/Editor/CustomChartSettings.jsx new file mode 100644 index 0000000000..9560f75589 --- /dev/null +++ b/client/app/visualizations/chart/Editor/CustomChartSettings.jsx @@ -0,0 +1,58 @@ +import { isNil, trimStart } from 'lodash'; +import React from 'react'; +import Switch from 'antd/lib/switch'; +import Input from 'antd/lib/input'; +import { EditorPropTypes } from '@/visualizations'; + +const { TextArea } = Input; + +const defaultCustomCode = trimStart(` +// Available variables are x, ys, element, and Plotly +// Type console.log(x, ys); for more info about x and ys +// To plot your graph call Plotly.plot(element, ...) +// Plotly examples and docs: https://plot.ly/javascript/ +`); + +export default function CustomChartSettings({ options, onOptionsChange }) { + return ( + +
+ + -
- -
- -
- -
- -
- - -
-
- - - {{$select.selected.label}} - -
-
-
-
- -
- - -
- -
- -
- -
- -
- -
- -
-
- -
-
-

{{$index == 0 ? 'Left' : 'Right'}} Y Axis

- -
- - - {{$select.selected | capitalize}} - -
-
-
-
-
- - -
-
- - -
-
- - -
- -
- -
- -
- -
- -
-
- -
- - - - - - - - - - - - - - - - - -
zIndexLeft Y AxisRight Y AxisLabelType
- - - - - - - - - - - -
- - {{$select.selected.value.name}} -
-
- -
- - - -
-
-
-
-
- -
- - - - - - - -
-
{{ name }}
-
- - - - - - - - - -
-
- -
-
- - - - {{$select.selected | capitalize}} - -
-
-
-
- -
-
-
- - - - - - - - - - -
-
-
-
- - - - - - - - - - -
-
-
-
- -
- - - - - - - -
-
{{ name }}
-
- - - - - - - - - -
-
- -
-
- -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
- diff --git a/client/app/visualizations/chart/getOptions.js b/client/app/visualizations/chart/getOptions.js new file mode 100644 index 0000000000..82a74c7ad7 --- /dev/null +++ b/client/app/visualizations/chart/getOptions.js @@ -0,0 +1,39 @@ +import { merge } from 'lodash'; +import { clientConfig } from '@/services/auth'; + +const DEFAULT_OPTIONS = { + globalSeriesType: 'column', + sortX: true, + legend: { enabled: true }, + yAxis: [{ type: 'linear' }, { type: 'linear', opposite: true }], + xAxis: { type: '-', labels: { enabled: true } }, + error_y: { type: 'data', visible: true }, + series: { stacking: null, error_y: { type: 'data', visible: true } }, + seriesOptions: {}, + valuesOptions: {}, + columnMapping: {}, + direction: { type: 'counterclockwise' }, + + // showDataLabels: false, // depends on chart type + numberFormat: '0,0[.]00000', + percentFormat: '0[.]00%', + // dateTimeFormat: 'DD/MM/YYYY HH:mm', // will be set from clientConfig + textFormat: '', // default: combination of {{ @@yPercent }} ({{ @@y }} ± {{ @@yError }}) + + missingValuesAsZero: true, +}; + +export default function getOptions(options) { + const result = merge({}, DEFAULT_OPTIONS, { + showDataLabels: options.globalSeriesType === 'pie', + dateTimeFormat: clientConfig.dateTimeFormat, + }, options); + + // Backward compatibility + if (['normal', 'percent'].indexOf(result.series.stacking) >= 0) { + result.series.percentValues = result.series.stacking === 'percent'; + result.series.stacking = 'stack'; + } + + return result; +} diff --git a/client/app/visualizations/chart/index.js b/client/app/visualizations/chart/index.js index d8a3e2fed5..b9ae2bfbe8 100644 --- a/client/app/visualizations/chart/index.js +++ b/client/app/visualizations/chart/index.js @@ -1,321 +1,22 @@ -import { - some, partial, intersection, without, includes, sortBy, each, map, keys, difference, merge, isNil, trim, pick, -} from 'lodash'; -import { angular2react } from 'angular2react'; import { registerVisualization } from '@/visualizations'; -import { clientConfig } from '@/services/auth'; -import ColorPalette from '@/visualizations/ColorPalette'; -import getChartData from './getChartData'; -import editorTemplate from './chart-editor.html'; +import getOptions from './getOptions'; import Renderer from './Renderer'; - -const DEFAULT_OPTIONS = { - globalSeriesType: 'column', - sortX: true, - legend: { enabled: true }, - yAxis: [{ type: 'linear' }, { type: 'linear', opposite: true }], - xAxis: { type: '-', labels: { enabled: true } }, - error_y: { type: 'data', visible: true }, - series: { stacking: null, error_y: { type: 'data', visible: true } }, - seriesOptions: {}, - valuesOptions: {}, - columnMapping: {}, - direction: { type: 'counterclockwise' }, - - // showDataLabels: false, // depends on chart type - numberFormat: '0,0[.]00000', - percentFormat: '0[.]00%', - // dateTimeFormat: 'DD/MM/YYYY HH:mm', // will be set from clientConfig - textFormat: '', // default: combination of {{ @@yPercent }} ({{ @@y }} ± {{ @@yError }}) - - missingValuesAsZero: true, -}; - -function initEditorForm(options, columns) { - const result = { - yAxisColumns: [], - seriesList: sortBy(keys(options.seriesOptions), name => options.seriesOptions[name].zIndex), - valuesList: keys(options.valuesOptions), - }; - - // Use only mappings for columns that exists in query results - const mappings = pick( - options.columnMapping, - map(columns, c => c.name), - ); - - each(mappings, (type, column) => { - switch (type) { - case 'x': - result.xAxisColumn = column; - break; - case 'y': - result.yAxisColumns.push(column); - break; - case 'series': - result.groupby = column; - break; - case 'yError': - result.errorColumn = column; - break; - case 'size': - result.sizeColumn = column; - break; - case 'zVal': - result.zValColumn = column; - break; - // no default - } - }); - - return result; -} - -const ChartEditor = { - template: editorTemplate, - bindings: { - data: '<', - options: '<', - onOptionsChange: '<', - }, - controller($scope) { - this.currentTab = 'general'; - this.setCurrentTab = (tab) => { - this.currentTab = tab; - }; - - this.colors = { - Automatic: null, - ...ColorPalette, - }; - - this.stackingOptions = { - Disabled: null, - Stack: 'stack', - }; - - this.chartTypes = { - line: { name: 'Line', icon: 'line-chart' }, - column: { name: 'Bar', icon: 'bar-chart' }, - area: { name: 'Area', icon: 'area-chart' }, - pie: { name: 'Pie', icon: 'pie-chart' }, - scatter: { name: 'Scatter', icon: 'circle-o' }, - bubble: { name: 'Bubble', icon: 'circle-o' }, - heatmap: { name: 'Heatmap', icon: 'th' }, - box: { name: 'Box', icon: 'square-o' }, - }; - - if (clientConfig.allowCustomJSVisualizations) { - this.chartTypes.custom = { name: 'Custom', icon: 'code' }; - } - - this.directions = [ - { label: 'Counterclockwise', value: 'counterclockwise' }, - { label: 'Clockwise', value: 'clockwise' }, - ]; - - this.xAxisScales = [ - { label: 'Auto Detect', value: '-' }, - { label: 'Datetime', value: 'datetime' }, - { label: 'Linear', value: 'linear' }, - { label: 'Logarithmic', value: 'logarithmic' }, - { label: 'Category', value: 'category' }, - ]; - this.yAxisScales = ['linear', 'logarithmic', 'datetime', 'category']; - - this.colorScheme = ['Blackbody', 'Bluered', 'Blues', 'Earth', 'Electric', - 'Greens', 'Greys', 'Hot', 'Jet', 'Picnic', 'Portland', - 'Rainbow', 'RdBu', 'Reds', 'Viridis', 'YlGnBu', 'YlOrRd', 'Custom...']; - - this.chartTypeChanged = () => { - keys(this.options.seriesOptions).forEach((key) => { - this.options.seriesOptions[key].type = this.options.globalSeriesType; - }); - this.options.showDataLabels = this.options.globalSeriesType === 'pie'; - $scope.$applyAsync(); - }; - - this.showSizeColumnPicker = () => some(this.options.seriesOptions, options => options.type === 'bubble'); - this.showZColumnPicker = () => some(this.options.seriesOptions, options => options.type === 'heatmap'); - - if (isNil(this.options.customCode)) { - this.options.customCode = trim(` -// Available variables are x, ys, element, and Plotly -// Type console.log(x, ys); for more info about x and ys -// To plot your graph call Plotly.plot(element, ...) -// Plotly examples and docs: https://plot.ly/javascript/ - `); - } - - this.form = initEditorForm(this.options, this.data.columns); - - const refreshColumns = () => { - this.columns = this.data.columns; - this.columnNames = map(this.columns, c => c.name); - if (this.columnNames.length > 0) { - each(difference(keys(this.options.columnMapping), this.columnNames), (column) => { - delete this.options.columnMapping[column]; - }); - } - }; - - const refreshColumnsAndForm = () => { - refreshColumns(); - const data = this.data; - if (data && (data.columns.length > 0) && (data.rows.length > 0)) { - this.form.yAxisColumns = intersection(this.form.yAxisColumns, this.columnNames); - if (!includes(this.columnNames, this.form.xAxisColumn)) { - this.form.xAxisColumn = undefined; - } - if (!includes(this.columnNames, this.form.groupby)) { - this.form.groupby = undefined; - } - } - }; - - const refreshSeries = () => { - const chartData = getChartData(this.data.rows, this.options); - const seriesNames = map(chartData, s => s.name); - const existing = keys(this.options.seriesOptions); - each(difference(seriesNames, existing), (name) => { - this.options.seriesOptions[name] = { - type: this.options.globalSeriesType, - yAxis: 0, - }; - this.form.seriesList.push(name); - }); - each(difference(existing, seriesNames), (name) => { - this.form.seriesList = without(this.form.seriesList, name); - delete this.options.seriesOptions[name]; - }); - - if (this.options.globalSeriesType === 'pie') { - const uniqueValuesNames = new Set(); - each(chartData, (series) => { - each(series.data, (row) => { - uniqueValuesNames.add(row.x); - }); - }); - const valuesNames = []; - uniqueValuesNames.forEach(v => valuesNames.push(v)); - - // initialize newly added values - const newValues = difference(valuesNames, keys(this.options.valuesOptions)); - each(newValues, (name) => { - this.options.valuesOptions[name] = {}; - this.form.valuesList.push(name); - }); - // remove settings for values that are no longer available - each(keys(this.options.valuesOptions), (name) => { - if (valuesNames.indexOf(name) === -1) { - delete this.options.valuesOptions[name]; - } - }); - this.form.valuesList = intersection(this.form.valuesList, valuesNames); - } - }; - - const setColumnRole = (role, column) => { - this.options.columnMapping[column] = role; - }; - - const unsetColumn = column => setColumnRole('unused', column); - - refreshColumns(); - - $scope.$watch('$ctrl.options.columnMapping', refreshSeries, true); - - $scope.$watch('$ctrl.data', () => { - refreshColumnsAndForm(); - refreshSeries(); - }); - - $scope.$watchCollection('$ctrl.form.seriesList', (value) => { - each(value, (name, index) => { - this.options.seriesOptions[name].zIndex = index; - this.options.seriesOptions[name].index = 0; // is this needed? - }); - }); - - $scope.$watchCollection('$ctrl.form.yAxisColumns', (value, old) => { - each(old, unsetColumn); - each(value, partial(setColumnRole, 'y')); - }); - - $scope.$watch('$ctrl.form.xAxisColumn', (value, old) => { - if (old !== undefined) { unsetColumn(old); } - if (value !== undefined) { setColumnRole('x', value); } - }); - - $scope.$watch('$ctrl.form.errorColumn', (value, old) => { - if (old !== undefined) { unsetColumn(old); } - if (value !== undefined) { setColumnRole('yError', value); } - }); - - $scope.$watch('$ctrl.form.sizeColumn', (value, old) => { - if (old !== undefined) { unsetColumn(old); } - if (value !== undefined) { setColumnRole('size', value); } - }); - - $scope.$watch('$ctrl.form.zValColumn', (value, old) => { - if (old !== undefined) { unsetColumn(old); } - if (value !== undefined) { setColumnRole('zVal', value); } - }); - - $scope.$watch('$ctrl.form.groupby', (value, old) => { - if (old !== undefined) { unsetColumn(old); } - if (value !== undefined) { setColumnRole('series', value); } - }); - - $scope.$watch('$ctrl.options', (options) => { - this.onOptionsChange(options); - }, true); - - this.templateHint = ` -
Use special names to access additional properties:
-
{{ @@name }} series name;
-
{{ @@x }} x-value;
-
{{ @@y }} y-value;
-
{{ @@yPercent }} relative y-value;
-
{{ @@yError }} y deviation;
-
{{ @@size }} bubble size;
-
Also, all query result columns can be referenced using - {{ column_name }} syntax.
- `; - }, -}; - -export default function init(ngModule) { - ngModule.component('chartEditor', ChartEditor); - - ngModule.run(($injector) => { - registerVisualization({ - type: 'CHART', - name: 'Chart', - isDefault: true, - getOptions: (options) => { - const result = merge({}, DEFAULT_OPTIONS, { - showDataLabels: options.globalSeriesType === 'pie', - dateTimeFormat: clientConfig.dateTimeFormat, - }, options); - - // Backward compatibility - if (['normal', 'percent'].indexOf(result.series.stacking) >= 0) { - result.series.percentValues = result.series.stacking === 'percent'; - result.series.stacking = 'stack'; - } - - return result; - }, - Renderer, - Editor: angular2react('chartEditor', ChartEditor, $injector), - - defaultColumns: 3, - defaultRows: 8, - minColumns: 1, - minRows: 5, - }); +import Editor from './Editor'; + +export default function init() { + registerVisualization({ + type: 'CHART', + name: 'Chart', + isDefault: true, + getOptions, + Renderer, + Editor, + + defaultColumns: 3, + defaultRows: 8, + minColumns: 1, + minRows: 5, }); } diff --git a/client/app/visualizations/chart/plotly/prepareDefaultData.js b/client/app/visualizations/chart/plotly/prepareDefaultData.js index aeedae3ba3..d42a935b66 100644 --- a/client/app/visualizations/chart/plotly/prepareDefaultData.js +++ b/client/app/visualizations/chart/plotly/prepareDefaultData.js @@ -1,4 +1,4 @@ -import { isNil, each, includes, isString, map, sortBy } from 'lodash'; +import { isNil, isString, extend, each, includes, map, sortBy } from 'lodash'; import { cleanNumber, normalizeValue, getSeriesAxis } from './utils'; import { ColorPaletteArray } from '@/visualizations/ColorPalette'; @@ -101,7 +101,10 @@ function prepareBoxSeries(series, options, { seriesColor }) { function prepareSeries(series, options, additionalOptions) { const { hoverInfoPattern, index } = additionalOptions; - const seriesOptions = options.seriesOptions[series.name] || { type: options.globalSeriesType }; + const seriesOptions = extend( + { type: options.globalSeriesType, yAxis: 0 }, + options.seriesOptions[series.name], + ); const seriesColor = getSeriesColor(seriesOptions, index); const seriesYAxis = getSeriesAxis(series, options);