Add visualization renderer to support Kibana visualizations inside Canvas.

This commit is contained in:
Peter Pisljar 2019-01-25 08:56:54 -08:00 committed by Luke Elmers
parent 2574028c7d
commit b66375eb8f
61 changed files with 605 additions and 417 deletions

View file

@ -30,6 +30,7 @@ import { shape } from './shape';
import { string } from './string';
import { style } from './style';
import { kibanaContext } from './kibana_context';
import { kibanaDatatable } from './kibana_datatable';
export const typeSpecs = [
boolean,
@ -45,4 +46,5 @@ export const typeSpecs = [
string,
style,
kibanaContext,
kibanaDatatable,
];

View file

@ -0,0 +1,44 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { map } from 'lodash';
export const kibanaDatatable = () => ({
name: 'kibana_datatable',
from: {
datatable: context => {
context.columns.forEach(c => c.id = c.name);
return {
type: 'kibana_datatable',
rows: context.rows,
columns: context.columns,
};
},
pointseries: context => {
const columns = map(context.columns, (column, name) => {
return { id: name, name, ...column };
});
return {
type: 'kibana_datatable',
rows: context.rows,
columns: columns,
};
}
},
});

View file

@ -34,7 +34,7 @@ const courierRequestHandler = courierRequestHandlerProvider().handler;
export const esaggs = () => ({
name: 'esaggs',
type: 'datatable',
type: 'kibana_datatable',
context: {
types: [
'kibana_context',
@ -90,7 +90,7 @@ export const esaggs = () => ({
});
return {
type: 'datatable',
type: 'kibana_datatable',
rows: response.rows,
columns: response.columns.map(column => ({
id: column.id,

View file

@ -40,10 +40,8 @@ export const inputControlVis = () => ({
type: 'render',
as: 'visualization',
value: {
visConfig: {
type: 'input_controls_vis',
params: params
},
visType: 'input_control_vis',
visConfig: params
}
};
}

View file

@ -46,12 +46,10 @@ export const kibanaMarkdown = () => ({
type: 'render',
as: 'visualization',
value: {
visType: 'markdown',
visConfig: {
type: 'markdown',
params: {
markdown: args.spec,
...params,
}
markdown: args.spec,
...params,
},
}
};

View file

@ -24,7 +24,7 @@ export const metric = () => ({
type: 'render',
context: {
types: [
'datatable'
'kibana_datatable'
],
},
help: i18n.translate('interpreter.functions.metric.help', {
@ -44,10 +44,8 @@ export const metric = () => ({
as: 'visualization',
value: {
visData: context,
visConfig: {
type: 'metric',
params: visConfigParams,
},
visType: 'metric',
visConfig: visConfigParams,
params: {
listenOnChange: true,
}

View file

@ -17,8 +17,7 @@
* under the License.
*/
import { VislibSlicesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib';
import chrome from 'ui/chrome';
import { VislibSlicesResponseHandlerProvider as vislibSlicesResponseHandler } from 'ui/vis/response_handlers/vislib';
import { i18n } from '@kbn/i18n';
export const kibanaPie = () => ({
@ -26,7 +25,7 @@ export const kibanaPie = () => ({
type: 'render',
context: {
types: [
'datatable'
'kibana_datatable'
],
},
help: i18n.translate('interpreter.functions.pie.help', {
@ -39,9 +38,7 @@ export const kibanaPie = () => ({
},
},
async fn(context, args) {
const $injector = await chrome.dangerouslyGetActiveInjector();
const Private = $injector.get('Private');
const responseHandler = Private(VislibSlicesResponseHandlerProvider).handler;
const responseHandler = vislibSlicesResponseHandler().handler;
const visConfigParams = JSON.parse(args.visConfig);
const convertedData = await responseHandler(context, visConfigParams.dimensions);
@ -51,10 +48,8 @@ export const kibanaPie = () => ({
as: 'visualization',
value: {
visData: convertedData,
visConfig: {
type: args.type,
params: visConfigParams,
},
visType: 'pie',
visConfig: visConfigParams,
params: {
listenOnChange: true,
}

View file

@ -24,7 +24,7 @@ export const regionmap = () => ({
type: 'render',
context: {
types: [
'datatable'
'kibana_datatable'
],
},
help: i18n.translate('interpreter.functions.regionmap.help', {
@ -44,10 +44,8 @@ export const regionmap = () => ({
as: 'visualization',
value: {
visData: context,
visConfig: {
type: 'region_map',
params: visConfigParams,
},
visType: 'region_map',
visConfig: visConfigParams,
params: {
listenOnChange: true,
}

View file

@ -28,7 +28,7 @@ export const kibanaTable = () => ({
type: 'render',
context: {
types: [
'datatable'
'kibana_datatable'
],
},
help: i18n.translate('interpreter.functions.table.help', {
@ -50,10 +50,8 @@ export const kibanaTable = () => ({
as: 'visualization',
value: {
visData: convertedData,
visConfig: {
type: 'table',
params: visConfigParams,
},
visType: 'table',
visConfig: visConfigParams,
params: {
listenOnChange: true,
}

View file

@ -24,7 +24,7 @@ export const tagcloud = () => ({
type: 'render',
context: {
types: [
'datatable'
'kibana_datatable'
],
},
help: i18n.translate('interpreter.functions.tagcloud.help', {
@ -44,10 +44,8 @@ export const tagcloud = () => ({
as: 'visualization',
value: {
visData: context,
visConfig: {
type: 'tag_cloud',
params: visConfigParams,
},
visType: 'tagcloud',
visConfig: visConfigParams,
params: {
listenOnChange: true,
}

View file

@ -25,7 +25,7 @@ export const tilemap = () => ({
type: 'render',
context: {
types: [
'datatable'
'kibana_datatable'
],
},
help: i18n.translate('interpreter.functions.tilemap.help', {
@ -51,10 +51,8 @@ export const tilemap = () => ({
as: 'visualization',
value: {
visData: convertedData,
visConfig: {
type: 'tile_map',
params: visConfigParams,
},
visType: 'tile_map',
visConfig: visConfigParams,
params: {
listenOnChange: true,
}

View file

@ -52,18 +52,26 @@ export const timelionVis = () => ({
const Private = $injector.get('Private');
const timelionRequestHandler = Private(TimelionRequestHandlerProvider).handler;
const visParams = { expression: args.expression, interval: args.interval };
const response = await timelionRequestHandler({
timeRange: get(context, 'timeRange', null),
query: get(context, 'query', null),
filters: get(context, 'filters', null),
forceFetch: true,
visParams: { expression: args.expression, interval: args.interval }
visParams: visParams,
});
response.visType = 'timelion';
return {
type: 'render',
as: 'visualization',
value: response,
value: {
visParams,
visType: 'timelion',
visData: response,
},
};
},
});

View file

@ -64,10 +64,17 @@ export const tsvb = () => ({
uiState: uiState,
});
response.visType = 'metrics';
return {
type: 'render',
as: 'visualization',
value: response,
value: {
visType: 'metrics',
visConfig: params,
uiState: uiState,
visData: response,
},
};
},
});

View file

@ -58,11 +58,9 @@ export const vega = () => ({
as: 'visualization',
value: {
visData: response,
visType: 'vega',
visConfig: {
type: 'vega',
params: {
spec: args.spec
}
spec: args.spec
},
}
};

View file

@ -26,7 +26,7 @@ export const vislib = () => ({
type: 'render',
context: {
types: [
'datatable'
'kibana_datatable'
],
},
help: i18n.translate('interpreter.functions.vislib.help', {
@ -51,10 +51,8 @@ export const vislib = () => ({
as: 'visualization',
value: {
visData: convertedData,
visConfig: {
type: args.type,
params: visConfigParams,
},
visType: args.type,
visConfig: visConfigParams,
params: {
listenOnChange: true,
}

View file

@ -134,10 +134,8 @@ export const visualization = () => ({
as: 'visualization',
value: {
visData: context,
visConfig: {
type: args.type,
params: visConfigParams
},
visType: args.type,
visConfig: visConfigParams
}
};
}

View file

@ -24,6 +24,7 @@ import { functions } from './functions';
import { functionsRegistry } from './functions_registry';
import { typesRegistry } from './types_registry';
import { renderFunctionsRegistry } from './render_functions_registry';
import { visualization } from './renderers/visualization';
const basePath = chrome.getBasePath();
@ -38,6 +39,7 @@ function addFunction(fnDef) {
}
functions.forEach(addFunction);
renderFunctionsRegistry.register(visualization);
let _resolve;
let _interpreterPromise;

View file

@ -0,0 +1,56 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import chrome from 'ui/chrome';
import { visualizationLoader } from 'ui/visualize/loader/visualization_loader';
import { VisProvider } from 'ui/visualize/loader/vis';
export const visualization = () => ({
name: 'visualization',
displayName: 'visualization',
reuseDomNode: true,
render: async (domNode, config, handlers) => {
const { visData, visConfig, params } = config;
const visType = config.visType || visConfig.type;
const $injector = await chrome.dangerouslyGetActiveInjector();
const Private = $injector.get('Private');
const Vis = Private(VisProvider);
if (handlers.vis) {
// special case in visualize, we need to render first (without executing the expression), for maps to work
if (visConfig) {
handlers.vis.setCurrentState({ type: visType, params: visConfig });
}
} else {
handlers.vis = new Vis({
type: visType,
params: visConfig,
});
handlers.vis.eventsSubject = handlers.eventsSubject;
}
const uiState = handlers.uiState || handlers.vis.getUiState();
handlers.onDestroy(() => visualizationLoader.destroy());
await visualizationLoader.render(domNode, handlers.vis, visData, uiState, params).then(() => {
if (handlers.done) handlers.done();
});
},
});

View file

@ -763,7 +763,15 @@ function discoverController(
Promise
.resolve(responseHandler(tabifiedData, buildVislibDimensions($scope.vis, $scope.timeRange)))
.then(resp => {
visualizeHandler.render({ value: resp });
visualizeHandler.render({
as: 'visualization',
value: {
visType: 'histogram',
visData: resp,
visConfig: $scope.vis.params,
params: {},
}
});
});
}

View file

@ -41,18 +41,10 @@ describe('TagCloudVisualizationTest', function () {
const dummyTableGroup = {
columns: [{
id: 'col-0',
'aggConfig': {
'id': '2',
'enabled': true,
'type': 'terms',
'schema': 'segment',
'params': { 'field': 'geo.dest', 'size': 5, 'order': 'desc', 'orderBy': '1' },
fieldFormatter: () => (x => x)
}, 'title': 'geo.dest: Descending'
title: 'geo.dest: Descending'
}, {
id: 'col-1',
'aggConfig': { 'id': '1', 'enabled': true, 'type': 'count', 'schema': 'metric', 'params': {} },
'title': 'Count'
title: 'Count'
}],
rows: [
{ 'col-0': 'CN', 'col-1': 26 },
@ -76,7 +68,11 @@ describe('TagCloudVisualizationTest', function () {
setupDOM('512px', '512px');
imageComparator = new ImageComparator();
vis = new Vis(indexPattern, {
type: 'tagcloud'
type: 'tagcloud',
params: {
bucket: { accessor: 0, format: {} },
metric: { accessor: 0, format: {} },
},
});
});

View file

@ -73,7 +73,7 @@ export class TagCloudVisualization {
this._updateParams();
}
if (status.data) {
if (status.data || status.params) {
this._updateData(data);
}
@ -115,10 +115,11 @@ export class TagCloudVisualization {
return;
}
const bucketFormatter = this._vis.params.bucket ? getFormat(this._vis.params.bucket.format) : null;
const hasTags = data.columns.length === 2;
const tagColumn = hasTags ? data.columns[0].id : -1;
const metricColumn = data.columns[hasTags ? 1 : 0].id;
const bucket = this._vis.params.bucket;
const metric = this._vis.params.metric;
const bucketFormatter = bucket ? getFormat(bucket.format) : null;
const tagColumn = bucket ? data.columns[bucket.accessor].id : -1;
const metricColumn = data.columns[metric.accessor].id;
const tags = data.rows.map((row, rowIndex) => {
const tag = row[tagColumn] === undefined ? 'all' : row[tagColumn];
const metric = row[metricColumn];

View file

@ -1,87 +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 { toArray } from 'lodash';
import { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities';
export function convertTableProvider(tooltipFormatter) {
return function (table, { metric, buckets = [] }) {
let slices;
const names = {};
const metricColumn = table.columns[metric.accessor];
const metricFieldFormatter = getFormat(metric.format);
if (!buckets.length) {
slices = [{
name: metricColumn.name,
size: table.rows[0][metricColumn.id],
aggConfig: metricColumn.aggConfig
}];
names[metricColumn.name] = metricColumn.name;
} else {
slices = [];
table.rows.forEach((row, rowIndex) => {
let parent;
let dataLevel = slices;
buckets.forEach(bucket => {
const bucketColumn = table.columns[bucket.accessor];
const bucketValueColumn = table.columns[bucket.accessor + 1];
const bucketFormatter = getFormat(bucket.format);
const name = bucketFormatter.convert(row[bucketColumn.id]);
const size = row[bucketValueColumn.id];
names[name] = name;
let slice = dataLevel.find(slice => slice.name === name);
if (!slice) {
slice = {
name,
size,
parent,
children: [],
aggConfig: bucketColumn.aggConfig,
rawData: {
table,
row: rowIndex,
column: bucket.accessor,
value: row[bucketColumn.id],
},
};
dataLevel.push(slice);
}
parent = slice;
dataLevel = slice.children;
});
});
}
return {
hits: table.rows.length,
raw: table,
names: toArray(names),
tooltipFormatter: tooltipFormatter(metricFieldFormatter),
slices: {
children: [
...slices
]
}
};
};
}

View file

@ -17,10 +17,69 @@
* under the License.
*/
import { HierarchicalTooltipFormatterProvider } from './_hierarchical_tooltip_formatter';
import { convertTableProvider } from './_convert_table';
import { toArray } from 'lodash';
import { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities';
export function BuildHierarchicalDataProvider(Private) {
const tooltipFormatter = Private(HierarchicalTooltipFormatterProvider);
return convertTableProvider(tooltipFormatter);
}
export const buildHierarchicalData = (table, { metric, buckets = [] }) => {
let slices;
const names = {};
const metricColumn = table.columns[metric.accessor];
const metricFieldFormatter = metric.format;
if (!buckets.length) {
slices = [{
name: metricColumn.name,
size: table.rows[0][metricColumn.id],
aggConfig: metricColumn.aggConfig
}];
names[metricColumn.name] = metricColumn.name;
} else {
slices = [];
table.rows.forEach((row, rowIndex) => {
let parent;
let dataLevel = slices;
buckets.forEach(bucket => {
const bucketColumn = table.columns[bucket.accessor];
const bucketValueColumn = table.columns[bucket.accessor + 1];
const bucketFormatter = getFormat(bucket.format);
const name = bucketFormatter.convert(row[bucketColumn.id]);
const size = row[bucketValueColumn.id];
names[name] = name;
let slice = dataLevel.find(slice => slice.name === name);
if (!slice) {
slice = {
name,
size,
parent,
children: [],
aggConfig: bucketColumn.aggConfig,
rawData: {
table,
row: rowIndex,
column: bucket.accessor,
value: row[bucketColumn.id],
},
};
dataLevel.push(slice);
}
parent = slice;
dataLevel = slice.children;
});
});
}
return {
hits: table.rows.length,
raw: table,
names: toArray(names),
tooltipFormatter: metricFieldFormatter,
slices: {
children: [
...slices
]
}
};
};

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { convertTableProvider } from './_convert_table';
import { buildHierarchicalData } from './build_hierarchical_data';
import { LegacyResponseHandlerProvider as legacyResponseHandlerProvider } from '../../vis/response_handlers/legacy';
jest.mock('../../registry/field_formats', () => ({
@ -34,8 +34,6 @@ jest.mock('../../chrome', () => ({
}));
describe('buildHierarchicalData convertTable', () => {
const mockToolTipFormatter = () => ({});
const convertTable = convertTableProvider(mockToolTipFormatter);
const responseHandler = legacyResponseHandlerProvider().handler;
describe('metric only', () => {
@ -60,7 +58,7 @@ describe('buildHierarchicalData convertTable', () => {
});
it('should set the slices with one child to a consistent label', () => {
const results = convertTable(table, dimensions);
const results = buildHierarchicalData(table, dimensions);
const checkLabel = 'Average bytes';
expect(results).toHaveProperty('names');
expect(results.names).toEqual([checkLabel]);
@ -120,40 +118,40 @@ describe('buildHierarchicalData convertTable', () => {
it('should set the correct hits attribute for each of the results', () => {
tables.forEach(t => {
const results = convertTable(t.tables[0], dimensions);
const results = buildHierarchicalData(t.tables[0], dimensions);
expect(results).toHaveProperty('hits');
expect(results.hits).toBe(4);
});
});
it('should set the correct names for each of the results', () => {
const results0 = convertTable(tables[0].tables[0], dimensions);
const results0 = buildHierarchicalData(tables[0].tables[0], dimensions);
expect(results0).toHaveProperty('names');
expect(results0.names).toHaveLength(5);
const results1 = convertTable(tables[1].tables[0], dimensions);
const results1 = buildHierarchicalData(tables[1].tables[0], dimensions);
expect(results1).toHaveProperty('names');
expect(results1.names).toHaveLength(5);
const results2 = convertTable(tables[2].tables[0], dimensions);
const results2 = buildHierarchicalData(tables[2].tables[0], dimensions);
expect(results2).toHaveProperty('names');
expect(results2.names).toHaveLength(4);
});
it('should set the parent of the first item in the split', () => {
const results0 = convertTable(tables[0].tables[0], dimensions);
const results0 = buildHierarchicalData(tables[0].tables[0], dimensions);
expect(results0).toHaveProperty('slices');
expect(results0.slices).toHaveProperty('children');
expect(results0.slices.children).toHaveLength(2);
expect(results0.slices.children[0].rawData.table.$parent).toHaveProperty('key', 'png');
const results1 = convertTable(tables[1].tables[0], dimensions);
const results1 = buildHierarchicalData(tables[1].tables[0], dimensions);
expect(results1).toHaveProperty('slices');
expect(results1.slices).toHaveProperty('children');
expect(results1.slices.children).toHaveLength(2);
expect(results1.slices.children[0].rawData.table.$parent).toHaveProperty('key', 'css');
const results2 = convertTable(tables[2].tables[0], dimensions);
const results2 = buildHierarchicalData(tables[2].tables[0], dimensions);
expect(results2).toHaveProperty('slices');
expect(results2.slices).toHaveProperty('children');
expect(results2.slices.children).toHaveLength(2);
@ -191,7 +189,7 @@ describe('buildHierarchicalData convertTable', () => {
});
it('should set the hits attribute for the results', () => {
const results = convertTable(table, dimensions);
const results = buildHierarchicalData(table, dimensions);
expect(results).toHaveProperty('raw');
expect(results).toHaveProperty('slices');
expect(results.slices).toHaveProperty('children');
@ -226,7 +224,7 @@ describe('buildHierarchicalData convertTable', () => {
});
it('should set the hits attribute for the results', () => {
const results = convertTable(table, dimensions);
const results = buildHierarchicalData(table, dimensions);
expect(results).toHaveProperty('raw');
expect(results).toHaveProperty('slices');
expect(results.slices).toHaveProperty('children');
@ -261,7 +259,7 @@ describe('buildHierarchicalData convertTable', () => {
});
it('should set the hits attribute for the results', () => {
const results = convertTable(table, dimensions);
const results = buildHierarchicalData(table, dimensions);
expect(results).toHaveProperty('raw');
expect(results).toHaveProperty('slices');
expect(results).toHaveProperty('names');

View file

@ -17,14 +17,12 @@
* under the License.
*/
import { BuildHierarchicalDataProvider } from './hierarchical/build_hierarchical_data';
import { AggResponsePointSeriesProvider } from './point_series/point_series';
import { buildHierarchicalData } from './hierarchical/build_hierarchical_data';
import { buildPointSeriesData } from './point_series/point_series';
import { tabifyAggResponse } from './tabify/tabify';
export function AggResponseIndexProvider(Private) {
return {
hierarchical: Private(BuildHierarchicalDataProvider),
pointSeries: Private(AggResponsePointSeriesProvider),
tabify: tabifyAggResponse
};
}
export const aggResponseIndex = {
hierarchical: buildHierarchicalData,
pointSeries: buildPointSeriesData,
tabify: tabifyAggResponse
};

View file

@ -28,7 +28,7 @@ describe('makeFakeXAspect', function () {
expect(aspect)
.to.have.property('accessor', -1)
.and.have.property('title', 'All docs')
.and.have.property('fieldFormatter')
.and.have.property('format')
.and.have.property('params');
});

View file

@ -17,14 +17,11 @@
* under the License.
*/
import _ from 'lodash';
import expect from 'expect.js';
import { getPoint } from '../_get_point';
describe('getPoint', function () {
const truthFormatted = _.constant(true);
const table = {
columns: [{ id: '0' }, { id: '1' }, { id: '3' }],
rows: [
@ -80,7 +77,7 @@ describe('getPoint', function () {
});
it('properly unwraps and scales values', function () {
const seriesAspect = [{ accessor: 1, fieldFormatter: _.identity }];
const seriesAspect = [{ accessor: 1 }];
const point = getPoint(table, xAspect, seriesAspect, yScale, row, 0, yAspect);
expect(point)
@ -90,12 +87,12 @@ describe('getPoint', function () {
});
it('properly formats series values', function () {
const seriesAspect = [{ accessor: 1, fieldFormatter: truthFormatted }];
const seriesAspect = [{ accessor: 1, format: { id: 'number', params: { pattern: '$' } } }];
const point = getPoint(table, xAspect, seriesAspect, yScale, row, 0, yAspect);
expect(point)
.to.have.property('x', 1)
.and.have.property('series', 'true')
.and.have.property('series', '$2')
.and.have.property('y', 3);
});
});

View file

@ -26,7 +26,7 @@ describe('initXAxis', function () {
const baseChart = {
aspects: {
x: [{
fieldFormatter: _.constant({}),
format: {},
title: 'label',
params: {}
}]
@ -38,7 +38,7 @@ describe('initXAxis', function () {
initXAxis(chart);
expect(chart)
.to.have.property('xAxisLabel', 'label')
.and.have.property('xAxisFormatter', chart.aspects.x[0].fieldFormatter);
.and.have.property('xAxisFormat', chart.aspects.x[0].format);
});
it('makes the chart ordered if the agg is ordered', function () {
@ -48,7 +48,7 @@ describe('initXAxis', function () {
initXAxis(chart);
expect(chart)
.to.have.property('xAxisLabel', 'label')
.and.have.property('xAxisFormatter', chart.aspects.x[0].fieldFormatter)
.and.have.property('xAxisFormat', chart.aspects.x[0].format)
.and.have.property('ordered');
});
@ -59,7 +59,7 @@ describe('initXAxis', function () {
initXAxis(chart);
expect(chart)
.to.have.property('xAxisLabel', 'label')
.and.have.property('xAxisFormatter', chart.aspects.x[0].fieldFormatter)
.and.have.property('xAxisFormat', chart.aspects.x[0].format)
.and.have.property('ordered');
expect(chart.ordered)

View file

@ -26,8 +26,8 @@ describe('initYAxis', function () {
const baseChart = {
aspects: {
y: [
{ title: 'y1', fieldFormatter: v => v },
{ title: 'y2', fieldFormatter: v => v },
{ title: 'y1', format: {} },
{ title: 'y2', format: {} },
],
x: [{
title: 'x'
@ -42,7 +42,7 @@ describe('initYAxis', function () {
it('sets the yAxisFormatter the the field formats convert fn', function () {
const chart = _.cloneDeep(singleYBaseChart);
initYAxis(chart);
expect(chart).to.have.property('yAxisFormatter');
expect(chart).to.have.property('yAxisFormat');
});
it('sets the yAxisLabel', function () {
@ -57,10 +57,10 @@ describe('initYAxis', function () {
const chart = _.cloneDeep(baseChart);
initYAxis(chart);
expect(chart).to.have.property('yAxisFormatter');
expect(chart.yAxisFormatter)
.to.be(chart.aspects.y[0].fieldFormatter)
.and.not.be(chart.aspects.y[1].fieldFormatter);
expect(chart).to.have.property('yAxisFormat');
expect(chart.yAxisFormat)
.to.be(chart.aspects.y[0].format)
.and.not.be(chart.aspects.y[1].format);
});
it('does not set the yAxisLabel, it does not make sense to put multiple labels on the same axis', function () {

View file

@ -19,20 +19,11 @@
import _ from 'lodash';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import { AggResponsePointSeriesProvider } from '../point_series';
import { buildPointSeriesData } from '../point_series';
describe('pointSeriesChartDataFromTable', function () {
this.slow(1000);
let pointSeriesChartDataFromTable;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
pointSeriesChartDataFromTable = Private(AggResponsePointSeriesProvider);
}));
it('handles a table with just a count', function () {
const table = {
columns: [{ id: '0' }],
@ -40,7 +31,7 @@ describe('pointSeriesChartDataFromTable', function () {
{ '0': 100 }
],
};
const chartData = pointSeriesChartDataFromTable(table, {
const chartData = buildPointSeriesData(table, {
y: [{
accessor: 0,
params: {},
@ -72,7 +63,7 @@ describe('pointSeriesChartDataFromTable', function () {
y: [{ accessor: 1, params: {} }],
};
const chartData = pointSeriesChartDataFromTable(table, dimensions);
const chartData = buildPointSeriesData(table, dimensions);
expect(chartData).to.be.an('object');
expect(chartData.series).to.be.an('array');
@ -97,7 +88,7 @@ describe('pointSeriesChartDataFromTable', function () {
y: [{ accessor: 1, params: {} }, { accessor: 2, params: {} }],
};
const chartData = pointSeriesChartDataFromTable(table, dimensions);
const chartData = buildPointSeriesData(table, dimensions);
expect(chartData).to.be.an('object');
expect(chartData.series).to.be.an('array');
expect(chartData.series).to.have.length(2);
@ -128,7 +119,7 @@ describe('pointSeriesChartDataFromTable', function () {
y: [{ accessor: 2, params: {} }, { accessor: 3, params: {} }],
};
const chartData = pointSeriesChartDataFromTable(table, dimensions);
const chartData = buildPointSeriesData(table, dimensions);
expect(chartData).to.be.an('object');
expect(chartData.series).to.be.an('array');
// one series for each extension, and then one for each metric inside

View file

@ -43,25 +43,6 @@ describe('orderedDateAxis', function () {
}
};
describe('xAxisFormatter', function () {
it('sets the xAxisFormatter', function () {
const args = _.cloneDeep(baseArgs);
orderedDateAxis(args.chart);
expect(args.chart).to.have.property('xAxisFormatter');
expect(args.chart.xAxisFormatter).to.be.a('function');
});
it('formats values using moment, and returns strings', function () {
const args = _.cloneDeep(baseArgs);
orderedDateAxis(args.chart);
const val = '2014-08-06T12:34:01';
expect(args.chart.xAxisFormatter(val))
.to.be(moment(val).format('hh:mm:ss'));
});
});
describe('ordered object', function () {
it('sets date: true', function () {
const args = _.cloneDeep(baseArgs);

View file

@ -29,7 +29,7 @@ describe('tooltipFormatter', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
tooltipFormatter = Private(PointSeriesTooltipFormatter);
tooltipFormatter = Private(PointSeriesTooltipFormatter)();
}));
function cell($row, i) {

View file

@ -17,7 +17,7 @@
* under the License.
*/
export function addToSiri(series, point, id, label, formatter) {
export function addToSiri(series, point, id, label, format) {
id = id == null ? '' : id + '';
if (series.has(id)) {
@ -30,6 +30,6 @@ export function addToSiri(series, point, id, label, formatter) {
label: label == null ? id : label,
count: 0,
values: [point],
yAxisFormatter: formatter,
format: format,
});
}

View file

@ -27,6 +27,6 @@ export function makeFakeXAspect() {
defaultMessage: 'All docs'
}),
params: {},
fieldFormatter: () => '',
format: {}
};
}

View file

@ -17,7 +17,6 @@
* under the License.
*/
import { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities';
import { makeFakeXAspect } from './_fake_x_aspect';
/**
@ -40,7 +39,6 @@ export function getAspects(table, dimensions) {
if (!column) {
return;
}
const formatter = getFormat(d.format);
if (!aspects[name]) {
aspects[name] = [];
}
@ -48,7 +46,7 @@ export function getAspects(table, dimensions) {
accessor: column.id,
column: d.accessor,
title: column.name,
fieldFormatter: val => formatter.convert(val, 'text'),
format: d.format,
params: d.params,
});
});

View file

@ -18,6 +18,7 @@
*/
import _ from 'lodash';
import { getFormat } from '../../visualize/loader/pipeline_helpers/utilities';
export function getPoint(table, x, series, yScale, row, rowIndex, y, z) {
const zRow = z && row[z.accessor];
@ -72,7 +73,10 @@ export function getPoint(table, x, series, yScale, row, rowIndex, y, z) {
if (series) {
const seriesArray = series.length ? series : [ series ];
point.series = seriesArray.map(s => s.fieldFormatter(row[s.accessor])).join(' - ');
point.series = seriesArray.map(s => {
const fieldFormatter = getFormat(s.format);
return fieldFormatter.convert(row[s.accessor]);
}).join(' - ');
} else if (y) {
// If the data is not split up with a series aspect, then
// each point's "series" becomes the y-agg that produced it

View file

@ -32,7 +32,7 @@ export function getSeries(table, chart) {
if (!multiY) {
const point = partGetPoint(row, rowIndex, aspects.y[0], aspects.z);
const id = `${point.series}-${aspects.y[0].accessor}`;
if (point) addToSiri(series, point, id, point.series, aspects.y[0].fieldFormatter);
if (point) addToSiri(series, point, id, point.series, aspects.y[0].format);
return;
}
@ -52,7 +52,7 @@ export function getSeries(table, chart) {
seriesLabel = prefix + seriesLabel;
}
addToSiri(series, point, seriesId, seriesLabel, y.fieldFormatter);
addToSiri(series, point, seriesId, seriesLabel, y.format);
});
}, new Map())

View file

@ -20,7 +20,7 @@
export function initXAxis(chart) {
const x = chart.aspects.x[0];
chart.xAxisFormatter = x.fieldFormatter ? x.fieldFormatter : String;
chart.xAxisFormat = x.format;
chart.xAxisLabel = x.title;
if (x.params.date) {
chart.ordered = {

View file

@ -22,17 +22,17 @@ export function initYAxis(chart) {
if (Array.isArray(y)) {
// TODO: vis option should allow choosing this format
chart.yAxisFormatter = y[0].fieldFormatter;
chart.yAxisFormat = y[0].format;
chart.yAxisLabel = y.length > 1 ? '' : y[0].title;
}
const z = chart.aspects.series;
if (z) {
if (Array.isArray(z)) {
chart.zAxisFormatter = z[0].fieldFormatter;
chart.zAxisFormat = z[0].format;
chart.zAxisLabel = '';
} else {
chart.zAxisFormatter = z.fieldFormatter;
chart.zAxisFormat = z.format;
chart.zAxisLabel = z.title;
}
}

View file

@ -17,15 +17,11 @@
* under the License.
*/
import moment from 'moment';
// import moment from 'moment';
export function orderedDateAxis(chart) {
const x = chart.aspects.x[0];
const { format, bounds } = x.params;
chart.xAxisFormatter = function (val) {
return moment(val).format(format);
};
const { bounds } = x.params;
chart.ordered.date = true;

View file

@ -18,6 +18,7 @@
*/
import $ from 'jquery';
import { getFormat } from '../../visualize/loader/pipeline_helpers/utilities';
export function PointSeriesTooltipFormatter($compile, $rootScope) {
@ -25,38 +26,41 @@ export function PointSeriesTooltipFormatter($compile, $rootScope) {
const $tooltip = $(require('ui/agg_response/point_series/_tooltip.html'));
$compile($tooltip)($tooltipScope);
return function tooltipFormatter(event) {
const data = event.data;
const datum = event.datum;
if (!datum) return '';
return function () {
return function tooltipFormatter(event) {
const data = event.data;
const datum = event.datum;
if (!datum) return '';
const details = $tooltipScope.details = [];
const details = $tooltipScope.details = [];
const addDetail = (label, value) => details.push({ label, value });
const addDetail = (label, value) => details.push({ label, value });
datum.extraMetrics.forEach(metric => {
addDetail(metric.label, metric.value);
});
datum.extraMetrics.forEach(metric => {
addDetail(metric.label, metric.value);
});
if (datum.x) {
addDetail(data.xAxisLabel, data.xAxisFormatter(datum.x));
}
if (datum.y) {
const value = datum.yScale ? datum.yScale * datum.y : datum.y;
addDetail(data.yAxisLabel, data.yAxisFormatter(value));
}
if (datum.z) {
addDetail(data.zAxisLabel, data.zAxisFormatter(datum.z));
}
if (datum.series && datum.parent) {
const dimension = datum.parent;
addDetail(dimension.title, dimension.fieldFormatter(datum.series));
}
if (datum.tableRaw) {
addDetail(datum.tableRaw.title, datum.tableRaw.value);
}
if (datum.x) {
addDetail(data.xAxisLabel, data.xAxisFormatter(datum.x));
}
if (datum.y) {
const value = datum.yScale ? datum.yScale * datum.y : datum.y;
addDetail(data.yAxisLabel, data.yAxisFormatter(value));
}
if (datum.z) {
addDetail(data.zAxisLabel, data.zAxisFormatter(datum.z));
}
if (datum.series && datum.parent) {
const dimension = datum.parent;
const seriesFormatter = getFormat(dimension.format);
addDetail(dimension.title, seriesFormatter.convert(datum.series));
}
if (datum.tableRaw) {
addDetail(datum.tableRaw.title, datum.tableRaw.value);
}
$tooltipScope.$apply();
return $tooltip[0].outerHTML;
$tooltipScope.$apply();
return $tooltip[0].outerHTML;
};
};
}

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { AggResponsePointSeriesProvider } from './point_series';
export { buildPointSeriesData } from './point_series';

View file

@ -22,29 +22,22 @@ import { getAspects } from './_get_aspects';
import { initYAxis } from './_init_y_axis';
import { initXAxis } from './_init_x_axis';
import { orderedDateAxis } from './_ordered_date_axis';
import { PointSeriesTooltipFormatter } from './_tooltip_formatter';
export function AggResponsePointSeriesProvider(Private) {
const tooltipFormatter = Private(PointSeriesTooltipFormatter);
return function pointSeriesChartDataFromTable(table, dimensions) {
const chart = {
aspects: getAspects(table, dimensions),
tooltipFormatter
};
initXAxis(chart);
initYAxis(chart);
if (chart.aspects.x[0].params.date) {
orderedDateAxis(chart);
}
chart.series = getSeries(table, chart);
delete chart.aspects;
return chart;
export const buildPointSeriesData = (table, dimensions) => {
const chart = {
aspects: getAspects(table, dimensions),
};
}
initXAxis(chart);
initYAxis(chart);
if (chart.aspects.x[0].params.date) {
orderedDateAxis(chart);
}
chart.series = getSeries(table, chart);
delete chart.aspects;
return chart;
};

View file

@ -52,6 +52,12 @@ describe('brushEvent', () => {
};
const baseEvent = {
aggConfigs: [{
params: {},
getIndexPattern: () => ({
timeFieldName: 'time',
})
}],
data: {
fieldFormatter: _.constant({}),
series: [
@ -64,12 +70,6 @@ describe('brushEvent', () => {
columns: [
{
id: '1',
aggConfig: {
params: {},
getIndexPattern: () => ({
timeFieldName: 'time',
})
}
},
]
}
@ -110,7 +110,7 @@ describe('brushEvent', () => {
beforeEach(() => {
dateEvent = _.cloneDeep(baseEvent);
dateEvent.data.series[0].values[0].xRaw.table.columns[0].aggConfig.params.field = dateField;
dateEvent.aggConfigs[0].params.field = dateField;
dateEvent.data.ordered = { date: true };
});
@ -144,7 +144,7 @@ describe('brushEvent', () => {
beforeEach(() => {
dateEvent = _.cloneDeep(baseEvent);
dateEvent.data.series[0].values[0].xRaw.table.columns[0].aggConfig.params.field = dateField;
dateEvent.aggConfigs[0].params.field = dateField;
dateEvent.data.ordered = { date: true };
});
@ -200,7 +200,7 @@ describe('brushEvent', () => {
beforeEach(() => {
numberEvent = _.cloneDeep(baseEvent);
numberEvent.data.series[0].values[0].xRaw.table.columns[0].aggConfig.params.field = numberField;
numberEvent.aggConfigs[0].params.field = numberField;
numberEvent.data.ordered = { date: false };
});

View file

@ -30,8 +30,10 @@ export function onBrushEvent(event, $state) {
if (!xRaw) return;
const column = xRaw.table.columns[xRaw.column];
if (!column) return;
const indexPattern = column.aggConfig.getIndexPattern();
const field = column.aggConfig.params.field;
const aggConfig = event.aggConfigs[xRaw.column];
if (!aggConfig) return;
const indexPattern = aggConfig.getIndexPattern();
const field = aggConfig.params.field;
if (!field) return;
const fieldName = field.name;

View file

@ -18,21 +18,13 @@
*/
import _ from 'lodash';
import ngMock from 'ng_mock';
import expect from 'expect.js';
import sinon from 'sinon';
import { AggResponseIndexProvider } from '../../../agg_response';
import { VislibSeriesResponseHandlerProvider } from '../../response_handlers/vislib';
import { aggResponseIndex } from '../../../agg_response';
import { VislibSeriesResponseHandlerProvider as vislibReponseHandler } from '../../response_handlers/vislib';
describe('renderbot#buildChartData', function () {
let buildChartData;
let aggResponse;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
aggResponse = Private(AggResponseIndexProvider);
buildChartData = Private(VislibSeriesResponseHandlerProvider).handler;
}));
const buildChartData = vislibReponseHandler().handler;
describe('for hierarchical vis', function () {
it('defers to hierarchical aggResponse converter', function () {
@ -43,7 +35,7 @@ describe('renderbot#buildChartData', function () {
}
};
const stub = sinon.stub(aggResponse, 'hierarchical').returns(football);
const stub = sinon.stub(aggResponseIndex, 'hierarchical').returns(football);
expect(buildChartData.call(renderbot, football)).to.be(football);
expect(stub).to.have.property('callCount', 1);
expect(stub.firstCall.args[0]).to.be(renderbot.vis);
@ -60,7 +52,7 @@ describe('renderbot#buildChartData', function () {
};
const football = { tables: [], hits: { total: 1 } };
const stub = sinon.stub(aggResponse, 'tabify').returns(football);
const stub = sinon.stub(aggResponseIndex, 'tabify').returns(football);
expect(buildChartData.call(renderbot, football)).to.eql({ rows: [], hits: 1 });
expect(stub).to.have.property('callCount', 1);
expect(stub.firstCall.args[0]).to.be(renderbot.vis);
@ -80,7 +72,7 @@ describe('renderbot#buildChartData', function () {
const esResp = { hits: { total: 1 } };
const tabbed = { tables: [ {}] };
sinon.stub(aggResponse, 'tabify').returns(tabbed);
sinon.stub(aggResponseIndex, 'tabify').returns(tabbed);
expect(buildChartData.call(renderbot, esResp)).to.eql(chart);
});
@ -98,7 +90,7 @@ describe('renderbot#buildChartData', function () {
}
};
sinon.stub(aggResponse, 'tabify').returns({
sinon.stub(aggResponseIndex, 'tabify').returns({
tables: [
{
aggConfig: { params: { row: true } },

View file

@ -17,8 +17,8 @@
* under the License.
*/
import { BuildHierarchicalDataProvider } from '../../agg_response/hierarchical/build_hierarchical_data';
import { AggResponsePointSeriesProvider } from '../../agg_response/point_series/point_series';
import { buildHierarchicalData } from '../../agg_response/hierarchical/build_hierarchical_data';
import { buildPointSeriesData } from '../../agg_response/point_series/point_series';
import { VisResponseHandlersRegistryProvider } from '../../registry/vis_response_handlers';
import { LegacyResponseHandlerProvider as legacyResponseHandlerProvider } from './legacy';
@ -78,18 +78,14 @@ const handlerFunction = function (convertTable) {
};
};
const VislibSeriesResponseHandlerProvider = function (Private) {
const buildPointSeriesData = Private(AggResponsePointSeriesProvider);
const VislibSeriesResponseHandlerProvider = function () {
return {
name: 'vislib_series',
handler: handlerFunction(buildPointSeriesData)
};
};
const VislibSlicesResponseHandlerProvider = function (Private) {
const buildHierarchicalData = Private(BuildHierarchicalDataProvider);
const VislibSlicesResponseHandlerProvider = function () {
return {
name: 'vislib_slices',
handler: handlerFunction(buildHierarchicalData)

View file

@ -96,7 +96,9 @@ export function VisProvider(Private, indexPatterns, getAppState) {
updateVisualizationConfig(state.params, this.params);
this.aggs = new AggConfigs(this.indexPattern, state.aggs, this.type.schemas.all);
if (state.aggs || !this.aggs) {
this.aggs = new AggConfigs(this.indexPattern, state.aggs, this.type.schemas.all);
}
}
setState(state, updateCurrentState = true) {

View file

@ -25,14 +25,12 @@ import 'plugins/kbn_vislib_vis_types/controls/gauge_options';
import 'plugins/kbn_vislib_vis_types/controls/point_series';
import './vislib_vis_legend';
import { BaseVisTypeProvider } from './base_vis_type';
import { AggResponsePointSeriesProvider } from '../../agg_response/point_series/point_series';
import VislibProvider from '../../vislib';
import { VisFiltersProvider } from '../vis_filters';
import $ from 'jquery';
import { defaultsDeep } from 'lodash';
export function VislibVisTypeProvider(Private, $rootScope, $timeout, $compile) {
const pointSeries = Private(AggResponsePointSeriesProvider);
const vislib = Private(VislibProvider);
const visFilters = Private(VisFiltersProvider);
const BaseVisType = Private(BaseVisTypeProvider);
@ -119,9 +117,6 @@ export function VislibVisTypeProvider(Private, $rootScope, $timeout, $compile) {
opts.responseHandler = 'vislib_series';
opts.responseHandlerConfig = { asAggConfigResults: true };
}
if (!opts.responseConverter) {
opts.responseConverter = pointSeries;
}
opts.events = defaultsDeep({}, opts.events, {
filterBucket: {
defaultAction: visFilters.filter,

View file

@ -237,7 +237,7 @@ describe('Vislib Data Class Test Suite', function () {
describe('getVisData', function () {
it('should return the rows property', function () {
const visData = data.getVisData();
expect(visData).to.eql(geohashGridData.rows);
expect(visData[0].title).to.eql(geohashGridData.rows[0].title);
});
});

View file

@ -23,6 +23,7 @@ import { VislibComponentsZeroInjectionInjectZerosProvider } from '../components/
import { VislibComponentsZeroInjectionOrderedXKeysProvider } from '../components/zero_injection/ordered_x_keys';
import { VislibComponentsLabelsLabelsProvider } from '../components/labels/labels';
import { VislibComponentsColorColorProvider } from '../../vis/components/color/color';
import { getFormat } from '../../visualize/loader/pipeline_helpers/utilities';
export function VislibLibDataProvider(Private) {
@ -59,6 +60,7 @@ export function VislibLibDataProvider(Private) {
newData[key] = data[key];
} else {
newData[key] = data[key].map(seri => {
const converter = getFormat(seri.format);
return {
id: seri.id,
label: seri.label,
@ -68,11 +70,19 @@ export function VislibLibDataProvider(Private) {
newVal.series = val.series || seri.label;
return newVal;
}),
yAxisFormatter: seri.yAxisFormatter,
yAxisFormatter: val => converter.convert(val)
};
});
}
});
const xConverter = getFormat(newData.xAxisFormat);
const yConverter = getFormat(newData.yAxisFormat);
const zConverter = getFormat(newData.zAxisFormat);
newData.xAxisFormatter = val => xConverter.convert(val);
newData.yAxisFormatter = val => yConverter.convert(val);
newData.zAxisFormatter = val => zConverter.convert(val);
return newData;
};

View file

@ -22,6 +22,9 @@ import _ from 'lodash';
import { dataLabel } from '../lib/_data_label';
import { VislibLibDispatchProvider } from '../lib/dispatch';
import { TooltipProvider } from '../../vis/components/tooltip';
import { getFormat } from '../../visualize/loader/pipeline_helpers/utilities';
import { HierarchicalTooltipFormatterProvider } from '../../agg_response/hierarchical/_hierarchical_tooltip_formatter';
import { PointSeriesTooltipFormatter } from '../../agg_response/point_series/_tooltip_formatter';
export function VislibVisualizationsChartProvider(Private) {
@ -45,12 +48,16 @@ export function VislibVisualizationsChartProvider(Private) {
const events = this.events = new Dispatch(handler);
const fieldFormatter = getFormat(this.handler.data.get('tooltipFormatter'));
const tooltipFormatterProvider = this.handler.visConfig.get('type') === 'pie' ?
Private(HierarchicalTooltipFormatterProvider) : Private(PointSeriesTooltipFormatter);
const tooltipFormatter = tooltipFormatterProvider(fieldFormatter);
if (this.handler.visConfig && this.handler.visConfig.get('addTooltip', false)) {
const $el = this.handler.el;
const formatter = this.handler.data.get('tooltipFormatter');
// Add tooltip
this.tooltip = new Tooltip('chart', $el, formatter, events);
this.tooltip = new Tooltip('chart', $el, tooltipFormatter, events);
this.tooltips.push(this.tooltip);
}

View file

@ -9,8 +9,8 @@
.visualization {
display: flex;
flex-direction: column;
height: auto;
width: 100%;
height: 100%;
overflow: auto;
position: relative;
padding: $euiSizeS;

View file

@ -56,16 +56,11 @@ export class Visualization extends React.Component<VisualizationProps> {
constructor(props: VisualizationProps) {
super(props);
const { vis, uiState, listenOnChange } = props;
vis._setUiState(props.uiState);
if (listenOnChange) {
uiState.on('change', this.onUiStateChanged);
}
props.vis._setUiState(props.uiState);
}
public render() {
const { vis, visData, onInit, uiState } = this.props;
const { vis, visData, onInit, uiState, listenOnChange } = this.props;
const noResults = this.showNoResultsMessage(vis, visData);
const requestError = shouldShowRequestErrorMessage(vis, visData);
@ -77,7 +72,13 @@ export class Visualization extends React.Component<VisualizationProps> {
) : noResults ? (
<VisualizationNoResults onInit={onInit} />
) : (
<VisualizationChart vis={vis} visData={visData} onInit={onInit} uiState={uiState} />
<VisualizationChart
vis={vis}
visData={visData}
onInit={onInit}
uiState={uiState}
listenOnChange={listenOnChange}
/>
)}
</div>
);
@ -89,28 +90,4 @@ export class Visualization extends React.Component<VisualizationProps> {
}
return true;
}
public componentWillUnmount() {
this.props.uiState.off('change', this.onUiStateChanged);
}
public componentDidUpdate(prevProps: VisualizationProps) {
const { listenOnChange } = this.props;
// If the listenOnChange prop changed, we need to register or deregister from uiState
if (prevProps.listenOnChange !== listenOnChange) {
if (listenOnChange) {
this.props.uiState.on('change', this.onUiStateChanged);
} else {
this.props.uiState.off('change', this.onUiStateChanged);
}
}
}
/**
* In case something in the uiState changed, we need to force a redraw of
* the visualization, since these changes could effect visualization rendering.
*/
private onUiStateChanged() {
this.forceUpdate();
}
}

View file

@ -32,6 +32,7 @@ interface VisualizationChartProps {
uiState: PersistedState;
vis: Vis;
visData: any;
listenOnChange: boolean;
}
class VisualizationChart extends React.Component<VisualizationChartProps> {
@ -123,6 +124,10 @@ class VisualizationChart extends React.Component<VisualizationChartProps> {
this.resizeChecker = new ResizeChecker(this.containerDiv.current);
this.resizeChecker.on('resize', () => this.startRenderVisualization());
if (this.props.listenOnChange) {
this.props.uiState.on('change', this.onUiStateChanged);
}
this.startRenderVisualization();
}
@ -142,6 +147,10 @@ class VisualizationChart extends React.Component<VisualizationChartProps> {
}
}
private onUiStateChanged = () => {
this.startRenderVisualization();
};
private startRenderVisualization(): void {
if (this.containerDiv.current && this.chartDiv.current) {
this.renderSubject.next({

View file

@ -18,7 +18,9 @@
*/
import { EventEmitter } from 'events';
import { debounce, forEach } from 'lodash';
import { debounce, forEach, get } from 'lodash';
// @ts-ignore
import { renderFunctionsRegistry } from 'plugins/interpreter/render_functions_registry';
import * as Rx from 'rxjs';
import { share } from 'rxjs/operators';
import { Inspector } from '../../inspector';
@ -62,6 +64,7 @@ export class EmbeddedVisualizeHandler {
public readonly data$: Rx.Observable<any>;
public readonly inspectorAdapters: Adapters = {};
private vis: Vis;
private handlers: any;
private loaded: boolean = false;
private destroyed: boolean = false;
@ -126,6 +129,12 @@ export class EmbeddedVisualizeHandler {
}
this.uiState = this.vis.getUiState();
this.handlers = {
vis: this.vis,
uiState: this.uiState,
onDestroy: (fn: () => never) => (this.handlers.destroyFn = fn),
};
this.vis.on('update', this.handleVisUpdate);
this.vis.on('reload', this.reload);
this.uiState.on('change', this.onUiStateChange);
@ -146,8 +155,9 @@ export class EmbeddedVisualizeHandler {
}
});
this.vis.eventsSubject = new Rx.Subject();
this.events$ = this.vis.eventsSubject.asObservable().pipe(share());
this.handlers.eventsSubject = new Rx.Subject();
this.vis.eventsSubject = this.handlers.eventsSubject;
this.events$ = this.handlers.eventsSubject.asObservable().pipe(share());
this.events$.subscribe(event => {
if (this.actions[event.name]) {
event.data.aggConfigs = getTableAggs(this.vis);
@ -215,6 +225,9 @@ export class EmbeddedVisualizeHandler {
this.uiState.off('change', this.onUiStateChange);
visualizationLoader.destroy(this.element);
this.renderCompleteHelper.destroy();
if (this.handlers.destroyFn) {
this.handlers.destroyFn();
}
}
/**
@ -230,21 +243,20 @@ export class EmbeddedVisualizeHandler {
* renders visualization with provided data
* @param visData: visualization data
*/
public render = (pipelineResponse: any = null) => {
let visData;
if (pipelineResponse) {
if (!pipelineResponse.value) {
throw new Error(pipelineResponse.error);
}
visData = pipelineResponse.value.visData || pipelineResponse.value;
if (pipelineResponse.value.visConfig) {
this.vis.params = pipelineResponse.value.visConfig.params;
}
public render = (pipelineResponse: any = {}) => {
// TODO: we have this weird situation when we need to render first, and then we call fetch and render ....
// we need to get rid of that ....
const renderer = renderFunctionsRegistry.get(get(pipelineResponse, 'as', 'visualization'));
if (!renderer) {
return;
}
return visualizationLoader
.render(this.element, this.vis, visData, this.uiState, {
listenOnChange: false,
})
renderer
.render(
this.element,
pipelineResponse.value || { visType: this.vis.type.name },
this.handlers
)
.then(() => {
if (!this.loaded) {
this.loaded = true;

View file

@ -22,9 +22,9 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function without splits or buckets 1`] = `"kibana_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[0,1],\\"buckets\\":[]}}' "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tagcloud function with buckets 1`] = `"tagcloud visConfig='{\\"metrics\\":[0],\\"bucket\\":1}' "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tagcloud function with buckets 1`] = `"tagcloud visConfig='{\\"metric\\":0,\\"bucket\\":1}' "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tagcloud function without buckets 1`] = `"tagcloud visConfig='{\\"metrics\\":[0]}' "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tagcloud function without buckets 1`] = `"tagcloud visConfig='{\\"metric\\":0}' "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tile_map function 1`] = `"tilemap visConfig='{\\"metric\\":0,\\"geohash\\":1,\\"geocentroid\\":3}' "`;

View file

@ -139,7 +139,8 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
});
describe('handles tagcloud function', () => {
const params = { metrics: {} };
const params = {};
it('without buckets', () => {
const schemas = { metric: [0] };
const actual = buildPipelineVisFunction.tagcloud({ params }, schemas);

View file

@ -234,7 +234,7 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = {
},
tagcloud: (visState, schemas) => {
const visConfig = visState.params;
visConfig.metrics = schemas.metric;
visConfig.metric = schemas.metric[0];
if (schemas.segment) {
visConfig.bucket = schemas.segment[0];
}

View file

@ -103,6 +103,9 @@ export const getFormat = (mapping: any) => {
};
export const getTableAggs = (vis: Vis): AggConfig[] => {
if (!vis.aggs || !vis.aggs.getResponseAggs) {
return [];
}
const columns = tabifyGetColumns(vis.aggs.getResponseAggs(), !vis.isHierarchical());
return columns.map((c: any) => c.aggConfig);
};

View file

@ -0,0 +1,151 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* @name Vis
*
* @description This class consists of aggs, params, listeners, title, and type.
* - Aggs: Instances of AggConfig.
* - Params: The settings in the Options tab.
*
* Not to be confused with vislib/vis.js.
*/
import { EventEmitter } from 'events';
import _ from 'lodash';
import { VisTypesRegistryProvider } from '../../registry/vis_types';
import { PersistedState } from '../../persisted_state';
import { FilterBarQueryFilterProvider } from '../../filter_bar/query_filter';
import { timefilter } from 'ui/timefilter';
export function VisProvider(Private, indexPatterns, getAppState) {
const visTypes = Private(VisTypesRegistryProvider);
const queryFilter = Private(FilterBarQueryFilterProvider);
class Vis extends EventEmitter {
constructor(visState = { type: 'histogram' }) {
super();
this._setUiState(new PersistedState());
this.setState(visState);
// Session state is for storing information that is transitory, and will not be saved with the visualization.
// For instance, map bounds, which depends on the view port, browser window size, etc.
this.sessionState = {};
this.API = {
indexPatterns: indexPatterns,
timeFilter: timefilter,
queryFilter: queryFilter,
events: {
filter: data => {
if (!this.eventsSubject) return;
this.eventsSubject.next({ name: 'filterBucket', data });
},
brush: data => {
if (!this.eventsSubject) return;
this.eventsSubject.next({ name: 'brush', data });
},
},
getAppState,
};
}
setState(state) {
this.title = state.title || '';
const type = state.type || this.type;
if (_.isString(type)) {
this.type = visTypes.byName[type];
if (!this.type) {
throw new Error(`Invalid type "${type}"`);
}
} else {
this.type = type;
}
this.params = _.defaultsDeep({},
_.cloneDeep(state.params || {}),
_.cloneDeep(this.type.visConfig.defaults || {})
);
}
setCurrentState(state) {
this.setState(state);
}
getState() {
return {
title: this.title,
type: this.type.name,
params: _.cloneDeep(this.params),
};
}
updateState() {
this.emit('update');
}
forceReload() {
this.emit('reload');
}
isHierarchical() {
if (_.isFunction(this.type.hierarchicalData)) {
return !!this.type.hierarchicalData(this);
} else {
return !!this.type.hierarchicalData;
}
}
hasUiState() {
return !!this.__uiState;
}
/***
* this should not be used outside of visualize
* @param uiState
* @private
*/
_setUiState(uiState) {
if (uiState instanceof PersistedState) {
this.__uiState = uiState;
}
}
getUiState() {
return this.__uiState;
}
/**
* Currently this is only used to extract map-specific information
* (e.g. mapZoom, mapCenter).
*/
uiStateVal(key, val) {
if (this.hasUiState()) {
if (_.isUndefined(val)) {
return this.__uiState.get(key);
}
return this.__uiState.set(key, val);
}
return val;
}
}
return Vis;
}