Replace TSVB timeseries charts with elastic-charts (#33558)

* Replace TSVB timeseries charts with elastic-charts

* Add sort index for series

* Update src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/index.js

Co-Authored-By: Nick Partridge <nick.ryan.partridge@gmail.com>

* Fix PR comments

* fix issue with scaling

* fix crosshair styles for bar
This commit is contained in:
Daniil Suleiman 2019-09-06 12:45:30 +03:00 committed by Alexey Antonov
parent bf9c86fceb
commit ab24359879
87 changed files with 1939 additions and 1904 deletions

View file

@ -52,7 +52,7 @@ export const isBackgroundDark = (backgroundColor, currentTheme) => {
const themeIsDark = isThemeDark(currentTheme);
// If a background color doesn't exist or it inherits, pass back if it's a darktheme
if (backgroundColor === undefined || backgroundColor === 'inherit') {
if (!backgroundColor || backgroundColor === 'inherit') {
return themeIsDark;
}

View file

@ -26,11 +26,10 @@ import { AddDeleteButtons } from './add_delete_buttons';
import { ColorPicker } from './color_picker';
import { FieldSelect } from './aggs/field_select';
import uuid from 'uuid';
import { IconSelect } from './icon_select';
import { IconSelect } from './icon_select/icon_select';
import { YesNo } from './yes_no';
import { QueryBarWrapper } from './query_bar_wrapper';
import { getDefaultQueryLanguage } from './lib/get_default_query_language';
import {
htmlIdGenerator,
EuiFlexGroup,

View file

@ -1,127 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import { EuiComboBox } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
function renderOption(option) {
const icon = option.value;
const label = option.label;
return (
<span>
<span className={`kuiIcon ${icon}`} aria-hidden="true" />
{` ${label}`}
</span>
);
}
export function IconSelect(props) {
const selectedIcon = props.icons.find(option => {
return props.value === option.value;
});
return (
<EuiComboBox
isClearable={false}
id={props.id}
options={props.icons}
selectedOptions={selectedIcon ? [selectedIcon] : []}
onChange={props.onChange}
singleSelection={{ asPlainText: true }}
renderOption={renderOption}
/>
);
}
IconSelect.defaultProps = {
icons: [
{
value: 'fa-asterisk',
label: i18n.translate('tsvb.iconSelect.asteriskLabel', { defaultMessage: 'Asterisk' }),
},
{
value: 'fa-bell',
label: i18n.translate('tsvb.iconSelect.bellLabel', { defaultMessage: 'Bell' }),
},
{
value: 'fa-bolt',
label: i18n.translate('tsvb.iconSelect.boltLabel', { defaultMessage: 'Bolt' }),
},
{
value: 'fa-bomb',
label: i18n.translate('tsvb.iconSelect.bombLabel', { defaultMessage: 'Bomb' }),
},
{
value: 'fa-bug',
label: i18n.translate('tsvb.iconSelect.bugLabel', { defaultMessage: 'Bug' }),
},
{
value: 'fa-comment',
label: i18n.translate('tsvb.iconSelect.commentLabel', { defaultMessage: 'Comment' }),
},
{
value: 'fa-exclamation-circle',
label: i18n.translate('tsvb.iconSelect.exclamationCircleLabel', {
defaultMessage: 'Exclamation Circle',
}),
},
{
value: 'fa-exclamation-triangle',
label: i18n.translate('tsvb.iconSelect.exclamationTriangleLabel', {
defaultMessage: 'Exclamation Triangle',
}),
},
{
value: 'fa-fire',
label: i18n.translate('tsvb.iconSelect.fireLabel', { defaultMessage: 'Fire' }),
},
{
value: 'fa-flag',
label: i18n.translate('tsvb.iconSelect.flagLabel', { defaultMessage: 'Flag' }),
},
{
value: 'fa-heart',
label: i18n.translate('tsvb.iconSelect.heartLabel', { defaultMessage: 'Heart' }),
},
{
value: 'fa-map-marker',
label: i18n.translate('tsvb.iconSelect.mapMarkerLabel', { defaultMessage: 'Map Marker' }),
},
{
value: 'fa-map-pin',
label: i18n.translate('tsvb.iconSelect.mapPinLabel', { defaultMessage: 'Map Pin' }),
},
{
value: 'fa-star',
label: i18n.translate('tsvb.iconSelect.starLabel', { defaultMessage: 'Star' }),
},
{
value: 'fa-tag',
label: i18n.translate('tsvb.iconSelect.tagLabel', { defaultMessage: 'Tag' }),
},
],
};
IconSelect.propTypes = {
icons: PropTypes.array,
id: PropTypes.string,
onChange: PropTypes.func,
value: PropTypes.string.isRequired,
};

View file

@ -0,0 +1,162 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`src/legacy/core_plugins/metrics/public/components/icon_select/icon_select.js <IconSelect /> should render and match a snapshot 1`] = `
<EuiComboBox
compressed={false}
fullWidth={false}
isClearable={false}
onChange={[MockFunction]}
options={
Array [
Object {
"label": "Asterisk",
"value": "fa-asterisk",
},
Object {
"label": "Bell",
"value": "fa-bell",
},
Object {
"label": "Bolt",
"value": "fa-bolt",
},
Object {
"label": "Comment",
"value": "fa-comment",
},
Object {
"label": "Map Marker",
"value": "fa-map-marker",
},
Object {
"label": "Map Pin",
"value": "fa-map-pin",
},
Object {
"label": "Star",
"value": "fa-star",
},
Object {
"label": "Tag",
"value": "fa-tag",
},
Object {
"label": "Bomb",
"value": "fa-bomb",
},
Object {
"label": "Bug",
"value": "fa-bug",
},
Object {
"label": "Exclamation Circle",
"value": "fa-exclamation-circle",
},
Object {
"label": "Exclamation Triangle",
"value": "fa-exclamation-triangle",
},
Object {
"label": "Fire",
"value": "fa-fire",
},
Object {
"label": "Flag",
"value": "fa-flag",
},
Object {
"label": "Heart",
"value": "fa-heart",
},
]
}
renderOption={[Function]}
selectedOptions={
Array [
Object {
"label": "Bell",
"value": "fa-bell",
},
]
}
singleSelection={
Object {
"asPlainText": true,
}
}
/>
`;
exports[`src/legacy/core_plugins/metrics/public/components/icon_select/icon_select.js <IconView /> should render and match a snapshot 1`] = `
<span>
<EuiIcon
type="editorComment"
/>
Comment
</span>
`;
exports[`src/legacy/core_plugins/metrics/public/components/icon_select/icon_select.js ICONS should match and save an icons collection snapshot 1`] = `
Array [
Object {
"label": "Asterisk",
"value": "fa-asterisk",
},
Object {
"label": "Bell",
"value": "fa-bell",
},
Object {
"label": "Bolt",
"value": "fa-bolt",
},
Object {
"label": "Comment",
"value": "fa-comment",
},
Object {
"label": "Map Marker",
"value": "fa-map-marker",
},
Object {
"label": "Map Pin",
"value": "fa-map-pin",
},
Object {
"label": "Star",
"value": "fa-star",
},
Object {
"label": "Tag",
"value": "fa-tag",
},
Object {
"label": "Bomb",
"value": "fa-bomb",
},
Object {
"label": "Bug",
"value": "fa-bug",
},
Object {
"label": "Exclamation Circle",
"value": "fa-exclamation-circle",
},
Object {
"label": "Exclamation Triangle",
"value": "fa-exclamation-triangle",
},
Object {
"label": "Fire",
"value": "fa-fire",
},
Object {
"label": "Flag",
"value": "fa-flag",
},
Object {
"label": "Heart",
"value": "fa-heart",
},
]
`;

View file

@ -0,0 +1,120 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import { EuiComboBox, EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ICON_TYPES_MAP } from '../../visualizations/constants/icons';
export const ICONS = [
{
value: 'fa-asterisk',
label: i18n.translate('tsvb.iconSelect.asteriskLabel', { defaultMessage: 'Asterisk' }),
},
{
value: 'fa-bell',
label: i18n.translate('tsvb.iconSelect.bellLabel', { defaultMessage: 'Bell' }),
},
{
value: 'fa-bolt',
label: i18n.translate('tsvb.iconSelect.boltLabel', { defaultMessage: 'Bolt' }),
},
{
value: 'fa-comment',
label: i18n.translate('tsvb.iconSelect.commentLabel', { defaultMessage: 'Comment' }),
},
{
value: 'fa-map-marker',
label: i18n.translate('tsvb.iconSelect.mapMarkerLabel', { defaultMessage: 'Map Marker' }),
},
{
value: 'fa-map-pin',
label: i18n.translate('tsvb.iconSelect.mapPinLabel', { defaultMessage: 'Map Pin' }),
},
{
value: 'fa-star',
label: i18n.translate('tsvb.iconSelect.starLabel', { defaultMessage: 'Star' }),
},
{
value: 'fa-tag',
label: i18n.translate('tsvb.iconSelect.tagLabel', { defaultMessage: 'Tag' }),
},
{
value: 'fa-bomb',
label: i18n.translate('tsvb.iconSelect.bombLabel', { defaultMessage: 'Bomb' }),
},
{
value: 'fa-bug',
label: i18n.translate('tsvb.iconSelect.bugLabel', { defaultMessage: 'Bug' }),
},
{
value: 'fa-exclamation-circle',
label: i18n.translate('tsvb.iconSelect.exclamationCircleLabel', {
defaultMessage: 'Exclamation Circle',
}),
},
{
value: 'fa-exclamation-triangle',
label: i18n.translate('tsvb.iconSelect.exclamationTriangleLabel', {
defaultMessage: 'Exclamation Triangle',
}),
},
{
value: 'fa-fire',
label: i18n.translate('tsvb.iconSelect.fireLabel', { defaultMessage: 'Fire' }),
},
{
value: 'fa-flag',
label: i18n.translate('tsvb.iconSelect.flagLabel', { defaultMessage: 'Flag' }),
},
{
value: 'fa-heart',
label: i18n.translate('tsvb.iconSelect.heartLabel', { defaultMessage: 'Heart' }),
},
];
export function IconView({ value: icon, label }) {
return (
<span>
<EuiIcon type={ICON_TYPES_MAP[icon]} />
{` ${label}`}
</span>
);
}
export function IconSelect({ value, onChange }) {
const selectedIcon = ICONS.find(option => value === option.value) || ICONS[0];
return (
<EuiComboBox
isClearable={false}
options={ICONS}
selectedOptions={[selectedIcon]}
onChange={onChange}
singleSelection={{ asPlainText: true }}
renderOption={IconView}
/>
);
}
IconSelect.propTypes = {
onChange: PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
};

View file

@ -0,0 +1,52 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { IconSelect, IconView, ICONS } from './icon_select';
describe('src/legacy/core_plugins/metrics/public/components/icon_select/icon_select.js', () => {
describe('<IconSelect />', () => {
test('should render and match a snapshot', () => {
const wrapper = shallow(<IconSelect onChange={jest.fn()} value={ICONS[1].value} />);
expect(wrapper).toMatchSnapshot();
});
test("should put the default value if the passed one does't match with icons collection", () => {
const wrapper = shallow(<IconSelect onChange={jest.fn()} value="unknown" />);
expect(wrapper.prop('selectedOptions')).toEqual([ICONS[0]]);
});
});
describe('<IconView />', () => {
test('should render and match a snapshot', () => {
const wrapper = shallow(<IconView label="Comment" value="fa-comment" />);
expect(wrapper).toMatchSnapshot();
});
});
describe('ICONS', () => {
test('should match and save an icons collection snapshot', () => {
expect(ICONS).toMatchSnapshot();
});
});
});

View file

@ -18,11 +18,11 @@
*/
import { expect } from 'chai';
import { tickFormatter } from '../tick_formatter';
import { createTickFormatter } from '../tick_formatter';
describe('tickFormatter(format, template)', () => {
describe('createTickFormatter(format, template)', () => {
it('returns a number with two decimal place by default', () => {
const fn = tickFormatter();
const fn = createTickFormatter();
expect(fn(1.5556)).to.equal('1.56');
});
@ -30,7 +30,7 @@ describe('tickFormatter(format, template)', () => {
const config = {
'format:percent:defaultPattern': '0.[00]%',
};
const fn = tickFormatter('percent', null, key => config[key]);
const fn = createTickFormatter('percent', null, key => config[key]);
expect(fn(0.5556)).to.equal('55.56%');
});
@ -38,12 +38,12 @@ describe('tickFormatter(format, template)', () => {
const config = {
'format:bytes:defaultPattern': '0.0b',
};
const fn = tickFormatter('bytes', null, key => config[key]);
const fn = createTickFormatter('bytes', null, key => config[key]);
expect(fn(1500 ^ 10)).to.equal('1.5KB');
});
it('returns a custom formatted string with custom formatter', () => {
const fn = tickFormatter('0.0a');
const fn = createTickFormatter('0.0a');
expect(fn(1500)).to.equal('1.5k');
});
@ -51,17 +51,22 @@ describe('tickFormatter(format, template)', () => {
const config = {
'format:number:defaultLocale': 'fr',
};
const fn = tickFormatter('0,0.0', null, key => config[key]);
const fn = createTickFormatter('0,0.0', null, key => config[key]);
expect(fn(1500)).to.equal('1 500,0');
});
it('returns a custom formatted string with custom formatter and template', () => {
const fn = tickFormatter('0.0a', '{{value}}/s');
const fn = createTickFormatter('0.0a', '{{value}}/s');
expect(fn(1500)).to.equal('1.5k/s');
});
it('returns "foo" if passed a string', () => {
const fn = createTickFormatter();
expect(fn('foo')).to.equal('foo');
});
it('returns value if passed a bad formatter', () => {
const fn = tickFormatter('102');
const fn = createTickFormatter('102');
expect(fn(100)).to.equal('100');
});
@ -69,7 +74,7 @@ describe('tickFormatter(format, template)', () => {
const config = {
'format:number:defaultPattern': '0,0.[00]',
};
const fn = tickFormatter('number', '{{value', key => config[key]);
const fn = createTickFormatter('number', '{{value', key => config[key]);
expect(fn(1.5556)).to.equal('1.56');
});
});

View file

@ -17,10 +17,10 @@
* under the License.
*/
export const COLORS = {
lineColor: 'rgba(105,112,125,0.2)',
textColor: 'rgba(0,0,0,0.4)',
textColorReversed: 'rgba(255,255,255,0.5)',
valueColor: 'rgba(0,0,0,0.7)',
valueColorReversed: 'rgba(255,255,255,0.8)',
};
import { uniq, map, size, flow } from 'lodash';
export const areFieldsDifferent = name => series =>
flow(
uniq,
size
)(map(series, name)) > 1;

View file

@ -19,7 +19,7 @@
import _ from 'lodash';
import { getLastValue } from '../../../common/get_last_value';
import { tickFormatter } from './tick_formatter';
import { createTickFormatter } from './tick_formatter';
import moment from 'moment';
export const convertSeriesToVars = (series, model, dateFormat = 'lll', getConfig = null) => {
@ -32,7 +32,7 @@ export const convertSeriesToVars = (series, model, dateFormat = 'lll', getConfig
.filter(v => v)
.join('.');
const formatter = tickFormatter(
const formatter = createTickFormatter(
seriesModel.formatter,
seriesModel.value_template,
getConfig

View file

@ -20,6 +20,10 @@ import { convertIntervalIntoUnit } from './get_interval';
import { i18n } from '@kbn/i18n';
export function getAxisLabelString(interval) {
if (!interval) {
return '';
}
const convertedValue = convertIntervalIntoUnit(interval);
if (convertedValue) {

View file

@ -20,6 +20,7 @@
import uuid from 'uuid';
import _ from 'lodash';
import { newMetricAggFn } from './new_metric_agg_fn';
import { STACKED_OPTIONS } from '../../visualizations/constants';
export const newSeriesFn = (obj = {}) => {
return _.assign(
@ -35,7 +36,7 @@ export const newSeriesFn = (obj = {}) => {
line_width: 1,
point_size: 1,
fill: 0.5,
stacked: 'none',
stacked: STACKED_OPTIONS.NONE,
},
obj
);

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export const isPercentDisabled = seriesQuantity => seriesQuantity < 2;

View file

@ -22,7 +22,7 @@ import { isNumber } from 'lodash';
import { fieldFormats } from 'ui/registry/field_formats';
import { inputFormats, outputFormats, isDuration } from '../lib/durations';
export const tickFormatter = (format = '0,0.[00]', template, getConfig = null) => {
export const createTickFormatter = (format = '0,0.[00]', template, getConfig = null) => {
if (!template) template = '{{value}}';
const render = handlebars.compile(template, { knownHelpersOnly: true });
let formatter;

View file

@ -24,7 +24,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { tickFormatter } from './lib/tick_formatter';
import { createTickFormatter } from './lib/tick_formatter';
import { convertSeriesToVars } from './lib/convert_series_to_vars';
import _ from 'lodash';
import 'brace/mode/markdown';
@ -59,7 +59,7 @@ export class MarkdownEditor extends Component {
const series = _.get(visData, `${model.id}.series`, []);
const variables = convertSeriesToVars(series, model, dateFormat, this.props.getConfig);
const rows = [];
const rawFormatter = tickFormatter('0.[0000]', null, this.props.getConfig);
const rawFormatter = createTickFormatter('0.[0000]', null, this.props.getConfig);
const createPrimitiveRow = key => {
const snippet = `{{ ${key} }}`;

View file

@ -57,7 +57,7 @@ export function PanelConfig(props) {
return function cleanup() {
visDataSubscription.unsubscribe();
};
}, [props.visData$]);
}, [model.id, props.visData$]);
const updateControlValidity = (controlKey, isControlValid) => {
formValidationResults[controlKey] = isControlValid;

View file

@ -19,7 +19,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { assign } from 'lodash';
import { assign, get } from 'lodash';
import { TimeseriesSeries as timeseries } from './vis_types/timeseries/series';
import { MetricSeries as metric } from './vis_types/metric/series';
@ -79,30 +79,41 @@ export class Series extends Component {
return Boolean(Component) ? (
<VisDataContext.Consumer>
{visData => (
<Component
className={this.props.className}
disableAdd={this.props.disableAdd}
uiRestrictions={visData.uiRestrictions}
disableDelete={this.props.disableDelete}
fields={this.props.fields}
name={this.props.name}
onAdd={this.props.onAdd}
onChange={this.handleChange}
onClone={this.props.onClone}
onDelete={this.props.onDelete}
model={this.props.model}
panel={this.props.panel}
selectedTab={this.state.selectedTab}
style={this.props.style}
switchTab={this.switchTab}
toggleVisible={this.toggleVisible}
togglePanelActivation={this.togglePanelActivation}
visible={this.state.visible}
dragHandleProps={this.props.dragHandleProps}
indexPatternForQuery={panel.index_pattern || panel.default_index_pattern}
/>
)}
{visData => {
const series = get(visData, `${panel.id}.series`, []);
const counter = {};
const seriesQuantity = series.reduce((acc, value) => {
counter[value.seriesId] = counter[value.seriesId] + 1 || 1;
acc[value.seriesId] = counter[value.seriesId];
return acc;
}, {});
return (
<Component
className={this.props.className}
disableAdd={this.props.disableAdd}
uiRestrictions={visData.uiRestrictions}
seriesQuantity={seriesQuantity}
disableDelete={this.props.disableDelete}
fields={this.props.fields}
name={this.props.name}
onAdd={this.props.onAdd}
onChange={this.handleChange}
onClone={this.props.onClone}
onDelete={this.props.onDelete}
model={this.props.model}
panel={this.props.panel}
selectedTab={this.state.selectedTab}
style={this.props.style}
switchTab={this.switchTab}
toggleVisible={this.toggleVisible}
togglePanelActivation={this.togglePanelActivation}
visible={this.state.visible}
dragHandleProps={this.props.dragHandleProps}
indexPatternForQuery={panel.index_pattern || panel.default_index_pattern}
/>
);
}}
</VisDataContext.Consumer>
) : (
<FormattedMessage

View file

@ -73,7 +73,7 @@ export class Split extends Component {
}
render() {
const { model, panel, uiRestrictions } = this.props;
const { model, panel, uiRestrictions, seriesQuantity } = this.props;
const indexPattern =
(model.override_index_pattern && model.series_index_pattern) || panel.index_pattern;
const splitMode = get(this.props, 'model.split_mode', SPLIT_MODES.EVERYTHING);
@ -86,6 +86,7 @@ export class Split extends Component {
fields={this.props.fields}
onChange={this.props.onChange}
uiRestrictions={uiRestrictions}
seriesQuantity={seriesQuantity}
/>
);
}
@ -96,4 +97,5 @@ Split.propTypes = {
model: PropTypes.object,
onChange: PropTypes.func,
panel: PropTypes.object,
seriesQuantity: PropTypes.object,
};

View file

@ -23,6 +23,7 @@ import { get, find } from 'lodash';
import { GroupBySelect } from './group_by_select';
import { createTextHandler } from '../lib/create_text_handler';
import { createSelectHandler } from '../lib/create_select_handler';
import { isPercentDisabled } from '../lib/stacked';
import { FieldSelect } from '../aggs/field_select';
import { MetricSelect } from '../aggs/metric_select';
import {
@ -36,6 +37,7 @@ import {
} from '@elastic/eui';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import { FIELD_TYPES } from '../../../common/field_types';
import { STACKED_OPTIONS } from '../../visualizations/constants';
const DEFAULTS = { terms_direction: 'desc', terms_size: 10, terms_order_by: '_count' };
@ -46,6 +48,7 @@ export const SplitByTermsUI = ({
model: seriesModel,
fields,
uiRestrictions,
seriesQuantity,
}) => {
const htmlId = htmlIdGenerator();
const handleTextChange = createTextHandler(onChange);
@ -86,6 +89,14 @@ export const SplitByTermsUI = ({
const selectedField = find(fields[indexPattern], ({ name }) => name === model.terms_field);
const selectedFieldType = get(selectedField, 'type');
if (
seriesQuantity &&
model.stacked === STACKED_OPTIONS.PERCENT &&
isPercentDisabled(seriesQuantity[model.id])
) {
onChange({ ['stacked']: STACKED_OPTIONS.NONE });
}
return (
<div>
<EuiFlexGroup>
@ -218,6 +229,7 @@ SplitByTermsUI.propTypes = {
indexPattern: PropTypes.string,
fields: PropTypes.object,
uiRestrictions: PropTypes.object,
seriesQuantity: PropTypes.object,
};
export const SplitByTerms = injectI18n(SplitByTermsUI);

View file

@ -40,8 +40,12 @@ describe('src/legacy/core_plugins/metrics/public/components/splits/terms.test.js
formatMessage: jest.fn(),
},
model: {
id: 123,
terms_field: 'OriginCityName',
},
seriesQuantity: {
id123: 123,
},
onChange: jest.fn(),
indexPattern: 'kibana_sample_data_flights',
fields: {

View file

@ -0,0 +1,26 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
export const bombIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M3.92176768,4.53361616 L3.46466635,5.59664282 L2.65302767,4.14840719 L0.999875397,3.99598886 L2.12641918,2.77654518 L1.76052549,1.15720408 L3.26840652,1.85178207 L4.69542405,1.00339256 L4.500802,2.65210905 L5.74864103,3.7471166 L4.73514583,3.9490388 L5.61669633,5.03766301 C6.0459022,4.69009896 6.67559863,4.75628272 7.02316269,5.18548858 L7.15815921,5.3521954 C7.8837785,5.06498672 8.68754455,4.943927 9.51742529,5.03115098 C12.2637217,5.31979836 14.2560398,7.78010639 13.9673924,10.5264028 C13.6787451,13.2726992 11.218437,15.2650173 8.47214065,14.9763699 C5.72584427,14.6877226 3.73352611,12.2274145 4.02217349,9.48111814 C4.10939747,8.6512374 4.39492411,7.8902053 4.82672133,7.24015658 L4.6917248,7.07344975 C4.34416075,6.64424389 4.41034451,6.01454746 4.83955037,5.6669834 L3.92176768,4.53361616 Z M5.46887076,6.44412936 L6.12580983,7.24015658 C6.03489722,7.30663504 5.87952666,7.491071 5.65969815,7.79346445 C5.30650784,8.32517447 5.08508565,8.93495668 5.01669539,9.5856466 C4.78577748,11.7826837 6.37963201,13.7509301 8.57666912,13.981848 C10.7737062,14.2127659 12.7419526,12.6189114 12.9728706,10.4218743 C13.2037885,8.2248372 11.6099339,6.25659078 9.41289682,6.02567287 C8.7622069,5.95728261 8.11971364,6.04708534 7.52619036,6.28200886 C7.24048061,6.40187373 7.0242157,6.5121707 6.87739563,6.61289978 L6.24601673,5.81480897 L5.46887076,6.44412936 Z M3.34009664,3.61834468 C3.6160708,3.60870745 3.83197954,3.37717367 3.82234231,3.10119951 C3.81270508,2.82522536 3.5811713,2.60931662 3.30519715,2.61895385 C3.02922299,2.62859108 2.81331425,2.86012485 2.82295148,3.13609901 C2.83258871,3.41207317 3.06412249,3.62798191 3.34009664,3.61834468 Z M9.2038399,8.01471666 C8.92921026,7.98585193 8.72997844,7.73982112 8.75884318,7.46519148 C8.78770792,7.19056185 9.03373872,6.99133003 9.30836836,7.02019477 C10.7411945,7.17079087 11.8319627,8.30660625 11.9782919,9.68376241 C11.9842403,9.72621687 11.9855849,9.77015886 11.9808868,9.81485847 C11.9520221,10.0894881 11.7059913,10.2887199 11.4313616,10.2598552 C11.1835304,10.2338071 10.9971003,10.0309074 10.9842889,9.78973323 C10.8859009,8.87197536 10.1588372,8.11509093 9.2038399,8.01471666 Z" />
</svg>
);

View file

@ -0,0 +1,26 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
export const fireIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M10.4285489,13.2131862 C11.3850613,12.5116211 12,11.4062861 12,10.1681623 C12,8.88948911 11.7647926,7.64522527 11.315853,6.48027409 L10.7184365,8.07936372 L9.62838732,6.51595716 C8.65161637,5.11502026 7.41222171,3.90368767 5.9820674,2.94508952 C5.99399825,3.12925457 6,3.31436795 6,3.50022638 C6,5.08794888 5.48443413,6.95085436 4.74437685,7.94154468 C4.26219055,8.5870316 4,9.35779518 4,10.1681623 C4,11.4427639 4.65170744,12.5766371 5.65687647,13.2741368 C5.73557697,13.041105 5.84196228,12.8229717 5.97161458,12.624616 C6.26836682,12.1706132 6.5,11.2280234 6.5,10.4287008 C6.5,9.92871906 6.42479348,9.44813694 6.28619258,9 C7.47628141,9.64169048 8.4897566,10.6210605 9.22434324,11.8251557 C9.23213615,11.8013168 9.2406884,11.7775998 9.25,11.7540048 C9.39656041,11.3826304 9.5,11.0805724 9.5,10.6495848 C9.5,10.6240812 9.49971537,10.5986511 9.49914944,10.5732981 C9.96875289,11.3529062 10.2928315,12.2486386 10.4285489,13.2131862 Z M10.4486865,5.94402249 C10.4642723,5.90230435 10.4813768,5.86079967 10.5,5.81950846 C10.7931208,5.16960326 11,4.64100165 11,3.88677339 C11,3.84214218 10.9994307,3.79763944 10.9982989,3.7532716 C12.2630338,5.59045545 13,7.7961396 13,10.1681623 C13,12.8367126 10.7614237,15 8,15 C5.23857625,15 3,12.8367126 3,10.1681623 C3,9.11340491 3.34972471,8.13758287 3.94322917,7.34307798 C4.53673363,6.54857309 5,4.8990409 5,3.50022638 C5,2.62525836 4.84958695,1.78423964 4.57238517,1 C6.95256283,2.12295834 8.97951321,3.83685594 10.4486865,5.94402249 Z" />
</svg>
);

View file

@ -27,7 +27,7 @@ import { VisEditorVisualization } from './vis_editor_visualization';
import { Visualization } from './visualization';
import { VisPicker } from './vis_picker';
import { PanelConfig } from './panel_config';
import { brushHandler } from '../lib/create_brush_handler';
import { createBrushHandler } from '../lib/create_brush_handler';
import { fetchFields } from '../lib/fetch_fields';
import { extractIndexPatterns } from '../../common/extract_index_patterns';
@ -51,7 +51,7 @@ export class VisEditor extends Component {
visFields: props.visFields,
extractedIndexPatterns: [''],
};
this.onBrush = brushHandler(props.vis.API.timeFilter);
this.onBrush = createBrushHandler(props.vis.API.timeFilter);
this.visDataSubject = new Rx.BehaviorSubject(this.props.visData);
this.visData$ = this.visDataSubject.asObservable().pipe(share());

View file

@ -31,7 +31,7 @@ import {
} from './lib/get_interval';
import { PANEL_TYPES } from '../../common/panel_types';
const MIN_CHART_HEIGHT = 250;
const MIN_CHART_HEIGHT = 300;
class VisEditorVisualizationUI extends Component {
constructor(props) {

View file

@ -3,4 +3,8 @@
flex-direction: column;
flex: 1 1 100%;
padding: $euiSizeS;
.tvbVisTimeSeries {
overflow: hidden;
}
}

View file

@ -20,9 +20,9 @@
import PropTypes from 'prop-types';
import React from 'react';
import { visWithSplits } from '../../vis_with_splits';
import { tickFormatter } from '../../lib/tick_formatter';
import { createTickFormatter } from '../../lib/tick_formatter';
import _, { get, isUndefined, assign, includes } from 'lodash';
import { Gauge } from '../../../visualizations/components/gauge';
import { Gauge } from '../../../visualizations/views/gauge';
import { getLastValue } from '../../../../common/get_last_value';
function getColors(props) {
@ -54,7 +54,7 @@ function GaugeVisualization(props) {
const seriesDef = model.series.find(s => includes(row.id, s.id));
const newProps = {};
if (seriesDef) {
newProps.formatter = tickFormatter(
newProps.formatter = createTickFormatter(
seriesDef.formatter,
seriesDef.value_template,
props.getConfig

View file

@ -20,9 +20,9 @@
import PropTypes from 'prop-types';
import React from 'react';
import { visWithSplits } from '../../vis_with_splits';
import { tickFormatter } from '../../lib/tick_formatter';
import { createTickFormatter } from '../../lib/tick_formatter';
import _, { get, isUndefined, assign, includes, pick } from 'lodash';
import { Metric } from '../../../visualizations/components/metric';
import { Metric } from '../../../visualizations/views/metric';
import { getLastValue } from '../../../../common/get_last_value';
import { isBackgroundInverted } from '../../../../common/set_is_reversed';
@ -54,7 +54,7 @@ function MetricVisualization(props) {
const seriesDef = model.series.find(s => includes(row.id, s.id));
const newProps = {};
if (seriesDef) {
newProps.formatter = tickFormatter(
newProps.formatter = createTickFormatter(
seriesDef.formatter,
seriesDef.value_template,
props.getConfig

View file

@ -21,7 +21,7 @@ import _, { isArray, last, get } from 'lodash';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { fieldFormats } from 'ui/registry/field_formats';
import { tickFormatter } from '../../lib/tick_formatter';
import { createTickFormatter } from '../../lib/tick_formatter';
import { calculateLabel } from '../../../../common/calculate_label';
import { isSortable } from './is_sortable';
import { EuiToolTip, EuiIcon } from '@elastic/eui';
@ -68,7 +68,7 @@ export class TableVis extends Component {
.map(item => {
const column = this.visibleSeries.find(c => c.id === item.id);
if (!column) return null;
const formatter = tickFormatter(
const formatter = createTickFormatter(
column.formatter,
column.value_template,
this.props.getConfig

View file

@ -41,6 +41,9 @@ import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import { getDefaultQueryLanguage } from '../../lib/get_default_query_language';
import { QueryBarWrapper } from '../../query_bar_wrapper';
import { isPercentDisabled } from '../../lib/stacked';
import { STACKED_OPTIONS } from '../../../visualizations/constants/chart';
export const TimeseriesConfig = injectI18n(function(props) {
const handleSelectChange = createSelectHandler(props.onChange);
const handleTextChange = createTextHandler(props.onChange);
@ -53,7 +56,7 @@ export const TimeseriesConfig = injectI18n(function(props) {
split_color_mode: 'gradient',
axis_min: '',
axis_max: '',
stacked: 'none',
stacked: STACKED_OPTIONS.NONE,
steps: 0,
};
const model = { ...defaults, ...props.model };
@ -62,22 +65,23 @@ export const TimeseriesConfig = injectI18n(function(props) {
const stackedOptions = [
{
label: intl.formatMessage({ id: 'tsvb.timeSeries.noneLabel', defaultMessage: 'None' }),
value: 'none',
value: STACKED_OPTIONS.NONE,
},
{
label: intl.formatMessage({ id: 'tsvb.timeSeries.stackedLabel', defaultMessage: 'Stacked' }),
value: 'stacked',
value: STACKED_OPTIONS.STACKED,
},
{
label: intl.formatMessage({
id: 'tsvb.timeSeries.stackedWithinSeriesLabel',
defaultMessage: 'Stacked within series',
}),
value: 'stacked_within_series',
value: STACKED_OPTIONS.STACKED_WITHIN_SERIES,
},
{
label: intl.formatMessage({ id: 'tsvb.timeSeries.percentLabel', defaultMessage: 'Percent' }),
value: 'percent',
value: STACKED_OPTIONS.PERCENT,
disabled: isPercentDisabled(props.seriesQuantity[model.id]),
},
];
const selectedStackedOption = stackedOptions.find(option => {
@ -130,6 +134,7 @@ export const TimeseriesConfig = injectI18n(function(props) {
});
let type;
if (model.chart_type === 'line') {
type = (
<EuiFlexGroup gutterSize="s" responsive={false} wrap={true}>
@ -282,7 +287,7 @@ export const TimeseriesConfig = injectI18n(function(props) {
}
>
<EuiFieldNumber
step={0.5}
step={0.1}
onChange={handleTextChange('fill')}
value={Number(model.fill)}
/>
@ -529,4 +534,5 @@ TimeseriesConfig.propTypes = {
model: PropTypes.object,
onChange: PropTypes.func,
indexPatternForQuery: PropTypes.string,
seriesQuantity: PropTypes.object,
};

View file

@ -51,6 +51,7 @@ const TimeseriesSeriesUI = injectI18n(function(props) {
intl,
name,
uiRestrictions,
seriesQuantity,
} = props;
const defaults = {
@ -87,6 +88,7 @@ const TimeseriesSeriesUI = injectI18n(function(props) {
panel={panel}
model={model}
uiRestrictions={uiRestrictions}
seriesQuantity={seriesQuantity}
/>
</div>
</div>
@ -98,6 +100,7 @@ const TimeseriesSeriesUI = injectI18n(function(props) {
model={model}
onChange={props.onChange}
indexPatternForQuery={props.indexPatternForQuery}
seriesQuantity={seriesQuantity}
/>
);
}
@ -211,6 +214,7 @@ TimeseriesSeriesUI.propTypes = {
uiRestrictions: PropTypes.object,
dragHandleProps: PropTypes.object,
indexPatternForQuery: PropTypes.string,
seriesQuantity: PropTypes.object,
};
export const TimeseriesSeries = injectI18n(TimeseriesSeriesUI);

View file

@ -19,35 +19,90 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { toastNotifications } from 'ui/notify';
import reactCSS from 'reactcss';
import { tickFormatter } from '../../lib/tick_formatter';
import _ from 'lodash';
import { Timeseries } from '../../../visualizations/components/timeseries';
import { startsWith, get, cloneDeep, map } from 'lodash';
import { toastNotifications } from 'ui/notify';
import { htmlIdGenerator } from '@elastic/eui';
import { ScaleType } from '@elastic/charts';
import { createTickFormatter } from '../../lib/tick_formatter';
import { TimeSeries } from '../../../visualizations/views/timeseries';
import { MarkdownSimple } from '../../../../../kibana_react/public';
import { replaceVars } from '../../lib/replace_vars';
import { getAxisLabelString } from '../../lib/get_axis_label_string';
import { getInterval } from '../../lib/get_interval';
import { areFieldsDifferent } from '../../lib/charts';
import { createXaxisFormatter } from '../../lib/create_xaxis_formatter';
function hasSeparateAxis(row) {
return row.separate_axis;
}
import { isBackgroundDark } from '../../../../common/set_is_reversed';
import { STACKED_OPTIONS } from '../../../visualizations/constants';
export class TimeseriesVisualization extends Component {
getInterval = () => {
const { visData, model } = this.props;
return getInterval(visData, model);
static propTypes = {
model: PropTypes.object,
onBrush: PropTypes.func,
visData: PropTypes.object,
dateFormat: PropTypes.string,
getConfig: PropTypes.func,
};
xaxisFormatter = val => {
xAxisFormatter = interval => val => {
const { scaledDataFormat, dateFormat } = this.props.visData;
if (!scaledDataFormat || !dateFormat) return val;
const formatter = createXaxisFormatter(this.getInterval(), scaledDataFormat, dateFormat);
if (!scaledDataFormat || !dateFormat) {
return val;
}
const formatter = createXaxisFormatter(interval, scaledDataFormat, dateFormat);
return formatter(val);
};
yAxisStackedByPercentFormatter = val => {
const n = Number(val) * 100;
return `${(Number.isNaN(n) ? 0 : n).toFixed(0)}%`;
};
applyDocTo = template => doc => {
const vars = replaceVars(template, null, doc);
if (vars instanceof Error) {
this.showToastNotification = vars.error.caused_by;
return template;
}
return vars;
};
static getYAxisDomain = model => {
const axisMin = get(model, 'axis_min', '').toString();
const axisMax = get(model, 'axis_max', '').toString();
return {
min: axisMin.length ? Number(axisMin) : undefined,
max: axisMax.length ? Number(axisMax) : undefined,
};
};
static addYAxis = (yAxis, { id, groupId, position, tickFormatter, domain, hide }) => {
yAxis.push({
id,
groupId,
position,
tickFormatter,
domain,
hide,
});
};
static getAxisScaleType = model =>
get(model, 'axis_scale') === 'log' ? ScaleType.Log : ScaleType.Linear;
static getTickFormatter = (model, getConfig) =>
createTickFormatter(get(model, 'formatter'), get(model, 'value_template'), getConfig);
componentDidUpdate() {
if (
this.showToastNotification &&
@ -71,181 +126,129 @@ export class TimeseriesVisualization extends Component {
}
}
prepareAnnotations = () => {
const { model, visData } = this.props;
return map(model.annotations, ({ id, color, icon, template }) => {
const annotationData = get(visData, `${model.id}.annotations.${id}`, []);
const applyDocToTemplate = this.applyDocTo(template);
return {
id,
color,
icon,
data: annotationData.map(({ docs, ...rest }) => ({
...rest,
docs: docs.map(applyDocToTemplate),
})),
};
});
};
render() {
const { backgroundColor, model, visData } = this.props;
const series = _.get(visData, `${model.id}.series`, []);
let annotations;
const { model, visData, onBrush } = this.props;
const styles = reactCSS({
default: {
tvbVis: {
backgroundColor: get(model, 'background_color'),
},
},
});
const series = get(visData, `${model.id}.series`, []);
const interval = getInterval(visData, model);
const yAxisIdGenerator = htmlIdGenerator('yaxis');
const mainAxisGroupId = yAxisIdGenerator('main_group');
const seriesModel = model.series.filter(s => !s.hidden).map(s => cloneDeep(s));
const enableHistogramMode = areFieldsDifferent('chart_type')(seriesModel);
const firstSeries = seriesModel.find(s => s.formatter && !s.separate_axis);
const mainAxisScaleType = TimeseriesVisualization.getAxisScaleType(model);
const mainAxisDomain = TimeseriesVisualization.getYAxisDomain(model);
const tickFormatter = TimeseriesVisualization.getTickFormatter(
firstSeries,
this.props.getConfig
);
const yAxis = [];
let mainDomainAdded = false;
this.showToastNotification = null;
if (model.annotations && Array.isArray(model.annotations)) {
annotations = model.annotations.map(annotation => {
const data = _.get(visData, `${model.id}.annotations.${annotation.id}`, []).map(item => [
item.key,
item.docs,
]);
return {
id: annotation.id,
color: annotation.color,
icon: annotation.icon,
series: data.map(s => {
return [
s[0],
s[1].map(doc => {
const vars = replaceVars(annotation.template, null, doc);
seriesModel.forEach(seriesGroup => {
const isStackedWithinSeries = seriesGroup.stacked === STACKED_OPTIONS.STACKED_WITHIN_SERIES;
const hasSeparateAxis = Boolean(seriesGroup.separate_axis);
const groupId = hasSeparateAxis || isStackedWithinSeries ? seriesGroup.id : mainAxisGroupId;
const domain = hasSeparateAxis
? TimeseriesVisualization.getYAxisDomain(seriesGroup)
: undefined;
const isCustomDomain = groupId !== mainAxisGroupId;
const seriesGroupTickFormatter = TimeseriesVisualization.getTickFormatter(
seriesGroup,
this.props.getConfig
);
const yScaleType = hasSeparateAxis
? TimeseriesVisualization.getAxisScaleType(seriesGroup)
: mainAxisScaleType;
if (vars instanceof Error) {
this.showToastNotification = vars.error.caused_by;
if (seriesGroup.stacked === STACKED_OPTIONS.PERCENT) {
seriesGroup.separate_axis = true;
seriesGroup.axisFormatter = 'percent';
seriesGroup.axis_min = seriesGroup.axis_min || 0;
seriesGroup.axis_max = seriesGroup.axis_max || 1;
seriesGroup.axis_position = model.axis_position;
}
return annotation.template;
}
return vars;
}),
];
}),
};
});
}
const seriesModel = model.series.map(s => _.cloneDeep(s));
const firstSeries = seriesModel.find(s => s.formatter && !s.separate_axis);
const formatter = tickFormatter(
_.get(firstSeries, 'formatter'),
_.get(firstSeries, 'value_template'),
this.props.getConfig
);
const mainAxis = {
position: model.axis_position,
tickFormatter: formatter,
axisFormatter: _.get(firstSeries, 'formatter', 'number'),
axisFormatterTemplate: _.get(firstSeries, 'value_template'),
};
if (model.axis_min) mainAxis.min = model.axis_min;
if (model.axis_max) mainAxis.max = model.axis_max;
if (model.axis_scale === 'log') {
mainAxis.mode = 'log';
mainAxis.transform = value => (value > 0 ? Math.log(value) / Math.LN10 : null);
mainAxis.inverseTransform = value => Math.pow(10, value);
}
const yaxes = [mainAxis];
seriesModel.forEach(s => {
series
.filter(r => _.startsWith(r.id, s.id))
.forEach(
r =>
(r.tickFormatter = tickFormatter(s.formatter, s.value_template, this.props.getConfig))
);
.filter(r => startsWith(r.id, seriesGroup.id))
.forEach(seriesDataRow => {
seriesDataRow.tickFormatter = seriesGroupTickFormatter;
seriesDataRow.groupId = groupId;
seriesDataRow.yScaleType = yScaleType;
seriesDataRow.hideInLegend = Boolean(seriesGroup.hide_in_legend);
seriesDataRow.useDefaultGroupDomain = !isCustomDomain;
});
if (s.hide_in_legend) {
series.filter(r => _.startsWith(r.id, s.id)).forEach(r => delete r.label);
}
if (s.stacked !== 'none') {
series
.filter(r => _.startsWith(r.id, s.id))
.forEach(row => {
row.data = row.data.map(point => {
if (!point[1]) return [point[0], 0];
return point;
});
});
}
if (s.stacked === 'percent') {
s.separate_axis = true;
s.axisFormatter = 'percent';
s.axis_min = 0;
s.axis_max = 1;
s.axis_position = model.axis_position;
const seriesData = series.filter(r => _.startsWith(r.id, s.id));
const first = seriesData[0];
if (first) {
first.data.forEach((row, index) => {
const rowSum = seriesData.reduce((acc, item) => {
return item.data[index][1] + acc;
}, 0);
seriesData.forEach(item => {
item.data[index][1] = (rowSum && item.data[index][1] / rowSum) || 0;
});
});
}
if (isCustomDomain) {
TimeseriesVisualization.addYAxis(yAxis, {
domain,
groupId,
id: yAxisIdGenerator(seriesGroup.id),
position: seriesGroup.axis_position,
hide: isStackedWithinSeries,
tickFormatter:
seriesGroup.stacked === STACKED_OPTIONS.PERCENT
? this.yAxisStackedByPercentFormatter
: seriesGroupTickFormatter,
});
} else if (!mainDomainAdded) {
TimeseriesVisualization.addYAxis(yAxis, {
tickFormatter,
id: yAxisIdGenerator('main'),
groupId: mainAxisGroupId,
position: model.axis_position,
domain: mainAxisDomain,
});
mainDomainAdded = true;
}
});
const interval = this.getInterval();
let axisCount = 1;
if (seriesModel.some(hasSeparateAxis)) {
seriesModel.forEach(row => {
if (row.separate_axis) {
axisCount++;
const formatter = tickFormatter(row.formatter, row.value_template, this.props.getConfig);
const yaxis = {
alignTicksWithAxis: 1,
position: row.axis_position,
tickFormatter: formatter,
axisFormatter: row.axis_formatter,
axisFormatterTemplate: row.value_template,
};
if (row.axis_min != null) yaxis.min = row.axis_min;
if (row.axis_max != null) yaxis.max = row.axis_max;
yaxes.push(yaxis);
// Assign axis and formatter to each series
series
.filter(r => _.startsWith(r.id, row.id))
.forEach(r => {
r.yaxis = axisCount;
});
}
});
}
const panelBackgroundColor = model.background_color || backgroundColor;
const style = { backgroundColor: panelBackgroundColor };
const params = {
dateFormat: this.props.dateFormat,
crosshair: true,
tickFormatter: formatter,
legendPosition: model.legend_position || 'right',
backgroundColor: panelBackgroundColor,
series,
annotations,
yaxes,
showGrid: Boolean(model.show_grid),
legend: Boolean(model.show_legend),
xAxisFormatter: this.xaxisFormatter,
onBrush: ranges => {
if (this.props.onBrush) this.props.onBrush(ranges);
},
};
if (interval) {
params.xaxisLabel = getAxisLabelString(interval);
}
return (
<div className="tvbVis" style={style}>
<Timeseries {...params} />
<div className="tvbVis" style={styles.tvbVis}>
<TimeSeries
series={series}
yAxis={yAxis}
onBrush={onBrush}
enableHistogramMode={enableHistogramMode}
isDarkMode={isBackgroundDark(model.background_color)}
showGrid={Boolean(model.show_grid)}
legend={Boolean(model.show_legend)}
legendPosition={model.legend_position}
xAxisLabel={getAxisLabelString(interval)}
xAxisFormatter={this.xAxisFormatter(interval)}
annotations={this.prepareAnnotations()}
/>
</div>
);
}
}
TimeseriesVisualization.propTypes = {
backgroundColor: PropTypes.string,
className: PropTypes.string,
model: PropTypes.object,
onBrush: PropTypes.func,
onChange: PropTypes.func,
visData: PropTypes.object,
dateFormat: PropTypes.string,
getConfig: PropTypes.func,
};

View file

@ -17,8 +17,8 @@
* under the License.
*/
import { tickFormatter } from '../../lib/tick_formatter';
import { TopN } from '../../../visualizations/components/top_n';
import { createTickFormatter } from '../../lib/tick_formatter';
import { TopN } from '../../../visualizations/views/top_n';
import { getLastValue } from '../../../../common/get_last_value';
import { isBackgroundInverted } from '../../../../common/set_is_reversed';
import { replaceVars } from '../../lib/replace_vars';
@ -54,7 +54,7 @@ export function TopNVisualization(props) {
const id = first(item.id.split(/:/));
const seriesConfig = model.series.find(s => s.id === id);
if (seriesConfig) {
const formatter = tickFormatter(
const tickFormatter = createTickFormatter(
seriesConfig.formatter,
seriesConfig.value_template,
props.getConfig
@ -73,7 +73,7 @@ export function TopNVisualization(props) {
return {
...item,
color,
tickFormatter: formatter,
tickFormatter,
};
}
return item;

View file

@ -17,4 +17,4 @@
@import './components/index';
// Visualizations
@import './visualizations/components/index';
@import './visualizations/views/index';

View file

@ -19,10 +19,12 @@
import moment from 'moment';
export const brushHandler = timefilter => ranges => {
const TIME_MODE = 'absolute';
export const createBrushHandler = timefilter => (from, to) => {
timefilter.setTime({
from: moment(ranges.xaxis.from).toISOString(),
to: moment(ranges.xaxis.to).toISOString(),
mode: 'absolute',
from: moment(from).toISOString(),
to: moment(to).toISOString(),
mode: TIME_MODE,
});
};

View file

@ -17,14 +17,12 @@
* under the License.
*/
import { brushHandler } from '../create_brush_handler';
import { createBrushHandler } from './create_brush_handler';
import moment from 'moment';
import { expect } from 'chai';
describe('brushHandler', () => {
let mockTimefilter;
let onBrush;
let range;
beforeEach(() => {
mockTimefilter = {
@ -33,14 +31,15 @@ describe('brushHandler', () => {
this.time = time;
},
};
onBrush = brushHandler(mockTimefilter);
onBrush = createBrushHandler(mockTimefilter);
});
test('returns brushHandler() that updates timefilter', () => {
range = { xaxis: { from: '2017-01-01T00:00:00Z', to: '2017-01-01T00:10:00Z' } };
onBrush(range);
expect(mockTimefilter.time.from).to.equal(moment(range.xaxis.from).toISOString());
expect(mockTimefilter.time.to).to.equal(moment(range.xaxis.to).toISOString());
expect(mockTimefilter.time.mode).to.equal('absolute');
it('returns brushHandler() that updates timefilter', () => {
const from = '2017-01-01T00:00:00Z';
const to = '2017-01-01T00:10:00Z';
onBrush(from, to);
expect(mockTimefilter.time.from).toEqual(moment(from).toISOString());
expect(mockTimefilter.time.to).toEqual(moment(to).toISOString());
expect(mockTimefilter.time.mode).toEqual('absolute');
});
});

View file

@ -1,92 +0,0 @@
// LEGEND
.tvbLegend {
@include euiFontSizeXS;
display: flex;
width: 200px;
padding: $euiSizeXS 0;
overflow: auto;
}
.tvbLegend__toggle {
align-self: flex-start;
color: $tvbValueColor;
.tvbVisTimeSeries--reversed & {
color: $tvbValueColorReversed;
}
}
.tvbLegend__series {
flex-grow: 1;
}
.tvbLegend__item {
cursor: pointer;
padding: $euiSizeXS;
border-bottom: 1px solid $tvbLineColor;
display: flex;
max-width: 170px;
&.disabled {
opacity: .5;
}
&:first-child {
border-top: 1px solid $tvbLineColor;
}
.tvbVisTimeSeries--reversed & {
border-color: $tvbLineColorReversed;
}
}
.tvbLegend__button {
text-align: left;
display: flex;
width: 100%;
}
.tvbLegend__itemLabel {
@include euiTextTruncate;
flex-grow: 1;
span {
color: $euiTextColor;
margin-left: $euiSizeXS;
.tvbVisTimeSeries--reversed & {
color: $tvbTextColorReversed;
}
}
}
.tvbLegend__itemValue {
font-weight: $euiFontWeightSemiBold;
color: $tvbValueColor;
margin-left: $euiSizeXS;
.tvbVisTimeSeries--reversed & {
color: $tvbValueColorReversed;
}
}
.tvbLegend--horizontal {
width: auto;
display: flex;
.tvbLegend__series {
display: flex;
flex-wrap: wrap;
}
.tvbLegend__item {
max-width: inherit;
margin-right: $euiSizeM;
border: none;
}
.tvbLegend__itemLabel {
flex: 0 1 auto;
}
}

View file

@ -1,132 +0,0 @@
@import '@elastic/eui/src/components/tool_tip/variables';
@import '@elastic/eui/src/components/tool_tip/mixins';
.tvbVisTimeSeries {
position: relative;
display: flex;
flex-direction: column;
flex: 1 0 auto;
}
.tvbVisTimeSeries__content {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
flex: 1 0 auto;
// TODO: Remove once tooltips are portaled
overflow: visible; // Ensures the tooltip doesn't cause scrollbars
}
.tvbVisTimeSeries__visualization {
cursor: crosshair;
display: flex;
flex-direction: column;
flex: 1 0 auto;
position: relative;
> .tvbVisTimeSeries__container {
min-width: 1px;
width: 100%;
height: 100%;
}
}
.tvbVisTimeSeries__container {
@include euiFontSizeXS;
position: relative;
display: flex;
flex-direction: column;
flex: 1 0 auto;
}
.tvbVisTimeSeries__axisLabel {
font-weight: $euiFontWeightBold;
color: $tvbTextColor;
text-align: center;
min-height: 1.2em;
&.tvbVisTimeSeries__axisLabel--reversed {
color: $tvbTextColorReversed;
}
}
// TOOLTIP
// EUITODO: Use EuiTooltip or somehow portal the current one
.tvbTooltip__container {
pointer-events: none;
position: absolute;
z-index: $euiZLevel9;
display: flex;
align-items: center;
padding: 0 $euiSizeS;
transform: translate(0, -50%);
}
.tvbTooltip__container--right {
flex-direction: row-reverse;
}
.tvbTooltip {
@include euiToolTipStyle;
@include euiFontSizeXS;
padding: $euiSizeS;
}
.tvbTooltip__caret {
$tempArrowSize: $euiSizeM;
width: $tempArrowSize;
height: $tempArrowSize;
border-radius: $euiBorderRadius / 2;
background-color: tintOrShade($euiColorFullShade, 25%, 90%);
transform-origin: center;
transform: rotateZ(45deg);
.tvbTooltip__container--left & {
margin-right: (($tempArrowSize/2) + 1px) * -1;
}
.tvbTooltip__container--right & {
margin-left: (($tempArrowSize/2) + 1px) * -1;
}
}
.tvbTooltip__item {
display: flex;
}
/**
* 1. Ensure tvbTooltip__label text wraps nicely.
* 2. Create consistent space between the dot icon and the label.
*/
.tvbTooltip__labelContainer {
display: flex;
flex-wrap: wrap;
flex-grow: 1;
min-width: 1px; /* 1 */
margin-left: $euiSizeXS; /* 2 */
}
/**
* 1. Ensure text wraps nicely.
*/
.tvbTooltip__label {
flex-grow: 1;
margin-right: $euiSizeXS;
word-wrap: break-word; /* 1 */
overflow-wrap: break-word; /* 1 */
min-width: 1px; /* 1 */
}
.tvbTooltip__icon,
.tvbTooltip__value {
flex-shrink: 0;
}
.tvbTooltip__timestamp {
color: transparentize($euiColorGhost, .3);
white-space: nowrap;
}

View file

@ -1,346 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { findDOMNode } from 'react-dom';
import _ from 'lodash';
import $ from 'ui/flot-charts';
import { eventBus } from '../lib/events';
import { Resize } from './resize';
import { calculateBarWidth } from '../lib/calculate_bar_width';
import { calculateFillColor } from '../lib/calculate_fill_color';
import { COLORS } from '../lib/colors';
export class FlotChart extends Component {
constructor(props) {
super(props);
this.handleResize = this.handleResize.bind(this);
}
shouldComponentUpdate(props) {
if (!this.plot) return true;
if (props.reversed !== this.props.reversed) {
return true;
}
// if the grid changes we need to re-render
if (props.showGrid !== this.props.showGrid) return true;
if (props.yaxes && this.props.yaxes) {
// We need to rerender if the axis change
const valuesChanged = props.yaxes.some((axis, i) => {
if (this.props.yaxes[i]) {
return (
axis.position !== this.props.yaxes[i].position ||
axis.max !== this.props.yaxes[i].max ||
axis.min !== this.props.yaxes[i].min ||
axis.axisFormatter !== this.props.yaxes[i].axisFormatter ||
axis.mode !== this.props.yaxes[i].mode ||
axis.axisFormatterTemplate !== this.props.yaxes[i].axisFormatterTemplate
);
}
});
if (props.yaxes.length !== this.props.yaxes.length || valuesChanged) {
return true;
}
}
return false;
}
shutdownChart() {
if (!this.plot) return;
$(this.target).off('plothover', this.props.plothover);
if (this.props.onMouseOver) $(this.target).off('plothover', this.handleMouseOver);
if (this.props.onMouseLeave) $(this.target).off('mouseleave', this.handleMouseLeave);
if (this.props.onBrush) $(this.target).off('plotselected', this.brushChart);
this.plot.shutdown();
if (this.props.crosshair) {
$(this.target).off('plothover', this.handlePlotover);
eventBus.off('thorPlotover', this.handleThorPlotover);
eventBus.off('thorPlotleave', this.handleThorPlotleave);
}
}
componentWillUnmount() {
this.shutdownChart();
}
filterByShow(show) {
if (show) {
return metric => {
return show.some(id => _.startsWith(id, metric.id));
};
}
return () => true;
}
componentWillReceiveProps(newProps) {
if (this.plot) {
const { series } = newProps;
const options = this.plot.getOptions();
_.set(options, 'series.bars.barWidth', calculateBarWidth(series));
_.set(options, 'xaxes[0].ticks', this.calculateTicks());
this.plot.setData(this.calculateData(series, newProps.show));
this.plot.setupGrid();
this.plot.draw();
if (!_.isEqual(this.props.series, newProps.series)) this.handleDraw(this.plot);
} else {
this.renderChart();
}
}
componentDidMount() {
this.renderChart();
}
componentDidUpdate() {
this.shutdownChart();
this.renderChart();
}
calculateData(data, show) {
return _(data)
.filter(this.filterByShow(show))
.map(set => {
if (_.isPlainObject(set)) {
return {
...set,
lines: this.computeColor(set.lines, set.color),
bars: this.computeColor(set.bars, set.color),
};
}
return {
color: '#990000',
data: set,
};
})
.reverse()
.value();
}
computeColor(style, color) {
if (style && style.show) {
const { fill, fillColor } = calculateFillColor(color, style.fill);
return {
...style,
fill,
fillColor,
};
}
return style;
}
handleDraw(plot) {
if (this.props.onDraw) this.props.onDraw(plot);
}
getOptions(props) {
const yaxes = props.yaxes || [{}];
const lineColor = COLORS.lineColor;
const textColor = props.reversed ? COLORS.textColorReversed : COLORS.textColor;
const borderWidth = { bottom: 1, top: 0, left: 0, right: 0 };
if (yaxes.some(y => y.position === 'left')) borderWidth.left = 1;
if (yaxes.some(y => y.position === 'right')) borderWidth.right = 1;
if (props.showGrid) {
borderWidth.top = 1;
borderWidth.left = 1;
borderWidth.right = 1;
}
const opts = {
legend: { show: false },
yaxes: yaxes.map(axis => {
axis.tickLength = props.showGrid ? null : 0;
return axis;
}),
yaxis: {
color: lineColor,
font: { color: textColor, size: 11 },
tickFormatter: props.tickFormatter,
},
xaxis: {
tickLength: props.showGrid ? null : 0,
color: lineColor,
timezone: 'browser',
mode: 'time',
font: { color: textColor, size: 11 },
ticks: this.calculateTicks(),
},
series: {
shadowSize: 0,
},
grid: {
margin: 0,
borderWidth,
borderColor: lineColor,
hoverable: true,
mouseActiveRadius: 200,
},
};
if (props.crosshair) {
_.set(opts, 'crosshair', {
mode: 'x',
color: '#C66',
lineWidth: 1,
});
}
if (props.onBrush) {
_.set(opts, 'selection', { mode: 'x', color: textColor });
}
if (props.xAxisFormatter) {
_.set(opts, 'xaxis.tickFormatter', props.xAxisFormatter);
}
_.set(opts, 'series.bars.barWidth', calculateBarWidth(props.series));
return _.assign(opts, props.options);
}
calculateTicks() {
const sample = this.props.xAxisFormatter(new Date());
const tickLetterWidth = 7;
const tickPadding = 45;
const ticks = Math.floor(
this.target.clientWidth / (sample.length * tickLetterWidth + tickPadding)
);
return ticks;
}
handleResize() {
const resize = findDOMNode(this.resize);
if (!this.rendered) {
this.renderChart();
return;
}
if (resize && resize.clientHeight > 0 && resize.clientHeight > 0) {
if (!this.plot) return;
const options = this.plot.getOptions();
_.set(options, 'xaxes[0].ticks', this.calculateTicks());
this.plot.resize();
this.plot.setupGrid();
this.plot.draw();
this.handleDraw(this.plot);
}
}
renderChart() {
const resize = findDOMNode(this.resize);
if (resize.clientWidth > 0 && resize.clientHeight > 0) {
this.rendered = true;
const { series } = this.props;
const data = this.calculateData(series, this.props.show);
this.plot = $.plot(this.target, data, this.getOptions(this.props));
this.handleDraw(this.plot);
_.defer(() => this.handleResize());
this.handleMouseOver = (...args) => {
if (this.props.onMouseOver) this.props.onMouseOver(...args, this.plot);
};
this.handleMouseLeave = (...args) => {
if (this.props.onMouseLeave) this.props.onMouseLeave(...args, this.plot);
};
$(this.target).on('plothover', this.handleMouseOver);
$(this.target).on('mouseleave', this.handleMouseLeave);
if (this.props.crosshair) {
this.handleThorPlotover = (e, pos, item, originalPlot) => {
if (this.plot !== originalPlot) {
this.plot.setCrosshair({ x: _.get(pos, 'x') });
this.props.plothover(e, pos, item);
}
};
this.handlePlotover = (e, pos, item) =>
eventBus.trigger('thorPlotover', [pos, item, this.plot]);
this.handlePlotleave = () => eventBus.trigger('thorPlotleave');
this.handleThorPlotleave = e => {
if (this.plot) this.plot.clearCrosshair();
if (this.props.plothover) this.props.plothover(e);
};
$(this.target).on('plothover', this.handlePlotover);
$(this.target).on('mouseleave', this.handlePlotleave);
eventBus.on('thorPlotover', this.handleThorPlotover);
eventBus.on('thorPlotleave', this.handleThorPlotleave);
}
if (_.isFunction(this.props.plothover)) {
$(this.target).bind('plothover', this.props.plothover);
}
$(this.target).on('mouseleave', () => {
eventBus.trigger('thorPlotleave');
});
if (_.isFunction(this.props.onBrush)) {
this.brushChart = (e, ranges) => {
this.props.onBrush(ranges);
this.plot.clearSelection();
};
$(this.target).on('plotselected', this.brushChart);
}
}
}
render() {
return (
<Resize
onResize={this.handleResize}
ref={el => (this.resize = el)}
className="tvbVisTimeSeries__container"
>
<div ref={el => (this.target = el)} className="tvbVisTimeSeries__container" />
</Resize>
);
}
}
FlotChart.defaultProps = {
showGrid: true,
};
FlotChart.propTypes = {
crosshair: PropTypes.bool,
onBrush: PropTypes.func,
onPlotCreate: PropTypes.func,
onMouseOver: PropTypes.func,
onMouseLeave: PropTypes.func,
options: PropTypes.object,
plothover: PropTypes.func,
reversed: PropTypes.bool,
series: PropTypes.array,
show: PropTypes.array,
tickFormatter: PropTypes.func,
showGrid: PropTypes.bool,
yaxes: PropTypes.array,
};

View file

@ -1,80 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import { createLegendSeries } from '../lib/create_legend_series';
import reactcss from 'reactcss';
import { htmlIdGenerator, EuiButtonIcon } from '@elastic/eui';
import { injectI18n } from '@kbn/i18n/react';
export const HorizontalLegend = injectI18n(function(props) {
const rows = props.series.map(createLegendSeries(props));
const htmlId = htmlIdGenerator();
const styles = reactcss(
{
hideLegend: {
legend: {
display: 'none',
},
},
},
{ hideLegend: !props.showLegend }
);
let legendToggleIcon = 'arrowDown';
if (!props.showLegend) {
legendToggleIcon = 'arrowUp';
}
return (
<div className="tvbLegend tvbLegend--horizontal">
<EuiButtonIcon
className="tvbLegend__toggle"
iconType={legendToggleIcon}
color="text"
iconSize="s"
onClick={props.onClick}
aria-label={props.intl.formatMessage({
id: 'tsvb.horizontalLegend.toggleChartAriaLabel',
defaultMessage: 'Toggle chart legend',
})}
title={props.intl.formatMessage({
id: 'tsvb.horizontalLegend.toggleChartAriaLabel',
defaultMessage: 'Toggle chart legend',
})}
aria-expanded={!!props.showLegend}
aria-controls={htmlId('legend')}
/>
<div className="tvbLegend__series" style={styles.legend} id={htmlId('legend')}>
{rows}
</div>
</div>
);
});
HorizontalLegend.propTypes = {
legendPosition: PropTypes.string,
onClick: PropTypes.func,
onToggle: PropTypes.func,
series: PropTypes.array,
showLegend: PropTypes.bool,
seriesValues: PropTypes.object,
seriesFilter: PropTypes.array,
tickFormatter: PropTypes.func,
};

View file

@ -1,41 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import { VerticalLegend } from './vertical_legend';
import { HorizontalLegend } from './horizontal_legend';
export function Legend(props) {
if (props.legendPosition === 'bottom') {
return <HorizontalLegend {...props} />;
}
return <VerticalLegend {...props} />;
}
Legend.propTypes = {
legendPosition: PropTypes.string,
onClick: PropTypes.func,
onToggle: PropTypes.func,
series: PropTypes.array,
showLegend: PropTypes.bool,
seriesValues: PropTypes.object,
seriesFilter: PropTypes.array,
tickFormatter: PropTypes.func,
};

View file

@ -1,81 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { findDOMNode } from 'react-dom';
export class Resize extends Component {
constructor(props) {
super(props);
this.state = {};
this.handleResize = this.handleResize.bind(this);
}
checkSize() {
const el = findDOMNode(this.el);
if (!el) return;
this.timeout = setTimeout(() => {
const { currentHeight, currentWidth } = this.state;
if (
currentHeight !== el.parentNode.clientHeight ||
currentWidth !== el.parentNode.clientWidth
) {
this.setState({
currentWidth: el.parentNode.clientWidth,
currentHeight: el.parentNode.clientHeight,
});
this.handleResize();
}
clearTimeout(this.timeout);
this.checkSize();
}, this.props.frequency);
}
componentDidMount() {
this.checkSize();
}
componentWillUnmount() {
clearTimeout(this.timeout);
}
handleResize() {
if (this.props.onResize) this.props.onResize();
}
render() {
const style = this.props.style || {};
const className = this.props.className || '';
return (
<div style={style} className={className} ref={el => (this.el = el)}>
{this.props.children}
</div>
);
}
}
Resize.defaultProps = {
frequency: 500,
};
Resize.propTypes = {
frequency: PropTypes.number,
onResize: PropTypes.func,
};

View file

@ -1,201 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import classNames from 'classnames';
import _ from 'lodash';
import { getLastValue } from '../../../common/get_last_value';
import { isBackgroundInverted } from '../../../common/set_is_reversed';
import { TimeseriesChart } from './timeseries_chart';
import { Legend } from './legend';
import { eventBus } from '../lib/events';
import reactcss from 'reactcss';
export class Timeseries extends Component {
constructor(props) {
super(props);
const values = this.getLastValues(props);
this.state = {
showLegend: props.legend != null ? props.legend : true,
values: values || {},
show: _.keys(values) || [],
ignoreLegendUpdates: false,
ignoreVisibilityUpdates: false,
};
this.toggleFilter = this.toggleFilter.bind(this);
this.handleHideClick = this.handleHideClick.bind(this);
this.plothover = this.plothover.bind(this);
}
filterLegend(id) {
if (!_.has(this.state.values, id)) return [];
const notAllShown = _.keys(this.state.values).length !== this.state.show.length;
const isCurrentlyShown = _.includes(this.state.show, id);
const show = [];
if (notAllShown && isCurrentlyShown) {
this.setState({ ignoreVisibilityUpdates: false, show: Object.keys(this.state.values) });
} else {
show.push(id);
this.setState({ ignoreVisibilityUpdates: true, show: [id] });
}
return show;
}
toggleFilter(event, id) {
const show = this.filterLegend(id);
if (_.isFunction(this.props.onFilter)) {
this.props.onFilter(show);
}
eventBus.trigger('toggleFilter', id, this);
}
getLastValues(props) {
const values = {};
props.series.forEach(row => {
// we need a valid identifier
if (!row.id) row.id = row.label;
values[row.id] = getLastValue(row.data);
});
return values;
}
updateLegend(pos, item) {
const values = {};
if (pos) {
this.props.series.forEach(row => {
if (row.data && Array.isArray(row.data)) {
if (
item &&
row.data[item.dataIndex] &&
row.data[item.dataIndex][0] === item.datapoint[0]
) {
values[row.id] = row.data[item.dataIndex][1];
} else {
let closest;
for (let i = 0; i < row.data.length; i++) {
closest = i;
if (row.data[i] && pos.x < row.data[i][0]) break;
}
if (!row.data[closest]) return (values[row.id] = null);
const [, value] = row.data[closest];
values[row.id] = (value != null && value) || null;
}
}
});
} else {
_.assign(values, this.getLastValues(this.props));
}
this.setState({ values });
}
componentWillReceiveProps(props) {
if (props.legend !== this.props.legend) this.setState({ showLegend: props.legend });
if (!this.state.ignoreLegendUpdates) {
const values = this.getLastValues(props);
const currentKeys = _.keys(this.state.values);
const keys = _.keys(values);
const diff = _.difference(keys, currentKeys);
const nextState = { values: values };
if (diff.length && !this.state.ignoreVisibilityUpdates) {
nextState.show = keys;
}
this.setState(nextState);
}
}
plothover(event, pos, item) {
this.updateLegend(pos, item);
}
handleHideClick() {
this.setState({ showLegend: !this.state.showLegend });
}
render() {
const classes = classNames('tvbVisTimeSeries', {
'tvbVisTimeSeries--reversed': isBackgroundInverted(this.props.backgroundColor),
});
const styles = reactcss(
{
bottomLegend: {
content: {
flexDirection: 'column',
},
},
},
{ bottomLegend: this.props.legendPosition === 'bottom' }
);
return (
<div className={classes} data-test-subj="timeseriesChart">
<div style={styles.content} className="tvbVisTimeSeries__content">
<div className="tvbVisTimeSeries__visualization">
<TimeseriesChart
dateFormat={this.props.dateFormat}
crosshair={this.props.crosshair}
onBrush={this.props.onBrush}
plothover={this.plothover}
backgroundColor={this.props.backgroundColor}
series={this.props.series}
annotations={this.props.annotations}
show={this.state.show}
showGrid={this.props.showGrid}
tickFormatter={this.props.tickFormatter}
options={this.props.options}
xaxisLabel={this.props.xaxisLabel}
yaxes={this.props.yaxes}
xAxisFormatter={this.props.xAxisFormatter}
/>
</div>
<Legend
legendPosition={this.props.legendPosition}
onClick={this.handleHideClick}
onToggle={this.toggleFilter}
series={this.props.series}
showLegend={this.state.showLegend}
seriesValues={this.state.values}
seriesFilter={this.state.show}
tickFormatter={this.props.tickFormatter}
/>
</div>
</div>
);
}
}
Timeseries.defaultProps = {
legend: true,
showGrid: true,
};
Timeseries.propTypes = {
legend: PropTypes.bool,
legendPosition: PropTypes.string,
onFilter: PropTypes.func,
series: PropTypes.array,
annotations: PropTypes.array,
backgroundColor: PropTypes.string,
options: PropTypes.object,
tickFormatter: PropTypes.func,
showGrid: PropTypes.bool,
xaxisLabel: PropTypes.string,
dateFormat: PropTypes.string,
};

View file

@ -1,229 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import classNames from 'classnames';
import { isBackgroundInverted, isBackgroundDark } from '../../../common/set_is_reversed';
import moment from 'moment';
import reactcss from 'reactcss';
import { FlotChart } from './flot_chart';
import { Annotation } from './annotation';
import { EuiIcon } from '@elastic/eui';
export function scaleUp(value) {
return window.devicePixelRatio * value;
}
export function scaleDown(value) {
return value / window.devicePixelRatio;
}
export class TimeseriesChart extends Component {
constructor(props) {
super(props);
this.state = {
annotations: [],
showTooltip: false,
mouseHoverTimer: false,
};
this.handleMouseLeave = this.handleMouseLeave.bind(this);
this.handleMouseOver = this.handleMouseOver.bind(this);
this.renderAnnotations = this.renderAnnotations.bind(this);
this.handleDraw = this.handleDraw.bind(this);
}
calculateLeftRight(item, plot) {
const canvas = plot.getCanvas();
const point = plot.pointOffset({ x: item.datapoint[0], y: item.datapoint[1] });
const edge = (scaleUp(point.left) + 10) / canvas.width;
let right;
let left;
if (edge > 0.5) {
right = scaleDown(canvas.width) - point.left;
left = null;
} else {
right = null;
left = point.left;
}
return [left, right];
}
handleDraw(plot) {
if (!plot || !this.props.annotations) return;
const annotations = this.props.annotations.reduce((acc, anno) => {
return acc.concat(
anno.series.map(series => {
return {
series,
plot,
key: `${anno.id}-${series[0]}`,
icon: anno.icon,
color: anno.color,
};
})
);
}, []);
this.setState({ annotations });
}
handleMouseOver(e, pos, item, plot) {
if (typeof this.state.mouseHoverTimer === 'number') {
window.clearTimeout(this.state.mouseHoverTimer);
}
if (item) {
const plotOffset = plot.getPlotOffset();
const point = plot.pointOffset({ x: item.datapoint[0], y: item.datapoint[1] });
const [left, right] = this.calculateLeftRight(item, plot);
const top = point.top;
this.setState({
showTooltip: true,
item,
left,
right,
top: top + 10,
bottom: plotOffset.bottom,
});
}
}
handleMouseLeave() {
this.state.mouseHoverTimer = window.setTimeout(() => {
this.setState({ showTooltip: false });
}, 250);
}
renderAnnotations(annotation) {
return (
<Annotation
series={annotation.series}
plot={annotation.plot}
key={annotation.key}
icon={annotation.icon}
color={annotation.color}
/>
);
}
render() {
const { item, right, top, left } = this.state;
const { series } = this.props;
let tooltip;
const styles = reactcss(
{
showTooltip: {
tooltipContainer: {
top: top - 8,
left,
right,
},
},
hideTooltip: {
tooltipContainer: { display: 'none' },
},
},
{
showTooltip: this.state.showTooltip,
hideTooltip: !this.state.showTooltip,
}
);
if (item) {
const metric = series.find(r => r.id === item.series.id);
const formatter = (metric && metric.tickFormatter) || this.props.tickFormatter || (v => v);
const value = item.datapoint[2] ? item.datapoint[1] - item.datapoint[2] : item.datapoint[1];
tooltip = (
<div
className={`tvbTooltip__container tvbTooltip__container--${right ? 'right' : 'left'}`}
style={styles.tooltipContainer}
>
<span className="tvbTooltip__caret" />
<div className="tvbTooltip">
<div className="tvbTooltip__timestamp">
{moment(item.datapoint[0]).format(this.props.dateFormat)}
</div>
<div className="tvbTooltip__item">
<EuiIcon className="tvbTooltip__icon" type="dot" color={item.series.color} />
<div className="tvbTooltip__labelContainer">
<div className="tvbTooltip__label">{item.series.label}</div>
<div className="tvbTooltip__value">{formatter(value)}</div>
</div>
</div>
</div>
</div>
);
}
const params = {
crosshair: this.props.crosshair,
onPlotCreate: this.handlePlotCreate,
onBrush: this.props.onBrush,
onMouseLeave: this.handleMouseLeave,
onMouseOver: this.handleMouseOver,
onDraw: this.handleDraw,
options: this.props.options,
plothover: this.props.plothover,
reversed: isBackgroundDark(this.props.backgroundColor),
series: this.props.series,
annotations: this.props.annotations,
showGrid: this.props.showGrid,
show: this.props.show,
tickFormatter: this.props.tickFormatter,
yaxes: this.props.yaxes,
xAxisFormatter: this.props.xAxisFormatter,
};
const annotations = this.state.annotations.map(this.renderAnnotations);
const axisLabelClass = classNames('tvbVisTimeSeries__axisLabel', {
'tvbVisTimeSeries__axisLabel--reversed': isBackgroundInverted(this.props.backgroundColor),
});
return (
<div ref={el => (this.container = el)} className="tvbVisTimeSeries__container">
{tooltip}
{annotations}
<FlotChart {...params} />
<div className={axisLabelClass}>{this.props.xaxisLabel}</div>
</div>
);
}
}
TimeseriesChart.defaultProps = {
showGrid: true,
dateFormat: 'll LTS',
};
TimeseriesChart.propTypes = {
crosshair: PropTypes.bool,
onBrush: PropTypes.func,
options: PropTypes.object,
plothover: PropTypes.func,
backgroundColor: PropTypes.string,
series: PropTypes.array,
annotations: PropTypes.array,
show: PropTypes.array,
tickFormatter: PropTypes.func,
yaxes: PropTypes.array,
showGrid: PropTypes.bool,
xaxisLabel: PropTypes.string,
dateFormat: PropTypes.string,
};

View file

@ -1,91 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import { createLegendSeries } from '../lib/create_legend_series';
import reactcss from 'reactcss';
import { htmlIdGenerator, EuiButtonIcon } from '@elastic/eui';
import { injectI18n } from '@kbn/i18n/react';
export const VerticalLegend = injectI18n(function(props) {
const rows = props.series.map(createLegendSeries(props));
const htmlId = htmlIdGenerator();
const hideLegend = !props.showLegend;
const leftLegend = props.legendPosition === 'left';
const styles = reactcss(
{
default: {
legend: { width: 200 },
},
leftLegend: {
legend: { order: '-1' },
control: { order: '2' },
},
hideLegend: {
legend: { width: 24 },
series: { display: 'none' },
},
},
{ hideLegend, leftLegend }
);
const openIcon = leftLegend ? 'arrowRight' : 'arrowLeft';
const closeIcon = leftLegend ? 'arrowLeft' : 'arrowRight';
const legendToggleIcon = hideLegend ? `${openIcon}` : `${closeIcon}`;
return (
<div className="tvbLegend" style={styles.legend}>
<EuiButtonIcon
className="tvbLegend__toggle"
style={styles.control}
iconType={legendToggleIcon}
color="text"
iconSize="s"
onClick={props.onClick}
aria-label={props.intl.formatMessage({
id: 'tsvb.verticalLegend.toggleChartAriaLabel',
defaultMessage: 'Toggle chart legend',
})}
title={props.intl.formatMessage({
id: 'tsvb.verticalLegend.toggleChartAriaLabel',
defaultMessage: 'Toggle chart legend',
})}
aria-expanded={!!props.showLegend}
aria-controls={htmlId('legend')}
/>
<div className="tvbLegend__series" style={styles.series} id={htmlId('legend')}>
{rows}
</div>
</div>
);
});
VerticalLegend.propTypes = {
legendPosition: PropTypes.string,
onClick: PropTypes.func,
onToggle: PropTypes.func,
series: PropTypes.array,
showLegend: PropTypes.bool,
seriesValues: PropTypes.object,
seriesFilter: PropTypes.array,
tickFormatter: PropTypes.func,
};

View file

@ -0,0 +1,41 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export const COLORS = {
LINE_COLOR: 'rgba(105,112,125,0.2)',
TEXT_COLOR: 'rgba(0,0,0,0.4)',
TEXT_COLOR_REVERSED: 'rgba(255,255,255,0.5)',
VALUE_COLOR: 'rgba(0,0,0,0.7)',
VALUE_COLOR_REVERSED: 'rgba(255,255,255,0.8)',
};
export const GRID_LINE_CONFIG = {
stroke: 'rgba(125,125,125,0.1)',
};
export const X_ACCESSOR_INDEX = 0;
export const STACK_ACCESSORS = [0];
export const Y_ACCESSOR_INDEXES = [1];
export const STACKED_OPTIONS = {
NONE: 'none',
PERCENT: 'percent',
STACKED: 'stacked',
STACKED_WITHIN_SERIES: 'stacked_within_series',
};

View file

@ -0,0 +1,57 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { bombIcon } from '../../components/svg/bomb_icon';
import { fireIcon } from '../../components/svg/fire_icon';
export const ICON_NAMES = {
ASTERISK: 'fa-asterisk',
BELL: 'fa-bell',
BOLT: 'fa-bolt',
BOMB: 'fa-bomb',
BUG: 'fa-bug',
COMMENT: 'fa-comment',
EXCLAMATION_CIRCLE: 'fa-exclamation-circle',
EXCLAMATION_TRIANGLE: 'fa-exclamation-triangle',
FIRE: 'fa-fire',
FLAG: 'fa-flag',
HEART: 'fa-heart',
MAP_MARKER: 'fa-map-marker',
MAP_PIN: 'fa-map-pin',
STAR: 'fa-star',
TAG: 'fa-tag',
};
export const ICON_TYPES_MAP = {
[ICON_NAMES.ASTERISK]: 'asterisk',
[ICON_NAMES.BELL]: 'bell',
[ICON_NAMES.BOLT]: 'bolt',
[ICON_NAMES.BOMB]: bombIcon,
[ICON_NAMES.BUG]: 'bug',
[ICON_NAMES.COMMENT]: 'editorComment',
[ICON_NAMES.EXCLAMATION_CIRCLE]: 'alert', // TODO: Change as an exclamation mark is added
[ICON_NAMES.EXCLAMATION_TRIANGLE]: 'alert',
[ICON_NAMES.FIRE]: fireIcon,
[ICON_NAMES.FLAG]: 'flag',
[ICON_NAMES.HEART]: 'heart',
[ICON_NAMES.MAP_MARKER]: 'mapMarker',
[ICON_NAMES.MAP_PIN]: 'pinFilled',
[ICON_NAMES.STAR]: 'starFilled',
[ICON_NAMES.TAG]: 'tag',
};

View file

@ -17,6 +17,5 @@
* under the License.
*/
import $ from 'jquery';
export const eventBus = $({});
export * from './chart';
export * from './icons';

View file

@ -1,56 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { expect } from 'chai';
import { calculateFillColor } from '../calculate_fill_color';
describe('calculateFillColor(color, fill)', () => {
it('should return "fill" and "fillColor" properties', () => {
const color = 'rgb(255,0,0)';
const fill = 1;
const data = calculateFillColor(color, fill);
expect(data.fill).to.be.true;
expect(data.fillColor).to.be.a('string');
});
it('should set "fill" property to false in case of 0 opacity', () => {
const color = 'rgb(255, 0, 0)';
const fill = 0;
const data = calculateFillColor(color, fill);
expect(data.fill).to.be.false;
});
it('should return the opacity less than 1', () => {
const color = 'rgba(255, 0, 0, 0.9)';
const fill = 10;
const data = calculateFillColor(color, fill);
expect(data.fillColor).to.equal('rgba(255, 0, 0, 0.9)');
});
it('should sum fill and color opacity', () => {
const color = 'rgba(255, 0, 0, 0.5)';
const fill = 0.5;
const data = calculateFillColor(color, fill);
expect(data.fillColor).to.equal('rgba(255, 0, 0, 0.25)');
});
});

View file

@ -17,15 +17,9 @@
* under the License.
*/
import _ from 'lodash';
// bar sizes are measured in milliseconds so this assumes that the different
// between timestamps is in milliseconds. A normal bar size is 70% which gives
// enough spacing for the bar.
export const calculateBarWidth = (series, multiplier = 0.7) => {
const first = _.first(series);
try {
return (first.data[1][0] - first.data[0][0]) * multiplier;
} catch (e) {
return 1000; // 1000 ms
}
};
// TODO: Remove bus when action/triggers are available with LegacyPluginApi or metric is converted to Embeddable
import $ from 'jquery';
export const ACTIVE_CURSOR = 'ACTIVE_CURSOR';
export const eventBus = $({});

View file

@ -1,48 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import _ from 'lodash';
import { EuiIcon } from '@elastic/eui';
export const createLegendSeries = props => (row, index = 0) => {
function tickFormatter(value) {
if (_.isFunction(props.tickFormatter)) return props.tickFormatter(value);
return value;
}
const key = `tvbLegend__item${row.id}${index}`;
const formatter = row.tickFormatter || tickFormatter;
const value = formatter(props.seriesValues[row.id]);
const classes = ['tvbLegend__item'];
if (!_.includes(props.seriesFilter, row.id)) classes.push('disabled');
if (row.label == null || row.legend === false)
return <div key={key} style={{ display: 'none' }} />;
return (
<div key={key} className={classes.join(' ')} data-test-subj="tsvbLegendItem">
<button onClick={event => props.onToggle(event, row.id)} className="tvbLegend__button">
<span className="tvbLegend__itemLabel" title={`${row.label}: ${value}`}>
<EuiIcon size="m" type="dot" color={row.color} />
<span>{row.label}</span>
</span>
<span className="tvbLegend__itemValue">{value}</span>
</button>
</div>
);
};

View file

@ -1,6 +1,5 @@
@import './annotation';
@import './gauge';
@import './metric';
@import './legend';
@import './timeseries_chart';
@import './top_n';

View file

@ -22,7 +22,7 @@ import React, { Component } from 'react';
import _ from 'lodash';
import reactcss from 'reactcss';
import { calculateCoordinates } from '../lib/calculate_coordinates';
import { COLORS } from '../lib/colors';
import { COLORS } from '../constants/chart';
export class GaugeVis extends Component {
constructor(props) {
@ -118,7 +118,7 @@ export class GaugeVis extends Component {
cx: 60,
cy: 60,
fill: 'rgba(0,0,0,0)',
stroke: COLORS.lineColor,
stroke: COLORS.LINE_COLOR,
strokeDasharray: `${sliceSize * size} ${size}`,
strokeWidth: this.props.innerLine,
},

View file

@ -17,17 +17,29 @@
* under the License.
*/
import { expect } from 'chai';
import { calculateBarWidth } from '../calculate_bar_width';
export const CurveType = {
CURVE_CARDINAL: 0,
CURVE_NATURAL: 1,
CURVE_MONOTONE_X: 2,
CURVE_MONOTONE_Y: 3,
CURVE_BASIS: 4,
CURVE_CATMULL_ROM: 5,
CURVE_STEP: 6,
CURVE_STEP_AFTER: 7,
CURVE_STEP_BEFORE: 8,
LINEAR: 9,
};
describe('calculateBarWidth(series, divisor, multiplier)', () => {
it('returns default bar width', () => {
const series = [{ data: [[100, 100], [200, 100]] }];
expect(calculateBarWidth(series)).to.equal(70);
});
export const ScaleType = {
Linear: 'linear',
Ordinal: 'ordinal',
Log: 'log',
Sqrt: 'sqrt',
Time: 'time',
};
it('returns custom bar width', () => {
const series = [{ data: [[100, 100], [200, 100]] }];
expect(calculateBarWidth(series, 2)).to.equal(200);
});
});
export const getSpecId = x => `id:${x}`;
export const getGroupId = x => `groupId:${x}`;
export const BarSeries = () => null;
export const AreaSeries = () => null;

View file

@ -0,0 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/area_decorator.js <AreaSeriesDecorator /> should render and match a snapshot 1`] = `
<AreaSeries
areaSeriesStyle={
Object {
"area": Object {
"fill": "rgb(0, 156, 224)",
"opacity": 1,
"visible": true,
},
"line": Object {
"stroke": "rgb(0, 156, 224)",
"strokeWidth": 2,
"visible": true,
},
"point": Object {
"radius": 1,
"stroke": "rgb(0, 156, 224)",
"strokeWidth": 5,
"visible": false,
},
}
}
curve={9}
customSeriesColors={
Map {
Object {
"colorValues": Array [],
"specId": "id:61ca57f1-469d-11e7-af02-69e470af7417:Rome",
} => "rgb(0, 156, 224)",
}
}
data={
Array [
Array [
1556917200000,
7,
],
Array [
1557003600000,
9,
],
]
}
enableHistogramMode={true}
groupId="groupId:yaxis_main_group"
hideInLegend={false}
histogramModeAlignment="center"
id="id:61ca57f1-469d-11e7-af02-69e470af7417:Rome"
name="Rome"
stackAsPercentage={false}
timeZone="local"
xAccessor={0}
xScaleType="time"
yAccessors={
Array [
1,
]
}
yScaleType="linear"
/>
`;

View file

@ -0,0 +1,55 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/bar_decorator.js <BarSeriesDecorator /> should render and match a snapshot 1`] = `
<BarSeries
barSeriesStyle={
Object {
"rect": Object {
"fill": "rgb(0, 156, 224)",
"opacity": 0.5,
},
"rectBorder": Object {
"stroke": "rgb(0, 156, 224)",
"strokeWidth": 2,
"visible": true,
},
}
}
customSeriesColors={
Map {
Object {
"colorValues": Array [],
"specId": "id:61ca57f1-469d-11e7-af02-69e470af7417:Rome",
} => "rgb(0, 156, 224)",
}
}
data={
Array [
Array [
1556917200000,
7,
],
Array [
1557003600000,
9,
],
]
}
enableHistogramMode={true}
groupId="groupId:yaxis_main_group"
hideInLegend={false}
histogramModeAlignment="center"
id="id:61ca57f1-469d-11e7-af02-69e470af7417:Rome"
name="Rome"
stackAsPercentage={false}
timeZone="local"
xAccessor={0}
xScaleType="time"
yAccessors={
Array [
1,
]
}
yScaleType="linear"
/>
`;

View file

@ -0,0 +1,81 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { getSpecId, getGroupId, ScaleType, AreaSeries } from '@elastic/charts';
import { getSeriesColors, getAreaStyles } from '../utils/series_styles';
import { ChartsEntities } from '../model/charts';
import { X_ACCESSOR_INDEX, Y_ACCESSOR_INDEXES } from '../../../constants';
export function AreaSeriesDecorator({
seriesId,
seriesGroupId,
name,
data,
hideInLegend,
lines,
color,
stackAccessors,
stackAsPercentage,
points,
xScaleType,
yScaleType,
timeZone,
enableHistogramMode,
useDefaultGroupDomain,
sortIndex,
}) {
const id = getSpecId(seriesId);
const groupId = getGroupId(seriesGroupId);
const customSeriesColors = getSeriesColors(color, id);
const areaSeriesStyle = getAreaStyles({ points, lines, color });
const seriesSettings = {
id,
name,
groupId,
data,
customSeriesColors,
hideInLegend,
xAccessor: X_ACCESSOR_INDEX,
yAccessors: Y_ACCESSOR_INDEXES,
stackAccessors,
stackAsPercentage,
xScaleType,
yScaleType,
timeZone,
enableHistogramMode,
useDefaultGroupDomain,
sortIndex,
...areaSeriesStyle,
};
if (enableHistogramMode) {
seriesSettings.histogramModeAlignment = 'center';
}
return <AreaSeries {...seriesSettings} />;
}
AreaSeriesDecorator.propTypes = ChartsEntities.AreaChart;
AreaSeriesDecorator.defaultProps = {
yScaleType: ScaleType.Linear,
xScaleType: ScaleType.Time,
};

View file

@ -0,0 +1,60 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { AreaSeriesDecorator } from './area_decorator';
describe('src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/area_decorator.js', () => {
let props;
beforeEach(() => {
props = {
lines: {
fill: 1,
lineWidth: 2,
show: true,
steps: false,
},
points: {
lineWidth: 5,
radius: 1,
show: false,
},
color: 'rgb(0, 156, 224)',
data: [[1556917200000, 7], [1557003600000, 9]],
hideInLegend: false,
stackAsPercentage: false,
seriesId: '61ca57f1-469d-11e7-af02-69e470af7417:Rome',
seriesGroupId: 'yaxis_main_group',
name: 'Rome',
stack: false,
timeZone: 'local',
enableHistogramMode: true,
};
});
describe('<AreaSeriesDecorator />', () => {
test('should render and match a snapshot', () => {
const wrapper = shallow(<AreaSeriesDecorator {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,80 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { getSpecId, getGroupId, ScaleType, BarSeries } from '@elastic/charts';
import { getSeriesColors, getBarStyles } from '../utils/series_styles';
import { ChartsEntities } from '../model/charts';
import { X_ACCESSOR_INDEX, Y_ACCESSOR_INDEXES } from '../../../constants';
export function BarSeriesDecorator({
seriesId,
seriesGroupId,
name,
data,
hideInLegend,
bars,
color,
stackAccessors,
stackAsPercentage,
xScaleType,
yScaleType,
timeZone,
enableHistogramMode,
useDefaultGroupDomain,
sortIndex,
}) {
const id = getSpecId(seriesId);
const groupId = getGroupId(seriesGroupId);
const customSeriesColors = getSeriesColors(color, id);
const barSeriesStyle = getBarStyles(bars, color);
const seriesSettings = {
id,
name,
groupId,
data,
customSeriesColors,
hideInLegend,
xAccessor: X_ACCESSOR_INDEX,
yAccessors: Y_ACCESSOR_INDEXES,
stackAccessors,
stackAsPercentage,
xScaleType,
yScaleType,
timeZone,
enableHistogramMode,
useDefaultGroupDomain,
sortIndex,
...barSeriesStyle,
};
if (enableHistogramMode) {
seriesSettings.histogramModeAlignment = 'center';
}
return <BarSeries {...seriesSettings} />;
}
BarSeriesDecorator.propTypes = ChartsEntities.BarChart;
BarSeriesDecorator.defaultProps = {
yScaleType: ScaleType.Linear,
xScaleType: ScaleType.Time,
};

View file

@ -0,0 +1,50 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { BarSeriesDecorator } from './bar_decorator';
describe('src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/bar_decorator.js', () => {
let props;
beforeEach(() => {
props = {
bars: { show: true, fill: 0.5, lineWidth: 2 },
color: 'rgb(0, 156, 224)',
data: [[1556917200000, 7], [1557003600000, 9]],
hideInLegend: false,
stackAsPercentage: false,
seriesId: '61ca57f1-469d-11e7-af02-69e470af7417:Rome',
seriesGroupId: 'yaxis_main_group',
name: 'Rome',
stack: false,
timeZone: 'local',
enableHistogramMode: true,
};
});
describe('<BarSeriesDecorator />', () => {
test('should render and match a snapshot', () => {
const wrapper = shallow(<BarSeriesDecorator {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,257 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import {
Axis,
Chart,
Position,
Settings,
getAxisId,
getGroupId,
DARK_THEME,
LIGHT_THEME,
getAnnotationId,
AnnotationDomainTypes,
LineAnnotation,
TooltipType,
} from '@elastic/charts';
import { EuiIcon } from '@elastic/eui';
import { timezoneProvider } from 'ui/vis/lib/timezone';
import { eventBus, ACTIVE_CURSOR } from '../../lib/active_cursor';
import chrome from 'ui/chrome';
import { GRID_LINE_CONFIG, ICON_TYPES_MAP, STACKED_OPTIONS } from '../../constants';
import { AreaSeriesDecorator } from './decorators/area_decorator';
import { BarSeriesDecorator } from './decorators/bar_decorator';
import { getStackAccessors } from './utils/stack_format';
const generateAnnotationData = (values, formatter) =>
values.map(({ key, docs }) => ({
dataValue: key,
details: docs[0],
header: formatter({
value: key,
}),
}));
const decorateFormatter = formatter => ({ value }) => formatter(value);
const handleCursorUpdate = cursor => {
eventBus.trigger(ACTIVE_CURSOR, cursor);
};
export const TimeSeries = ({
isDarkMode,
showGrid,
legend,
legendPosition,
xAxisLabel,
series,
yAxis,
onBrush,
xAxisFormatter,
annotations,
enableHistogramMode,
}) => {
const chartRef = useRef();
const updateCursor = (_, cursor) => {
if (chartRef.current) {
chartRef.current.dispatchExternalCursorEvent(cursor);
}
};
useEffect(() => {
eventBus.on(ACTIVE_CURSOR, updateCursor);
return () => {
eventBus.off(ACTIVE_CURSOR, undefined, updateCursor);
};
}, []); // eslint-disable-line
const tooltipFormatter = decorateFormatter(xAxisFormatter);
const uiSettings = chrome.getUiSettingsClient();
const timeZone = timezoneProvider(uiSettings)();
const hasBarChart = series.some(({ bars }) => bars.show);
return (
<Chart ref={chartRef} renderer="canvas" className="tvbVisTimeSeries">
<Settings
showLegend={legend}
legendPosition={legendPosition}
onBrushEnd={onBrush}
animateData={false}
onCursorUpdate={handleCursorUpdate}
theme={
hasBarChart
? {}
: {
crosshair: {
band: {
fill: '#F00',
},
},
}
}
baseTheme={isDarkMode ? DARK_THEME : LIGHT_THEME}
tooltip={{
snap: true,
type: TooltipType.VerticalCursor,
headerFormatter: tooltipFormatter,
}}
/>
{annotations.map(({ id, data, icon, color }) => {
const dataValues = generateAnnotationData(data, tooltipFormatter);
const style = { line: { stroke: color } };
return (
<LineAnnotation
key={id}
annotationId={getAnnotationId(id)}
domainType={AnnotationDomainTypes.XDomain}
dataValues={dataValues}
marker={<EuiIcon type={ICON_TYPES_MAP[icon] || 'asterisk'} />}
hideLinesTooltips={true}
style={style}
/>
);
})}
{series.map(
(
{
id,
label,
bars,
lines,
data,
hideInLegend,
xScaleType,
yScaleType,
groupId,
color,
stack,
points,
useDefaultGroupDomain,
},
sortIndex
) => {
const stackAccessors = getStackAccessors(stack);
const isPercentage = stack === STACKED_OPTIONS.PERCENT;
const key = `${id}-${label}`;
if (bars.show) {
return (
<BarSeriesDecorator
key={key}
seriesId={id}
seriesGroupId={groupId}
name={label.toString()}
data={data}
hideInLegend={hideInLegend}
bars={bars}
color={color}
stackAccessors={stackAccessors}
stackAsPercentage={isPercentage}
xScaleType={xScaleType}
yScaleType={yScaleType}
timeZone={timeZone}
enableHistogramMode={enableHistogramMode}
useDefaultGroupDomain={useDefaultGroupDomain}
sortIndex={sortIndex}
/>
);
}
if (lines.show) {
return (
<AreaSeriesDecorator
key={key}
seriesId={id}
seriesGroupId={groupId}
name={label.toString()}
data={data}
hideInLegend={hideInLegend}
lines={lines}
color={color}
stackAccessors={stackAccessors}
stackAsPercentage={isPercentage}
points={points}
xScaleType={xScaleType}
yScaleType={yScaleType}
timeZone={timeZone}
enableHistogramMode={enableHistogramMode}
useDefaultGroupDomain={useDefaultGroupDomain}
sortIndex={sortIndex}
/>
);
}
return null;
}
)}
{yAxis.map(({ id, groupId, position, tickFormatter, domain, hide }) => (
<Axis
key={groupId}
groupId={getGroupId(groupId)}
id={getAxisId(id)}
position={position}
domain={domain}
hide={hide}
showGridLines={showGrid}
gridLineStyle={GRID_LINE_CONFIG}
tickFormat={tickFormatter}
/>
))}
<Axis
id={getAxisId('bottom')}
position={Position.Bottom}
title={xAxisLabel}
tickFormat={xAxisFormatter}
showGridLines={showGrid}
gridLineStyle={GRID_LINE_CONFIG}
/>
</Chart>
);
};
TimeSeries.defaultProps = {
showGrid: true,
legend: true,
legendPosition: 'right',
};
TimeSeries.propTypes = {
isDarkMode: PropTypes.bool,
showGrid: PropTypes.bool,
legend: PropTypes.bool,
legendPosition: PropTypes.string,
xAxisLabel: PropTypes.string,
series: PropTypes.array,
yAxis: PropTypes.array,
onBrush: PropTypes.func,
xAxisFormatter: PropTypes.func,
annotations: PropTypes.array,
enableHistogramMode: PropTypes.bool.isRequired,
};

View file

@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/model/charts.js ChartsEntities should match a snapshot of ChartsEntities 1`] = `
Object {
"AreaChart": Object {
"color": [Function],
"data": [Function],
"enableHistogramMode": [Function],
"hideInLegend": [Function],
"lines": [Function],
"name": [Function],
"points": [Function],
"seriesGroupId": [Function],
"seriesId": [Function],
"sortIndex": [Function],
"stackAccessors": [Function],
"stackAsPercentage": [Function],
"timeZone": [Function],
"useDefaultGroupDomain": [Function],
"xScaleType": [Function],
"yScaleType": [Function],
},
"BarChart": Object {
"bars": [Function],
"color": [Function],
"data": [Function],
"enableHistogramMode": [Function],
"hideInLegend": [Function],
"name": [Function],
"seriesGroupId": [Function],
"seriesId": [Function],
"sortIndex": [Function],
"stackAccessors": [Function],
"stackAsPercentage": [Function],
"timeZone": [Function],
"useDefaultGroupDomain": [Function],
"xScaleType": [Function],
"yScaleType": [Function],
},
}
`;

View file

@ -0,0 +1,70 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
const Chart = {
seriesId: PropTypes.string.isRequired,
seriesGroupId: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
/**
* @example
* [[1556917200000, 6], [1556231200000, 16]]
*/
data: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)).isRequired,
hideInLegend: PropTypes.bool.isRequired,
color: PropTypes.string.isRequired,
stackAsPercentage: PropTypes.bool.isRequired,
stackAccessors: PropTypes.arrayOf(PropTypes.number),
xScaleType: PropTypes.string,
yScaleType: PropTypes.string,
timeZone: PropTypes.string.isRequired,
enableHistogramMode: PropTypes.bool.isRequired,
useDefaultGroupDomain: PropTypes.bool,
sortIndex: PropTypes.number,
};
const BarChart = {
...Chart,
bars: PropTypes.shape({
fill: PropTypes.number,
lineWidth: PropTypes.number,
show: PropTypes.boolean,
}).isRequired,
};
const AreaChart = {
...Chart,
lines: PropTypes.shape({
fill: PropTypes.number,
lineWidth: PropTypes.number,
show: PropTypes.bool,
steps: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
}).isRequired,
points: PropTypes.shape({
lineWidth: PropTypes.number,
radius: PropTypes.number,
show: PropTypes.bool,
}).isRequired,
};
export const ChartsEntities = {
BarChart,
AreaChart,
};

View file

@ -0,0 +1,28 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ChartsEntities } from './charts';
describe('src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/model/charts.js', () => {
describe('ChartsEntities', () => {
test('should match a snapshot of ChartsEntities', () => {
expect(ChartsEntities).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,90 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/series_styles.js getAreaStyles() should match a snapshot 1`] = `
Object {
"areaSeriesStyle": Object {
"area": Object {
"fill": "rgb(224, 0, 221)",
"opacity": 0,
"visible": true,
},
"line": Object {
"stroke": "rgb(224, 0, 221)",
"strokeWidth": 1,
"visible": true,
},
"point": Object {
"radius": 1,
"stroke": "rgb(224, 0, 221)",
"strokeWidth": 1,
"visible": true,
},
},
"curve": 6,
}
`;
exports[`src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/series_styles.js getAreaStyles() should set default values if points, lines and color are empty 1`] = `
Object {
"areaSeriesStyle": Object {
"area": Object {
"fill": "",
"opacity": undefined,
"visible": false,
},
"line": Object {
"stroke": "",
"strokeWidth": 0,
"visible": false,
},
"point": Object {
"radius": 0.5,
"stroke": "#000",
"strokeWidth": 5,
"visible": false,
},
},
"curve": 9,
}
`;
exports[`src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/series_styles.js getBarStyles() should match a snapshot 1`] = `
Object {
"barSeriesStyle": Object {
"rect": Object {
"fill": "rgb(224, 0, 221)",
"opacity": 0.5,
},
"rectBorder": Object {
"stroke": "rgb(224, 0, 221)",
"strokeWidth": 2,
"visible": true,
},
},
}
`;
exports[`src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/series_styles.js getBarStyles() should set default values if bars and colors are empty 1`] = `
Object {
"barSeriesStyle": Object {
"rect": Object {
"fill": "#000",
"opacity": 1,
},
"rectBorder": Object {
"stroke": "#000",
"strokeWidth": 0,
"visible": true,
},
},
}
`;
exports[`src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/series_styles.js getSeriesColors() should match a snapshot 1`] = `
Map {
Object {
"colorValues": Array [],
"specId": "IT",
} => "rgb(224, 0, 221)",
}
`;

View file

@ -0,0 +1,67 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { CurveType } from '@elastic/charts';
const DEFAULT_COLOR = '#000';
export const getAreaStyles = ({ points, lines, color }) => ({
areaSeriesStyle: {
line: {
stroke: color,
strokeWidth: Number(lines.lineWidth) || 0,
visible: Boolean(lines.show && lines.lineWidth),
},
area: {
fill: color,
opacity: lines.fill <= 0 ? 0 : lines.fill,
visible: Boolean(lines.show),
},
point: {
radius: points.radius || 0.5,
stroke: color || DEFAULT_COLOR,
strokeWidth: points.lineWidth || 5,
visible: points.lineWidth > 0 && Boolean(points.show),
},
},
curve: lines.steps ? CurveType.CURVE_STEP : CurveType.LINEAR,
});
export const getBarStyles = ({ show = true, lineWidth = 0, fill = 1 }, color) => ({
barSeriesStyle: {
rectBorder: {
stroke: color || DEFAULT_COLOR,
strokeWidth: lineWidth,
visible: show,
},
rect: {
fill: color || DEFAULT_COLOR,
opacity: fill,
},
},
});
export const getSeriesColors = (color, specId) => {
const map = new Map();
const seriesColorsValues = { specId, colorValues: [] };
map.set(seriesColorsValues, color);
return map;
};

View file

@ -0,0 +1,82 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { getBarStyles, getSeriesColors, getAreaStyles } from './series_styles';
describe('src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/series_styles.js', () => {
let bars;
let color;
let specId;
let points;
let lines;
beforeEach(() => {
bars = {
fill: 0.5,
lineWidth: 2,
show: true,
};
color = 'rgb(224, 0, 221)';
specId = 'IT';
points = {
lineWidth: 1,
show: true,
radius: 1,
};
lines = {
fill: 0,
lineWidth: 1,
show: true,
steps: true,
};
});
describe('getBarStyles()', () => {
test('should match a snapshot', () => {
expect(getBarStyles(bars, color)).toMatchSnapshot();
});
test('should set default values if bars and colors are empty', () => {
bars = {};
color = '';
expect(getBarStyles(bars, color)).toMatchSnapshot();
});
});
describe('getSeriesColors()', () => {
test('should match a snapshot', () => {
expect(getSeriesColors(color, specId)).toMatchSnapshot();
});
});
describe('getAreaStyles()', () => {
test('should match a snapshot', () => {
expect(getAreaStyles({ points, lines, color })).toMatchSnapshot();
});
test('should set default values if points, lines and color are empty', () => {
points = {};
lines = {};
color = '';
expect(getAreaStyles({ points, lines, color })).toMatchSnapshot();
});
});
});

View file

@ -17,16 +17,15 @@
* under the License.
*/
import Color from 'color';
import { STACK_ACCESSORS, STACKED_OPTIONS } from '../../../constants';
export const calculateFillColor = (color, fill = 1) => {
const initialColor = new Color(color).rgb();
const opacity = Math.min(Number(fill), 1) * initialColor.valpha;
const [r, g, b] = initialColor.color;
return {
fill: opacity > 0,
fillColor: new Color([r, g, b, Number(opacity.toFixed(2))]).string(),
};
export const getStackAccessors = stack => {
switch (stack) {
case STACKED_OPTIONS.STACKED:
case STACKED_OPTIONS.STACKED_WITHIN_SERIES:
case STACKED_OPTIONS.PERCENT:
return STACK_ACCESSORS;
default:
return undefined;
}
};

View file

@ -17,20 +17,21 @@
* under the License.
*/
import { getValueBy } from '../get_value_by';
import { expect } from 'chai';
import { getStackAccessors } from './stack_format';
import { X_ACCESSOR_INDEX, STACKED_OPTIONS } from '../../../constants';
describe('getValueBy(fn, data)', () => {
it("returns max for getValueBy('max', data) ", () => {
const data = [[0, 5], [1, 3], [2, 4], [3, 6], [4, 5]];
expect(getValueBy('max', data)).to.equal(6);
});
it('returns 0 if data is not array', () => {
const data = '1';
expect(getValueBy('max', data)).to.equal(0);
});
it('returns value if data is number', () => {
const data = 1;
expect(getValueBy('max', data)).to.equal(1);
describe('src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/stack_format.js', () => {
describe('getStackAccessors()', () => {
test('should return an accessor if the stack is stacked', () => {
expect(getStackAccessors(STACKED_OPTIONS.STACKED)).toEqual([X_ACCESSOR_INDEX]);
});
test('should return an accessor if the stack is percent', () => {
expect(getStackAccessors(STACKED_OPTIONS.PERCENT)).toEqual([X_ACCESSOR_INDEX]);
});
test('should return undefined if the stack does not match with STACKED and PERCENT', () => {
expect(getStackAccessors(STACKED_OPTIONS.NONE)).toBeUndefined();
});
});
});

View file

@ -22,35 +22,34 @@ import { getDefaultDecoration } from '../../helpers/get_default_decoration';
describe('getDefaultDecoration', () => {
describe('stack option', () => {
it('should set a stack option to false', () => {
it('should set a stack option to none', () => {
const series = {
id: 'test_id',
stacked: 'none',
};
expect(getDefaultDecoration(series)).to.have.property('stack', false);
series.stacked = 'none';
expect(getDefaultDecoration(series)).to.have.property('stack', false);
expect(getDefaultDecoration(series)).to.have.property('stack', 'none');
});
it('should set a stack option to true', () => {
it('should set a stack option to stacked/percent', () => {
const series = {
stacked: 'stacked',
id: 'test_id',
};
expect(getDefaultDecoration(series)).to.have.property('stack', true);
expect(getDefaultDecoration(series)).to.have.property('stack', 'stacked');
series.stacked = 'percent';
expect(getDefaultDecoration(series)).to.have.property('stack', true);
expect(getDefaultDecoration(series)).to.have.property('stack', 'percent');
});
it('should set a stack option to be series id', () => {
it('should set a stack option to stacked_within_series', () => {
const series = {
stacked: 'stacked_within_series',
id: 'test_id',
};
expect(getDefaultDecoration(series)).to.have.property('stack', series.id);
expect(getDefaultDecoration(series)).to.have.property('stack', 'stacked_within_series');
});
});

View file

@ -21,21 +21,10 @@ export const getDefaultDecoration = series => {
const pointSize =
series.point_size != null ? Number(series.point_size) : Number(series.line_width);
const showPoints = series.chart_type === 'line' && pointSize !== 0;
let stack;
switch (series.stacked) {
case 'stacked':
case 'percent':
stack = true;
break;
case 'stacked_within_series':
stack = series.id;
break;
default:
stack = false;
}
return {
stack,
seriesId: series.id,
stack: series.stacked,
lines: {
show: series.chart_type === 'line' && series.line_width !== 0,
fill: Number(series.fill),

View file

@ -109,6 +109,7 @@ describe('seriesAgg(resp, panel, series)', () => {
color: '#F00',
label: 'Total CPU',
stack: false,
seriesId: 'test',
lines: { show: true, fill: 0, lineWidth: 1, steps: false },
points: { show: true, radius: 1, lineWidth: 1 },
bars: { fill: 0, lineWidth: 1, show: false },

View file

@ -98,6 +98,7 @@ describe('stdSibling(resp, panel, series)', () => {
label: 'Overall Std. Deviation of Average of cpu',
color: 'rgb(255, 0, 0)',
stack: false,
seriesId: 'test',
lines: { show: true, fill: 0, lineWidth: 1, steps: false },
points: { show: true, radius: 1, lineWidth: 1 },
bars: { fill: 0, lineWidth: 1, show: false },

View file

@ -74,7 +74,6 @@ export default function ({ getService, getPageObjects }) {
it('tsvb time series shows no data message', async () => {
expect(await testSubjects.exists('noTSVBDataMessage')).to.be(true);
await dashboardExpect.tsvbTimeSeriesLegendCount(0);
});
it('metric value shows no data', async () => {
@ -134,11 +133,6 @@ export default function ({ getService, getPageObjects }) {
await dashboardExpect.goalAndGuageLabelsExist(['0', '0%']);
});
it('tsvb time series shows no data message', async () => {
expect(await testSubjects.exists('noTSVBDataMessage')).to.be(true);
await dashboardExpect.tsvbTimeSeriesLegendCount(0);
});
it('metric value shows no data', async () => {
await dashboardExpect.metricValuesExist(['-']);
});
@ -195,11 +189,6 @@ export default function ({ getService, getPageObjects }) {
await dashboardExpect.goalAndGuageLabelsExist(['39.958%', '7,544']);
});
it('tsvb time series', async () => {
expect(await testSubjects.exists('noTSVBDataMessage')).to.be(false);
await dashboardExpect.tsvbTimeSeriesLegendCount(10);
});
it('metric value', async () => {
await dashboardExpect.metricValuesExist(['101']);
});

View file

@ -50,7 +50,6 @@ export default function ({ getService, getPageObjects }) {
await dashboardExpect.tagCloudWithValuesFound(['CN', 'IN', 'US', 'BR', 'ID']);
// TODO add test for 'region map viz'
// TODO add test for 'tsvb gauge' viz
await dashboardExpect.tsvbTimeSeriesLegendCount(1);
// TODO add test for 'geo map' viz
// This tests the presence of the two input control embeddables
await dashboardExpect.inputControlItemCount(5);
@ -86,7 +85,6 @@ export default function ({ getService, getPageObjects }) {
await dashboardExpect.tsvbMetricValuesExist(['0']);
await dashboardExpect.tsvbMarkdownWithValuesExists(['Hi Avg last bytes: 0']);
await dashboardExpect.tsvbTableCellCount(0);
await dashboardExpect.tsvbTimeSeriesLegendCount(1);
await dashboardExpect.tsvbTopNValuesExist(['0']);
await dashboardExpect.vegaTextsDoNotExist(['5,000']);
};

View file

@ -75,7 +75,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
await visualBuilder.changePanelPreview();
await visualBuilder.cloneSeries();
const legend = await visualBuilder.getLegentItems();
const legend = await visualBuilder.getLegendItems();
const series = await visualBuilder.getSeries();
expect(legend.length).to.be(2);
expect(series.length).to.be(2);
@ -108,7 +108,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
expect(actualCount).to.be(expectedLegendValue);
});
it('should show the correct count in the legend with "Human readable" duration formatter', async () => {
it.skip('should show the correct count in the legend with "Human readable" duration formatter', async () => {
await visualBuilder.clickSeriesOption();
await visualBuilder.changeDataFormatter('Duration');
await visualBuilder.setDurationFormatterSettings({ to: 'Human readable' });
@ -126,7 +126,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
expect(actualCountMin).to.be('3 hours');
});
describe('Dark mode', () => {
// --reversed class is not implemented in @elastic\chart
describe.skip('Dark mode', () => {
before(async () => {
await kibanaServer.uiSettings.update({
'theme:darkMode': true,

View file

@ -67,11 +67,14 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro
}
public async checkTimeSeriesChartIsPresent() {
await testSubjects.existOrFail('timeseriesChart');
const isPresent = await find.existsByCssSelector('.tvbVisTimeSeries');
if (!isPresent) {
throw new Error(`TimeSeries chart is not loaded`);
}
}
public async checkTimeSeriesLegendIsPresent() {
const isPresent = await find.existsByCssSelector('.tvbLegend');
const isPresent = await find.existsByCssSelector('.echLegend');
if (!isPresent) {
throw new Error(`TimeSeries legend is not loaded`);
}
@ -291,9 +294,11 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro
await el.type(value);
}
public async getRhythmChartLegendValue() {
public async getRhythmChartLegendValue(nth = 0) {
await PageObjects.visualize.waitForVisualizationRenderingStabilized();
const metricValue = await find.byCssSelector('.tvbLegend__itemValue');
const metricValue = (await find.allByCssSelector(
`.echLegendItem .echLegendItem__displayValue`
))[nth];
await metricValue.moveMouseTo();
return await metricValue.getVisibleText();
}
@ -502,8 +507,8 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro
await PageObjects.visualize.waitForRenderingCount(prevRenderingCount + 1);
}
public async getLegentItems(): Promise<WebElementWrapper[]> {
return await testSubjects.findAll('tsvbLegendItem');
public async getLegendItems(): Promise<WebElementWrapper[]> {
return await find.allByCssSelector('.echLegendItem');
}
public async getSeries(): Promise<WebElementWrapper[]> {

View file

@ -62,14 +62,6 @@ export function DashboardExpectProvider({ getService, getPageObjects }) {
});
}
async tsvbTimeSeriesLegendCount(expectedCount) {
log.debug(`DashboardExpect.tsvbTimeSeriesLegendCount(${expectedCount})`);
await retry.try(async () => {
const tsvbLegendItems = await testSubjects.findAll('tsvbLegendItem', findTimeout);
expect(tsvbLegendItems.length).to.be(expectedCount);
});
}
async fieldSuggestions(expectedFields) {
log.debug(`DashboardExpect.fieldSuggestions(${expectedFields})`);
const fields = await filterBar.getFilterEditorFields();

View file

@ -181,7 +181,7 @@ export const NodesOverview = injectI18n(
private handleViewChange = (view: string) => this.props.onViewChange(view);
// TODO: Change this to a real implimentation using the tickFormatter from the prototype as an example.
// TODO: Change this to a real implementation using the tickFormatter from the prototype as an example.
private formatter = (val: string | number) => {
const { metric } = this.props.options;
const metricFormatter = get(

View file

@ -3259,7 +3259,6 @@
"tsvb.getInterval.secondsLabel": "秒",
"tsvb.getInterval.weeksLabel": "週間",
"tsvb.getInterval.yearsLabel": "年",
"tsvb.horizontalLegend.toggleChartAriaLabel": "チャートの凡例を切り替える",
"tsvb.iconSelect.asteriskLabel": "アスタリスク",
"tsvb.iconSelect.bellLabel": "ベル",
"tsvb.iconSelect.boltLabel": "ボルト",
@ -3564,7 +3563,6 @@
"tsvb.validateInterval.notifier.maxBucketsExceededErrorMessage": "バケットの最高数を超えました。{buckets} が {maxBuckets} を超えています。パネルオプションでより広い間隔を試してみてください。",
"tsvb.vars.variableNameAriaLabel": "変数名",
"tsvb.vars.variableNamePlaceholder": "変数名",
"tsvb.verticalLegend.toggleChartAriaLabel": "チャートの凡例を切り替える",
"tsvb.visEditorVisualization.applyChangesLabel": "変更を適用",
"tsvb.visEditorVisualization.autoApplyLabel": "自動適用",
"tsvb.visEditorVisualization.changesHaveNotBeenAppliedMessage": "ビジュアライゼーションへの変更が適用されました。",

View file

@ -3259,7 +3259,6 @@
"tsvb.getInterval.secondsLabel": "秒",
"tsvb.getInterval.weeksLabel": "周",
"tsvb.getInterval.yearsLabel": "年",
"tsvb.horizontalLegend.toggleChartAriaLabel": "切换图例",
"tsvb.iconSelect.asteriskLabel": "星号",
"tsvb.iconSelect.bellLabel": "钟铃",
"tsvb.iconSelect.boltLabel": "闪电",
@ -3564,7 +3563,6 @@
"tsvb.validateInterval.notifier.maxBucketsExceededErrorMessage": "超过最大桶数:{buckets} 大于 {maxBuckets},请在面板选项中尝试更大的时间间隔。",
"tsvb.vars.variableNameAriaLabel": "变量名称",
"tsvb.vars.variableNamePlaceholder": "变量名称",
"tsvb.verticalLegend.toggleChartAriaLabel": "切换图例",
"tsvb.visEditorVisualization.applyChangesLabel": "应用更改",
"tsvb.visEditorVisualization.autoApplyLabel": "自动应用",
"tsvb.visEditorVisualization.changesHaveNotBeenAppliedMessage": "未应用对此可视化的更改。",