Charts plugin (combining ui/color_maps and EuiUtils) (#55469) (#56151)

* Combine ui/color_maps and EuiUtils into new Charts plugin
* EuiUtils is now the theme service
* ui/color_maps is now the colorMaps service
* Fix all imports of each to pull from new Charts plugin
* Add theme methods to both setup and start contracts
* Move and jestify heatMapColors tests
* Convert remaining js files to ts
* Move vis/color to Charts plugin
* Update missed visTypeVislib naming
This commit is contained in:
Nick Partridge 2020-01-28 09:45:46 -06:00 committed by GitHub
parent a9874f3a58
commit f67aabb8a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
107 changed files with 1454 additions and 953 deletions

View file

@ -12,6 +12,7 @@
"embeddableExamples": "examples/embeddable_examples",
"share": "src/plugins/share",
"home": "src/plugins/home",
"charts": "src/plugins/charts",
"esUi": "src/plugins/es_ui_shared",
"devTools": "src/plugins/dev_tools",
"expressions": "src/plugins/expressions",

View file

@ -32,10 +32,10 @@ import {
} from 'src/plugins/data/public';
import { createSavedSearchesLoader } from './saved_searches';
import { DiscoverStartPlugins } from './plugin';
import { EuiUtilsStart } from '../../../../../plugins/eui_utils/public';
import { SharePluginStart } from '../../../../../plugins/share/public';
import { SavedSearch } from './np_ready/types';
import { DocViewsRegistry } from './np_ready/doc_views/doc_views_registry';
import { ChartsPluginStart } from '../../../../../plugins/charts/public';
export interface DiscoverServices {
addBasePath: (path: string) => string;
@ -45,7 +45,7 @@ export interface DiscoverServices {
data: DataPublicPluginStart;
docLinks: DocLinksStart;
docViewsRegistry: DocViewsRegistry;
eui_utils: EuiUtilsStart;
theme: ChartsPluginStart['theme'];
filterManager: FilterManager;
indexPatterns: IndexPatternsContract;
inspector: unknown;
@ -77,7 +77,7 @@ export async function buildServices(
data: plugins.data,
docLinks: core.docLinks,
docViewsRegistry,
eui_utils: plugins.eui_utils,
theme: plugins.charts.theme,
filterManager: plugins.data.query.filterManager,
getSavedSearchById: async (id: string) => savedObjectService.get(id),
getSavedSearchUrlById: async (id: string) => savedObjectService.urlFor(id),

View file

@ -114,13 +114,13 @@ export class DiscoverHistogram extends Component<DiscoverHistogramProps, Discove
private subscription?: Subscription;
public state = {
chartsTheme: getServices().eui_utils.getChartsThemeDefault(),
chartsTheme: getServices().theme.chartsDefaultTheme,
};
componentDidMount() {
this.subscription = getServices()
.eui_utils.getChartsTheme$()
.subscribe((chartsTheme: EuiChartThemeType['theme']) => this.setState({ chartsTheme }));
this.subscription = getServices().theme.chartsTheme$.subscribe(
(chartsTheme: EuiChartThemeType['theme']) => this.setState({ chartsTheme })
);
}
componentWillUnmount() {

View file

@ -27,7 +27,7 @@ import { IEmbeddableStart, IEmbeddableSetup } from '../../../../../plugins/embed
import { getInnerAngularModule, getInnerAngularModuleEmbeddable } from './get_inner_angular';
import { setAngularModule, setServices } from './kibana_services';
import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public';
import { EuiUtilsStart } from '../../../../../plugins/eui_utils/public';
import { ChartsPluginStart } from '../../../../../plugins/charts/public';
import { buildServices } from './build_services';
import { SharePluginStart } from '../../../../../plugins/share/public';
import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public';
@ -56,7 +56,7 @@ export interface DiscoverStartPlugins {
uiActions: IUiActionsStart;
embeddable: IEmbeddableStart;
navigation: NavigationStart;
eui_utils: EuiUtilsStart;
charts: ChartsPluginStart;
data: DataPublicPluginStart;
share: SharePluginStart;
inspector: any;

View file

@ -52,7 +52,6 @@ import './visualize/legacy';
import './dashboard/legacy';
import './management';
import './dev_tools';
import 'ui/color_maps';
import 'ui/agg_response';
import 'ui/agg_types';
import { showAppRedirectNotification } from 'ui/notify';

View file

@ -23,11 +23,12 @@ import _ from 'lodash';
import d3 from 'd3';
import { i18n } from '@kbn/i18n';
import { KibanaMapLayer } from 'ui/vis/map/kibana_map_layer';
import { truncatedColorMaps } from 'ui/color_maps';
import * as topojson from 'topojson-client';
import { toastNotifications } from 'ui/notify';
import * as colorUtil from 'ui/vis/map/color_util';
import { truncatedColorMaps } from '../../../../plugins/charts/public';
const EMPTY_STYLE = {
weight: 1,
opacity: 0.6,

View file

@ -19,11 +19,11 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { truncatedColorSchemas as colorSchemas } from 'ui/color_maps';
import { mapToLayerWithId } from './util';
import { createRegionMapVisualization } from './region_map_visualization';
import { Status } from '../../visualizations/public';
import { RegionMapOptions } from './components/region_map_options';
import { truncatedColorSchemas } from '../../../../plugins/charts/public';
// TODO: reference to TILE_MAP plugin should be removed
import { ORIGIN } from '../../tile_map/common/origin';
@ -60,7 +60,7 @@ provided base maps, or add your own. Darker colors represent higher values.',
editorConfig: {
optionsTemplate: props => <RegionMapOptions {...props} serviceSettings={serviceSettings} />,
collections: {
colorSchemas,
colorSchemas: truncatedColorSchemas,
vectorLayers: [],
tmsLayers: [],
},

View file

@ -19,11 +19,11 @@
import { i18n } from '@kbn/i18n';
import ChoroplethLayer from './choropleth_layer';
import { truncatedColorMaps } from 'ui/color_maps';
import { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities';
import { toastNotifications } from 'ui/notify';
import { TileMapTooltipFormatter } from './tooltip_formatter';
import { truncatedColorMaps } from '../../../../plugins/charts/public';
// TODO: reference to TILE_MAP plugin should be removed
import { BaseMapsVisualizationProvider } from '../../tile_map/public/base_maps_visualization';

View file

@ -22,9 +22,10 @@ import _ from 'lodash';
import d3 from 'd3';
import $ from 'jquery';
import { EventEmitter } from 'events';
import { truncatedColorMaps } from 'ui/color_maps';
import * as colorUtil from 'ui/vis/map/color_util';
import { truncatedColorMaps } from '../../../../../plugins/charts/public';
export class ScaledCirclesMarkers extends EventEmitter {
constructor(
featureCollection,
@ -87,7 +88,7 @@ export class ScaledCirclesMarkers extends EventEmitter {
const quantizeDomain = min !== max ? [min, max] : d3.scale.quantize().domain();
this._legendColors = makeLegendColors(this._colorRamp);
this._legendColors = this.getLegendColors();
this._legendQuantizer = d3.scale
.quantize()
.domain(quantizeDomain)
@ -222,11 +223,11 @@ export class ScaledCirclesMarkers extends EventEmitter {
getBounds() {
return this._leafletLayer.getBounds();
}
}
function makeLegendColors(colorRampKey) {
const colorRamp = _.get(truncatedColorMaps[colorRampKey], 'value');
return colorUtil.getLegendColors(colorRamp);
getLegendColors() {
const colorRamp = _.get(truncatedColorMaps[this._colorRamp], 'value');
return colorUtil.getLegendColors(colorRamp);
}
}
function makeColorDarker(color) {

View file

@ -21,7 +21,6 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { truncatedColorSchemas as colorSchemas } from 'ui/color_maps';
import { convertToGeoJson } from 'ui/vis/map/convert_to_geojson';
import { createTileMapVisualization } from './tile_map_visualization';
@ -29,6 +28,7 @@ import { Status } from '../../visualizations/public';
import { TileMapOptions } from './components/tile_map_options';
import { MapTypes } from './map_types';
import { supportsCssFilters } from './css_filters';
import { truncatedColorSchemas } from '../../../../plugins/charts/public';
export function createTileMapTypeDefinition(dependencies) {
const CoordinateMapsVisualization = createTileMapVisualization(dependencies);
@ -63,7 +63,7 @@ export function createTileMapTypeDefinition(dependencies) {
responseHandler: convertToGeoJson,
editorConfig: {
collections: {
colorSchemas,
colorSchemas: truncatedColorSchemas,
legendPositions: [
{
value: 'bottomleft',

View file

@ -20,12 +20,12 @@
import React from 'react';
import { shallow } from 'enzyme';
import { MetricVisComponent } from './metric_vis_component';
import { Vis } from '../legacy_imports';
import { MetricVisComponent, MetricVisComponentProps } from './metric_vis_component';
jest.mock('ui/new_platform');
type Props = MetricVisComponent['props'];
type Props = MetricVisComponentProps;
const baseVisData = {
columns: [{ id: 'col-0', name: 'Count' }],

View file

@ -22,15 +22,16 @@ import React, { Component } from 'react';
import { isColorDark } from '@elastic/eui';
import { getHeatmapColors, getFormat, Vis } from '../legacy_imports';
import { getFormat, Vis } from '../legacy_imports';
import { MetricVisValue } from './metric_vis_value';
import { fieldFormats } from '../../../../../plugins/data/public';
import { Context } from '../metric_vis_fn';
import { KibanaDatatable } from '../../../../../plugins/expressions/public';
import { getHeatmapColors } from '../../../../../plugins/charts/public';
import { VisParams, MetricVisMetric } from '../types';
import { SchemaConfig } from '../../../visualizations/public';
interface MetricVisComponentProps {
export interface MetricVisComponentProps {
visParams: VisParams;
visData: Context;
vis: Vis;
@ -50,7 +51,6 @@ export class MetricVisComponent extends Component<MetricVisComponentProps> {
const to = isPercentageMode ? Math.round((100 * range.to) / max) : range.to;
labels.push(`${from} - ${to}`);
});
return labels;
}

View file

@ -27,6 +27,7 @@ import { plugin } from '.';
const plugins: Readonly<MetricVisPluginSetupDependencies> = {
expressions: npSetup.plugins.expressions,
visualizations: visualizationsSetup,
charts: npSetup.plugins.charts,
};
const pluginInstance = plugin({} as PluginInitializerContext);

View file

@ -18,8 +18,6 @@
*/
export { Vis, VisParams } from 'ui/vis';
export { vislibColorMaps, colorSchemas, ColorSchemas } from 'ui/color_maps';
export { getHeatmapColors } from 'ui/color_maps';
export { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities';
export { VisOptionsProps } from 'ui/vis/editors/default';
// @ts-ignore

View file

@ -18,8 +18,7 @@
*/
import { createMetricVisFn } from './metric_vis_fn';
// eslint-disable-next-line
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
jest.mock('ui/new_platform');

View file

@ -19,7 +19,6 @@
import { i18n } from '@kbn/i18n';
import { vislibColorMaps, ColorSchemas } from './legacy_imports';
import {
ExpressionFunction,
KibanaDatatable,
@ -29,6 +28,7 @@ import {
} from '../../../../plugins/expressions/public';
import { ColorModes } from '../../vis_type_vislib/public';
import { visType, DimensionsVisParam, VisParams } from './types';
import { ColorSchemas, vislibColorMaps } from '../../../../plugins/charts/public';
export type Context = KibanaDatatable;

View file

@ -29,7 +29,7 @@ import {
setup as visualizationsSetup,
start as visualizationsStart,
} from '../../visualizations/public/np_ready/public/legacy';
import { metricVisTypeDefinition } from './metric_vis_type';
import { createMetricVisTypeDefinition } from './metric_vis_type';
jest.mock('ui/new_platform');
@ -37,7 +37,7 @@ describe('metric_vis - createMetricVisTypeDefinition', () => {
let vis: Vis;
beforeAll(() => {
visualizationsSetup.types.createReactVisualization(metricVisTypeDefinition);
visualizationsSetup.types.createReactVisualization(createMetricVisTypeDefinition());
(npStart.plugins.data.fieldFormats.getType as jest.Mock).mockImplementation(() => {
return fieldFormats.UrlFormat;
});

View file

@ -22,9 +22,10 @@ import { i18n } from '@kbn/i18n';
import { MetricVisComponent } from './components/metric_vis_component';
import { MetricVisOptions } from './components/metric_vis_options';
import { ColorModes } from '../../vis_type_vislib/public';
import { Schemas, AggGroupNames, colorSchemas, ColorSchemas } from './legacy_imports';
import { Schemas, AggGroupNames } from './legacy_imports';
import { ColorSchemas, colorSchemas } from '../../../../plugins/charts/public';
export const metricVisTypeDefinition = {
export const createMetricVisTypeDefinition = () => ({
name: 'metric',
title: i18n.translate('visTypeMetric.metricTitle', { defaultMessage: 'Metric' }),
icon: 'visMetric',
@ -121,4 +122,4 @@ export const metricVisTypeDefinition = {
},
]),
},
};
});

View file

@ -22,12 +22,14 @@ import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressio
import { VisualizationsSetup } from '../../visualizations/public';
import { createMetricVisFn } from './metric_vis_fn';
import { metricVisTypeDefinition } from './metric_vis_type';
import { createMetricVisTypeDefinition } from './metric_vis_type';
import { ChartsPluginSetup } from '../../../../plugins/charts/public';
/** @internal */
export interface MetricVisPluginSetupDependencies {
expressions: ReturnType<ExpressionsPublicPlugin['setup']>;
visualizations: VisualizationsSetup;
charts: ChartsPluginSetup;
}
/** @internal */
@ -38,9 +40,12 @@ export class MetricVisPlugin implements Plugin<void, void> {
this.initializerContext = initializerContext;
}
public setup(core: CoreSetup, { expressions, visualizations }: MetricVisPluginSetupDependencies) {
public setup(
core: CoreSetup,
{ expressions, visualizations, charts }: MetricVisPluginSetupDependencies
) {
expressions.registerFunction(createMetricVisFn);
visualizations.types.createReactVisualization(metricVisTypeDefinition);
visualizations.types.createReactVisualization(createMetricVisTypeDefinition());
}
public start(core: CoreStart) {

View file

@ -17,10 +17,10 @@
* under the License.
*/
import { ColorSchemas } from './legacy_imports';
import { Range } from '../../../../plugins/expressions/public';
import { SchemaConfig } from '../../visualizations/public';
import { ColorModes, Labels, Style } from '../../vis_type_vislib/public';
import { ColorSchemas } from '../../../../plugins/charts/public';
export const visType = 'metric';

View file

@ -26,6 +26,14 @@ import { fromNode, delay } from 'bluebird';
import { ImageComparator } from 'test_utils/image_comparator';
import simpleloadPng from './simpleload.png';
// Replace with mock when converting to jest tests
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { seedColors } from '../../../../../../plugins/charts/public/services/colors/seed_colors';
const colors = {
seedColors,
};
describe('tag cloud tests', function() {
const minValue = 1;
const maxValue = 9;
@ -124,7 +132,7 @@ describe('tag cloud tests', function() {
)}`, function() {
beforeEach(async function() {
setupDOM();
tagCloud = new TagCloud(domNode);
tagCloud = new TagCloud(domNode, colors);
tagCloud.setData(test.data);
tagCloud.setOptions(test.options);
await fromNode(cb => tagCloud.once('renderComplete', cb));
@ -156,7 +164,7 @@ describe('tag cloud tests', function() {
//TagCloud takes at least 600ms to complete (due to d3 animation)
//renderComplete should only notify at the last one
tagCloud = new TagCloud(domNode);
tagCloud = new TagCloud(domNode, colors);
tagCloud.setData(baseTest.data);
tagCloud.setOptions(baseTest.options);
@ -188,7 +196,7 @@ describe('tag cloud tests', function() {
describe('should use the latest state before notifying (when modifying options multiple times)', function() {
beforeEach(async function() {
setupDOM();
tagCloud = new TagCloud(domNode);
tagCloud = new TagCloud(domNode, colors);
tagCloud.setData(baseTest.data);
tagCloud.setOptions(baseTest.options);
tagCloud.setOptions(logScaleTest.options);
@ -215,7 +223,7 @@ describe('tag cloud tests', function() {
describe('should use the latest state before notifying (when modifying data multiple times)', function() {
beforeEach(async function() {
setupDOM();
tagCloud = new TagCloud(domNode);
tagCloud = new TagCloud(domNode, colors);
tagCloud.setData(baseTest.data);
tagCloud.setOptions(baseTest.options);
tagCloud.setData(trimDataTest.data);
@ -245,7 +253,7 @@ describe('tag cloud tests', function() {
counter = 0;
setupDOM();
return new Promise((resolve, reject) => {
tagCloud = new TagCloud(domNode);
tagCloud = new TagCloud(domNode, colors);
tagCloud.setData(baseTest.data);
tagCloud.setOptions(baseTest.options);
@ -291,7 +299,7 @@ describe('tag cloud tests', function() {
describe('should show correct data when state-updates are interleaved with resize event', function() {
beforeEach(async function() {
setupDOM();
tagCloud = new TagCloud(domNode);
tagCloud = new TagCloud(domNode, colors);
tagCloud.setData(logScaleTest.data);
tagCloud.setOptions(logScaleTest.options);
@ -329,7 +337,7 @@ describe('tag cloud tests', function() {
setupDOM();
domNode.style.width = '1px';
domNode.style.height = '1px';
tagCloud = new TagCloud(domNode);
tagCloud = new TagCloud(domNode, colors);
tagCloud.setData(baseTest.data);
tagCloud.setOptions(baseTest.options);
await fromNode(cb => tagCloud.once('renderComplete', cb));
@ -355,7 +363,7 @@ describe('tag cloud tests', function() {
domNode.style.width = '1px';
domNode.style.height = '1px';
tagCloud = new TagCloud(domNode);
tagCloud = new TagCloud(domNode, colors);
tagCloud.setData(baseTest.data);
tagCloud.setOptions(baseTest.options);
await fromNode(cb => tagCloud.once('renderComplete', cb));
@ -380,7 +388,7 @@ describe('tag cloud tests', function() {
describe(`tags should no longer fit after making container smaller`, function() {
beforeEach(async function() {
setupDOM();
tagCloud = new TagCloud(domNode);
tagCloud = new TagCloud(domNode, colors);
tagCloud.setData(baseTest.data);
tagCloud.setOptions(baseTest.options);
await fromNode(cb => tagCloud.once('renderComplete', cb));
@ -412,7 +420,7 @@ describe('tag cloud tests', function() {
});
it('should render simple image', async function() {
tagCloud = new TagCloud(domNode);
tagCloud = new TagCloud(domNode, colors);
tagCloud.setData(baseTest.data);
tagCloud.setOptions(baseTest.options);

View file

@ -22,11 +22,15 @@ import ngMock from 'ng_mock';
import LogstashIndexPatternStubProvider from 'fixtures/stubbed_logstash_index_pattern';
import { Vis } from 'ui/vis';
import { ImageComparator } from 'test_utils/image_comparator';
import { TagCloudVisualization } from '../tag_cloud_visualization';
import { createTagCloudVisualization } from '../tag_cloud_visualization';
import basicdrawPng from './basicdraw.png';
import afterresizePng from './afterresize.png';
import afterparamChange from './afterparamchange.png';
// Replace with mock when converting to jest tests
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { seedColors } from '../../../../../../plugins/charts/public/services/colors/seed_colors';
const THRESHOLD = 0.65;
const PIXEL_DIFF = 64;
@ -55,6 +59,11 @@ describe('TagCloudVisualizationTest', function() {
{ 'col-0': 'BR', 'col-1': 3 },
],
};
const TagCloudVisualization = createTagCloudVisualization({
colors: {
seedColors,
},
});
beforeEach(ngMock.module('kibana'));
beforeEach(

View file

@ -19,7 +19,6 @@
import d3 from 'd3';
import d3TagCloud from 'd3-cloud';
import { seedColors } from 'ui/vis/components/color/seed_colors';
import { EventEmitter } from 'events';
const ORIENTATIONS = {
@ -38,7 +37,7 @@ const D3_SCALING_FUNCTIONS = {
};
export class TagCloud extends EventEmitter {
constructor(domNode) {
constructor(domNode, colors) {
super();
//DOM
@ -55,6 +54,7 @@ export class TagCloud extends EventEmitter {
this._spiral = 'archimedean'; //layout shape
this._timeInterval = 1000; //time allowed for layout algorithm
this._padding = 5;
this._seedColors = colors.seedColors;
//OPTIONS
this._orientation = 'single';
@ -208,7 +208,7 @@ export class TagCloud extends EventEmitter {
enteringTags.style('font-style', this._fontStyle);
enteringTags.style('font-weight', () => this._fontWeight);
enteringTags.style('font-family', () => this._fontFamily);
enteringTags.style('fill', getFill);
enteringTags.style('fill', this.getFill.bind(this));
enteringTags.attr('text-anchor', () => 'middle');
enteringTags.attr('transform', affineTransform);
enteringTags.attr('data-test-subj', getDisplayText);
@ -369,6 +369,11 @@ export class TagCloud extends EventEmitter {
};
return debug;
}
getFill(tag) {
const colorScale = d3.scale.ordinal().range(this._seedColors);
return colorScale(tag.text);
}
}
TagCloud.STATUS = { COMPLETE: 0, INCOMPLETE: 1 };
@ -402,11 +407,6 @@ function getSizeInPixels(tag) {
return `${tag.size}px`;
}
const colorScale = d3.scale.ordinal().range(seedColors);
function getFill(tag) {
return colorScale(tag.text);
}
function hashWithinRange(str, max) {
str = JSON.stringify(str);
let hash = 0;

View file

@ -31,130 +31,132 @@ import { FeedbackMessage } from './feedback_message';
const MAX_TAG_COUNT = 200;
export class TagCloudVisualization {
constructor(node, vis) {
this._containerNode = node;
export function createTagCloudVisualization({ colors }) {
return class TagCloudVisualization {
constructor(node, vis) {
this._containerNode = node;
const cloudRelativeContainer = document.createElement('div');
cloudRelativeContainer.classList.add('tgcVis');
cloudRelativeContainer.setAttribute('style', 'position: relative');
const cloudContainer = document.createElement('div');
cloudContainer.classList.add('tgcVis');
cloudContainer.setAttribute('data-test-subj', 'tagCloudVisualization');
this._containerNode.classList.add('visChart--vertical');
cloudRelativeContainer.appendChild(cloudContainer);
this._containerNode.appendChild(cloudRelativeContainer);
const cloudRelativeContainer = document.createElement('div');
cloudRelativeContainer.classList.add('tgcVis');
cloudRelativeContainer.setAttribute('style', 'position: relative');
const cloudContainer = document.createElement('div');
cloudContainer.classList.add('tgcVis');
cloudContainer.setAttribute('data-test-subj', 'tagCloudVisualization');
this._containerNode.classList.add('visChart--vertical');
cloudRelativeContainer.appendChild(cloudContainer);
this._containerNode.appendChild(cloudRelativeContainer);
this._vis = vis;
this._truncated = false;
this._tagCloud = new TagCloud(cloudContainer);
this._tagCloud.on('select', event => {
if (!this._visParams.bucket) {
this._vis = vis;
this._truncated = false;
this._tagCloud = new TagCloud(cloudContainer, colors);
this._tagCloud.on('select', event => {
if (!this._visParams.bucket) {
return;
}
this._vis.API.events.filter({
table: event.meta.data,
column: 0,
row: event.meta.rowIndex,
});
});
this._renderComplete$ = Rx.fromEvent(this._tagCloud, 'renderComplete');
this._feedbackNode = document.createElement('div');
this._containerNode.appendChild(this._feedbackNode);
this._feedbackMessage = React.createRef();
render(
<I18nContext>
<FeedbackMessage ref={this._feedbackMessage} />
</I18nContext>,
this._feedbackNode
);
this._labelNode = document.createElement('div');
this._containerNode.appendChild(this._labelNode);
this._label = React.createRef();
render(<Label ref={this._label} />, this._labelNode);
}
async render(data, visParams, status) {
if (!(status.resize || status.data || status.params)) return;
if (status.params || status.data) {
this._updateParams(visParams);
this._updateData(data);
}
if (status.resize) {
this._resize();
}
await this._renderComplete$.pipe(take(1)).toPromise();
if (data.columns.length !== 2) {
this._feedbackMessage.current.setState({
shouldShowTruncate: false,
shouldShowIncomplete: false,
});
return;
}
this._vis.API.events.filter({
table: event.meta.data,
column: 0,
row: event.meta.rowIndex,
this._label.current.setState({
label: `${data.columns[0].name} - ${data.columns[1].name}`,
shouldShowLabel: visParams.showLabel,
});
});
this._renderComplete$ = Rx.fromEvent(this._tagCloud, 'renderComplete');
this._feedbackNode = document.createElement('div');
this._containerNode.appendChild(this._feedbackNode);
this._feedbackMessage = React.createRef();
render(
<I18nContext>
<FeedbackMessage ref={this._feedbackMessage} />
</I18nContext>,
this._feedbackNode
);
this._labelNode = document.createElement('div');
this._containerNode.appendChild(this._labelNode);
this._label = React.createRef();
render(<Label ref={this._label} />, this._labelNode);
}
async render(data, visParams, status) {
if (!(status.resize || status.data || status.params)) return;
if (status.params || status.data) {
this._updateParams(visParams);
this._updateData(data);
}
if (status.resize) {
this._resize();
}
await this._renderComplete$.pipe(take(1)).toPromise();
if (data.columns.length !== 2) {
this._feedbackMessage.current.setState({
shouldShowTruncate: false,
shouldShowIncomplete: false,
shouldShowTruncate: this._truncated,
shouldShowIncomplete: this._tagCloud.getStatus() === TagCloud.STATUS.INCOMPLETE,
});
return;
}
this._label.current.setState({
label: `${data.columns[0].name} - ${data.columns[1].name}`,
shouldShowLabel: visParams.showLabel,
});
this._feedbackMessage.current.setState({
shouldShowTruncate: this._truncated,
shouldShowIncomplete: this._tagCloud.getStatus() === TagCloud.STATUS.INCOMPLETE,
});
}
destroy() {
this._tagCloud.destroy();
unmountComponentAtNode(this._feedbackNode);
unmountComponentAtNode(this._labelNode);
}
_updateData(data) {
if (!data || !data.rows.length) {
this._tagCloud.setData([]);
return;
destroy() {
this._tagCloud.destroy();
unmountComponentAtNode(this._feedbackNode);
unmountComponentAtNode(this._labelNode);
}
const bucket = this._visParams.bucket;
const metric = this._visParams.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];
return {
displayText: bucketFormatter ? bucketFormatter.convert(tag, 'text') : tag,
rawText: tag,
value: metric,
meta: {
data: data,
rowIndex: rowIndex,
},
};
});
_updateData(data) {
if (!data || !data.rows.length) {
this._tagCloud.setData([]);
return;
}
if (tags.length > MAX_TAG_COUNT) {
tags.length = MAX_TAG_COUNT;
this._truncated = true;
} else {
this._truncated = false;
const bucket = this._visParams.bucket;
const metric = this._visParams.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];
return {
displayText: bucketFormatter ? bucketFormatter.convert(tag, 'text') : tag,
rawText: tag,
value: metric,
meta: {
data: data,
rowIndex: rowIndex,
},
};
});
if (tags.length > MAX_TAG_COUNT) {
tags.length = MAX_TAG_COUNT;
this._truncated = true;
} else {
this._truncated = false;
}
this._tagCloud.setData(tags);
}
this._tagCloud.setData(tags);
}
_updateParams(visParams) {
this._visParams = visParams;
this._tagCloud.setOptions(visParams);
}
_updateParams(visParams) {
this._visParams = visParams;
this._tagCloud.setOptions(visParams);
}
_resize() {
this._tagCloud.resize();
}
_resize() {
this._tagCloud.resize();
}
};
}

View file

@ -27,6 +27,7 @@ import { plugin } from '.';
const plugins: Readonly<TagCloudPluginSetupDependencies> = {
expressions: npSetup.plugins.expressions,
visualizations: visualizationsSetup,
charts: npSetup.plugins.charts,
};
const pluginInstance = plugin({} as PluginInitializerContext);

View file

@ -20,14 +20,21 @@
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public';
import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public';
import { VisualizationsSetup } from '../../visualizations/public';
import { ChartsPluginSetup } from '../../../../plugins/charts/public';
import { createTagCloudFn } from './tag_cloud_fn';
import { tagcloudVisDefinition } from './tag_cloud_type';
import { createTagCloudVisTypeDefinition } from './tag_cloud_type';
/** @internal */
export interface TagCloudPluginSetupDependencies {
expressions: ReturnType<ExpressionsPublicPlugin['setup']>;
visualizations: VisualizationsSetup;
charts: ChartsPluginSetup;
}
/** @internal */
export interface TagCloudVisDependencies {
colors: ChartsPluginSetup['colors'];
}
/** @internal */
@ -38,9 +45,17 @@ export class TagCloudPlugin implements Plugin<void, void> {
this.initializerContext = initializerContext;
}
public setup(core: CoreSetup, { expressions, visualizations }: TagCloudPluginSetupDependencies) {
public setup(
core: CoreSetup,
{ expressions, visualizations, charts }: TagCloudPluginSetupDependencies
) {
const visualizationDependencies: TagCloudVisDependencies = {
colors: charts.colors,
};
expressions.registerFunction(createTagCloudFn);
visualizations.types.createBaseVisualization(tagcloudVisDefinition);
visualizations.types.createBaseVisualization(
createTagCloudVisTypeDefinition(visualizationDependencies)
);
}
public start(core: CoreStart) {

View file

@ -25,9 +25,10 @@ import { Status } from '../../visualizations/public';
import { TagCloudOptions } from './components/tag_cloud_options';
// @ts-ignore
import { TagCloudVisualization } from './components/tag_cloud_visualization';
import { createTagCloudVisualization } from './components/tag_cloud_visualization';
import { TagCloudVisDependencies } from './plugin';
export const tagcloudVisDefinition = {
export const createTagCloudVisTypeDefinition = (deps: TagCloudVisDependencies) => ({
name: 'tagcloud',
title: i18n.translate('visTypeTagCloud.vis.tagCloudTitle', { defaultMessage: 'Tag Cloud' }),
icon: 'visTagCloud',
@ -44,7 +45,7 @@ export const tagcloudVisDefinition = {
},
},
requiresUpdateStatus: [Status.PARAMS, Status.RESIZE, Status.DATA],
visualization: TagCloudVisualization,
visualization: createTagCloudVisualization(deps),
editorConfig: {
collections: {
scales: [
@ -121,4 +122,4 @@ export const tagcloudVisDefinition = {
]),
},
useCustomNoDataScreen: true,
};
});

View file

@ -22,7 +22,7 @@ import { Legacy } from 'kibana';
import { LegacyPluginApi, LegacyPluginInitializer } from '../../types';
const kbnVislibVisTypesPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) =>
const visTypeVislibPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) =>
new Plugin({
id: 'vis_type_vislib',
require: ['kibana', 'elasticsearch', 'visualizations', 'interpreter', 'data'],
@ -41,4 +41,4 @@ const kbnVislibVisTypesPluginInitializer: LegacyPluginInitializer = ({ Plugin }:
} as Legacy.PluginSpecOptions);
// eslint-disable-next-line import/no-default-export
export default kbnVislibVisTypesPluginInitializer;
export default visTypeVislibPluginInitializer;

View file

@ -38,9 +38,9 @@ import {
} from './utils/collections';
import { getAreaOptionTabs, countLabel } from './utils/common_config';
import { createVislibVisController } from './vis_controller';
import { KbnVislibVisTypesDependencies } from './plugin';
import { VisTypeVislibDependencies } from './plugin';
export const createAreaVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) => ({
export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({
name: 'area',
title: i18n.translate('visTypeVislib.area.areaTitle', { defaultMessage: 'Area' }),
icon: 'visArea',

View file

@ -22,10 +22,11 @@ import { i18n } from '@kbn/i18n';
import { EuiLink, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { VisOptionsProps, ColorSchema } from '../../legacy_imports';
import { VisOptionsProps } from '../../legacy_imports';
import { SelectOption } from './select';
import { SwitchOption } from './switch';
import { ColorSchemaVislibParams } from '../../types';
import { ColorSchema } from '../../../../../../plugins/charts/public';
export type SetColorSchemaOptionsValue = <T extends keyof ColorSchemaVislibParams>(
paramName: T,

View file

@ -22,12 +22,12 @@ import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { ColorSchemas } from '../../../legacy_imports';
import { ColorRanges, ColorSchemaOptions, SwitchOption } from '../../common';
import { GaugeOptionsInternalProps } from '.';
import { ColorSchemaVislibParams } from '../../../types';
import { Gauge } from '../../../gauge';
import { SetColorRangeValue } from '../../common/color_ranges';
import { ColorSchemas } from '../../../../../../../plugins/charts/public';
function RangesPanel({
setGaugeValue,

View file

@ -19,12 +19,13 @@
import { i18n } from '@kbn/i18n';
import { Schemas, AggGroupNames, ColorSchemas, RangeValues } from './legacy_imports';
import { Schemas, AggGroupNames, RangeValues } from './legacy_imports';
import { GaugeOptions } from './components/options';
import { getGaugeCollections, Alignments, ColorModes, GaugeTypes } from './utils/collections';
import { createVislibVisController } from './vis_controller';
import { ColorSchemaVislibParams, Labels, Style } from './types';
import { KbnVislibVisTypesDependencies } from './plugin';
import { VisTypeVislibDependencies } from './plugin';
import { ColorSchemas } from '../../../../plugins/charts/public';
export interface Gauge extends ColorSchemaVislibParams {
backStyle: 'Full';
@ -54,7 +55,7 @@ export interface GaugeVisParams {
gauge: Gauge;
}
export const createGaugeVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) => ({
export const createGaugeVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({
name: 'gauge',
title: i18n.translate('visTypeVislib.gauge.gaugeTitle', { defaultMessage: 'Gauge' }),
icon: 'visGauge',

View file

@ -19,13 +19,14 @@
import { i18n } from '@kbn/i18n';
import { Schemas, AggGroupNames, ColorSchemas } from './legacy_imports';
import { Schemas, AggGroupNames } from './legacy_imports';
import { GaugeOptions } from './components/options';
import { getGaugeCollections, GaugeTypes, ColorModes } from './utils/collections';
import { createVislibVisController } from './vis_controller';
import { KbnVislibVisTypesDependencies } from './plugin';
import { VisTypeVislibDependencies } from './plugin';
import { ColorSchemas } from '../../../../plugins/charts/public';
export const createGoalVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) => ({
export const createGoalVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({
name: 'goal',
title: i18n.translate('visTypeVislib.goal.goalTitle', { defaultMessage: 'Goal' }),
icon: 'visGoal',

View file

@ -19,13 +19,14 @@
import { i18n } from '@kbn/i18n';
import { Schemas, AggGroupNames, ColorSchemas, RangeValues } from './legacy_imports';
import { Schemas, AggGroupNames, RangeValues } from './legacy_imports';
import { AxisTypes, getHeatmapCollections, Positions, ScaleTypes } from './utils/collections';
import { HeatmapOptions } from './components/options';
import { createVislibVisController } from './vis_controller';
import { TimeMarker } from './vislib/visualizations/time_marker';
import { CommonVislibParams, ColorSchemaVislibParams, ValueAxis } from './types';
import { KbnVislibVisTypesDependencies } from './plugin';
import { VisTypeVislibDependencies } from './plugin';
import { ColorSchemas } from '../../../../plugins/charts/public';
export interface HeatmapVisParams extends CommonVislibParams, ColorSchemaVislibParams {
type: 'heatmap';
@ -39,7 +40,7 @@ export interface HeatmapVisParams extends CommonVislibParams, ColorSchemaVislibP
times: TimeMarker[];
}
export const createHeatmapVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) => ({
export const createHeatmapVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({
name: 'heatmap',
title: i18n.translate('visTypeVislib.heatmap.heatmapTitle', { defaultMessage: 'Heat Map' }),
icon: 'visHeatmap',

View file

@ -38,9 +38,9 @@ import {
} from './utils/collections';
import { getAreaOptionTabs, countLabel } from './utils/common_config';
import { createVislibVisController } from './vis_controller';
import { KbnVislibVisTypesDependencies } from './plugin';
import { VisTypeVislibDependencies } from './plugin';
export const createHistogramVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) => ({
export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({
name: 'histogram',
title: i18n.translate('visTypeVislib.histogram.histogramTitle', {
defaultMessage: 'Vertical Bar',

View file

@ -38,9 +38,9 @@ import {
} from './utils/collections';
import { getAreaOptionTabs, countLabel } from './utils/common_config';
import { createVislibVisController } from './vis_controller';
import { KbnVislibVisTypesDependencies } from './plugin';
import { VisTypeVislibDependencies } from './plugin';
export const createHorizontalBarVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) => ({
export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({
name: 'horizontal_bar',
title: i18n.translate('visTypeVislib.horizontalBar.horizontalBarTitle', {
defaultMessage: 'Horizontal Bar',

View file

@ -18,7 +18,7 @@
*/
import { PluginInitializerContext } from '../../../../core/public';
import { KbnVislibVisTypesPlugin as Plugin } from './plugin';
import { VisTypeVislibPlugin as Plugin } from './plugin';
export function plugin(initializerContext: PluginInitializerContext) {
return new Plugin(initializerContext);

View file

@ -20,28 +20,23 @@
import { npSetup, npStart } from 'ui/new_platform';
import { PluginInitializerContext } from 'kibana/public';
// @ts-ignore
import { vislibColor } from 'ui/vis/components/color/color';
import { plugin } from '.';
import {
KbnVislibVisTypesPluginSetupDependencies,
KbnVislibVisTypesPluginStartDependencies,
VisTypeVislibPluginSetupDependencies,
VisTypeVislibPluginStartDependencies,
} from './plugin';
import {
setup as visualizationsSetup,
start as visualizationsStart,
} from '../../visualizations/public/np_ready/public/legacy';
const setupPlugins: Readonly<KbnVislibVisTypesPluginSetupDependencies> = {
const setupPlugins: Readonly<VisTypeVislibPluginSetupDependencies> = {
expressions: npSetup.plugins.expressions,
visualizations: visualizationsSetup,
__LEGACY: {
vislibColor,
},
charts: npSetup.plugins.charts,
};
const startPlugins: Readonly<KbnVislibVisTypesPluginStartDependencies> = {
const startPlugins: Readonly<VisTypeVislibPluginStartDependencies> = {
expressions: npStart.plugins.expressions,
visualizations: visualizationsStart,
};

View file

@ -20,7 +20,6 @@
export { AggGroupNames, VisOptionsProps } from 'ui/vis/editors/default';
export { Schemas } from 'ui/vis/editors/default/schemas';
export { RangeValues, RangesParamEditor } from 'ui/vis/editors/default/controls/ranges';
export { ColorSchema, ColorSchemas, colorSchemas, getHeatmapColors } from 'ui/color_maps';
export { AggConfig, Vis, VisParams } from 'ui/vis';
export { AggType } from 'ui/agg_types';
// @ts-ignore

View file

@ -38,9 +38,9 @@ import {
} from './utils/collections';
import { getAreaOptionTabs, countLabel } from './utils/common_config';
import { createVislibVisController } from './vis_controller';
import { KbnVislibVisTypesDependencies } from './plugin';
import { VisTypeVislibDependencies } from './plugin';
export const createLineVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) => ({
export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({
name: 'line',
title: i18n.translate('visTypeVislib.line.lineTitle', { defaultMessage: 'Line' }),
icon: 'visLine',

View file

@ -24,7 +24,7 @@ import { PieOptions } from './components/options';
import { getPositions, Positions } from './utils/collections';
import { createVislibVisController } from './vis_controller';
import { CommonVislibParams } from './types';
import { KbnVislibVisTypesDependencies } from './plugin';
import { VisTypeVislibDependencies } from './plugin';
export interface PieVisParams extends CommonVislibParams {
type: 'pie';
@ -38,7 +38,7 @@ export interface PieVisParams extends CommonVislibParams {
};
}
export const createPieVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) => ({
export const createPieVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({
name: 'pie',
title: i18n.translate('visTypeVislib.pie.pieTitle', { defaultMessage: 'Pie' }),
icon: 'visPie',

View file

@ -42,7 +42,7 @@ jest.mock('./vislib/response_handler', () => ({
}));
describe('interpreter/functions#pie', () => {
const fn = functionWrapper(createPieVisFn());
const fn = functionWrapper(createPieVisFn);
const context = {
type: 'kibana_datatable',
rows: [{ 'col-0-1': 0 }],

View file

@ -43,12 +43,7 @@ interface RenderValue {
type Return = Render<RenderValue>;
export const createPieVisFn = () => (): ExpressionFunction<
typeof name,
Context,
Arguments,
Return
> => ({
export const createPieVisFn = (): ExpressionFunction<typeof name, Context, Arguments, Return> => ({
name: 'kibana_pie',
type: 'render',
context: {

View file

@ -26,7 +26,7 @@ import {
import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public';
import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/public';
import { createKbnVislibVisTypesFn } from './vis_type_vislib_vis_fn';
import { createVisTypeVislibVisFn } from './vis_type_vislib_vis_fn';
import { createPieVisFn } from './pie_fn';
import {
createHistogramVisTypeDefinition,
@ -38,45 +38,43 @@ import {
createGaugeVisTypeDefinition,
createGoalVisTypeDefinition,
} from './vis_type_vislib_vis_types';
import { ChartsPluginSetup } from '../../../../plugins/charts/public';
type KbnVislibVisTypesCoreSetup = CoreSetup<KbnVislibVisTypesPluginStartDependencies>;
export interface LegacyDependencies {
vislibColor: (colors: Array<string | number>, mappings: any) => (value: any) => any;
export interface VisTypeVislibDependencies {
uiSettings: IUiSettingsClient;
charts: ChartsPluginSetup;
}
export type KbnVislibVisTypesDependencies = LegacyDependencies & {
uiSettings: IUiSettingsClient;
};
/** @internal */
export interface KbnVislibVisTypesPluginSetupDependencies {
export interface VisTypeVislibPluginSetupDependencies {
expressions: ReturnType<ExpressionsPublicPlugin['setup']>;
visualizations: VisualizationsSetup;
__LEGACY: LegacyDependencies;
charts: ChartsPluginSetup;
}
/** @internal */
export interface KbnVislibVisTypesPluginStartDependencies {
export interface VisTypeVislibPluginStartDependencies {
expressions: ReturnType<ExpressionsPublicPlugin['start']>;
visualizations: VisualizationsStart;
}
type VisTypeVislibCoreSetup = CoreSetup<VisTypeVislibPluginStartDependencies>;
/** @internal */
export class KbnVislibVisTypesPlugin implements Plugin<Promise<void>, void> {
export class VisTypeVislibPlugin implements Plugin<Promise<void>, void> {
constructor(public initializerContext: PluginInitializerContext) {}
public async setup(
core: KbnVislibVisTypesCoreSetup,
{ expressions, visualizations, __LEGACY }: KbnVislibVisTypesPluginSetupDependencies
core: VisTypeVislibCoreSetup,
{ expressions, visualizations, charts }: VisTypeVislibPluginSetupDependencies
) {
const visualizationDependencies: Readonly<KbnVislibVisTypesDependencies> = {
...__LEGACY,
const visualizationDependencies: Readonly<VisTypeVislibDependencies> = {
uiSettings: core.uiSettings,
charts,
};
expressions.registerFunction(createKbnVislibVisTypesFn());
expressions.registerFunction(createPieVisFn());
expressions.registerFunction(createVisTypeVislibVisFn);
expressions.registerFunction(createPieVisFn);
[
createHistogramVisTypeDefinition,
@ -90,7 +88,7 @@ export class KbnVislibVisTypesPlugin implements Plugin<Promise<void>, void> {
].forEach(vis => visualizations.types.createBaseVisualization(vis(visualizationDependencies)));
}
public start(core: CoreStart, deps: KbnVislibVisTypesPluginStartDependencies) {
public start(core: CoreStart, deps: VisTypeVislibPluginStartDependencies) {
// nothing to do here
}
}

View file

@ -17,7 +17,6 @@
* under the License.
*/
import { ColorSchemas } from './legacy_imports';
import { TimeMarker } from './vislib/visualizations/time_marker';
import {
Positions,
@ -30,6 +29,7 @@ import {
ScaleTypes,
ThresholdLineStyles,
} from './utils/collections';
import { ColorSchemas } from '../../../../plugins/charts/public';
export interface CommonVislibParams {
addTooltip: boolean;

View file

@ -19,7 +19,8 @@
import { i18n } from '@kbn/i18n';
import { $Values } from '@kbn/utility-types';
import { colorSchemas } from '../legacy_imports';
import { colorSchemas } from '../../../../../plugins/charts/public';
export const Positions = Object.freeze({
RIGHT: 'right' as 'right',

View file

@ -24,7 +24,7 @@ import { Vis, VisParams } from './legacy_imports';
// @ts-ignore
import { Vis as Vislib } from './vislib/vis';
import { Positions } from './utils/collections';
import { KbnVislibVisTypesDependencies } from './plugin';
import { VisTypeVislibDependencies } from './plugin';
import { mountReactNode } from '../../../../core/public/utils';
import { VisLegend, CUSTOM_LEGEND_VIS_TYPES } from './vislib/components/legend';
@ -35,7 +35,7 @@ const legendClassName = {
right: 'visLib--legend-right',
};
export const createVislibVisController = (deps: KbnVislibVisTypesDependencies) => {
export const createVislibVisController = (deps: VisTypeVislibDependencies) => {
return class VislibVisController {
unmount: (() => void) | null = null;
visParams?: VisParams;

View file

@ -45,7 +45,7 @@ interface RenderValue {
type Return = Render<RenderValue>;
export const createKbnVislibVisTypesFn = () => (): ExpressionFunction<
export const createVisTypeVislibVisFn = (): ExpressionFunction<
typeof name,
Context,
Arguments,

View file

@ -20,10 +20,14 @@
import _ from 'lodash';
import $ from 'jquery';
import { vislibColor } from 'ui/vis/components/color/color';
import { Vis } from '../../../vis';
// TODO: Remove when converted to jest mocks
import {
ColorsService,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../../../../plugins/charts/public/services';
const $visCanvas = $('<div>')
.attr('id', 'vislib-vis-fixtures')
.css({
@ -57,9 +61,14 @@ afterEach(function() {
const getDeps = () => {
const uiSettings = new Map();
const colors = new ColorsService();
colors.init(uiSettings);
return {
uiSettings,
vislibColor,
charts: {
colors,
},
};
};

View file

@ -35,14 +35,16 @@ import { getFormat } from '../../legacy_imports';
* @param attr {Object|*} Visualization options
*/
export class Data {
constructor(data, uiState, vislibColor) {
constructor(data, uiState, createColorLookupFunction) {
this.uiState = uiState;
this.vislibColor = vislibColor;
this.createColorLookupFunction = createColorLookupFunction;
this.data = this.copyDataObj(data);
this.type = this.getDataType();
this._cleanVisData();
this.labels = this._getLabels(this.data);
this.color = this.labels ? vislibColor(this.labels, uiState.get('vis.colors')) : undefined;
this.color = this.labels
? createColorLookupFunction(this.labels, uiState.get('vis.colors'))
: undefined;
this._normalizeOrdered();
}
@ -473,7 +475,7 @@ export class Data {
const defaultColors = this.uiState.get('vis.defaultColors');
const overwriteColors = this.uiState.get('vis.colors');
const colors = defaultColors ? _.defaults({}, overwriteColors, defaultColors) : overwriteColors;
return this.vislibColor(this.getLabels(), colors);
return this.createColorLookupFunction(this.getLabels(), colors);
}
/**
@ -483,7 +485,7 @@ export class Data {
* @returns {Function} Performs lookup on string and returns hex color
*/
getPieColorFunc() {
return this.vislibColor(
return this.createColorLookupFunction(
this.pieNames(this.getVisData()).map(function(d) {
return d.label;
}),

View file

@ -35,8 +35,8 @@ const DEFAULT_VIS_CONFIG = {
};
export class VisConfig {
constructor(visConfigArgs, data, uiState, el, vislibColor) {
this.data = new Data(data, uiState, vislibColor);
constructor(visConfigArgs, data, uiState, el, createColorLookupFunction) {
this.data = new Data(data, uiState, createColorLookupFunction);
const visType = visTypes[visConfigArgs.type];
const typeDefaults = visType(visConfigArgs, this.data);

View file

@ -55,7 +55,7 @@ export class Vis extends EventEmitter {
this.data,
this.uiState,
this.element,
this.deps.vislibColor
this.deps.charts.colors.createColorLookupFunction.bind(this.deps.charts.colors)
);
}

View file

@ -20,7 +20,7 @@
import d3 from 'd3';
import _ from 'lodash';
import { getHeatmapColors } from '../../../legacy_imports';
import { getHeatmapColors } from '../../../../../../../plugins/charts/public';
const arcAngles = {
angleFactor: 0.75,

View file

@ -249,7 +249,7 @@ export class PointSeries extends Chart {
if (!seriArgs.show) return;
const SeriClass =
seriTypes[seriArgs.type || self.handler.visConfig.get('chart.type')] || seriTypes.line;
const series = new SeriClass(self.handler, svg, data.series[i], seriArgs, this.deps);
const series = new SeriClass(self.handler, svg, data.series[i], seriArgs, self.deps);
series.events = self.events;
svg.call(series.draw());
self.series.push(series);

View file

@ -23,7 +23,7 @@ import moment from 'moment';
import { isColorDark } from '@elastic/eui';
import { PointSeries } from './_point_series';
import { getHeatmapColors } from '../../../legacy_imports';
import { getHeatmapColors } from '../../.../../../../../../../plugins/charts/public';
const defaults = {
color: undefined, // todo
@ -42,6 +42,7 @@ const defaults = {
export class HeatmapChart extends PointSeries {
constructor(handler, chartEl, chartData, seriesConfigArgs, deps) {
super(handler, chartEl, chartData, seriesConfigArgs, deps);
this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults);
this.handler.visConfig.set('legend', {

View file

@ -27,11 +27,13 @@ import { inspectorPluginMock } from '../../../../../plugins/inspector/public/moc
import { uiActionsPluginMock } from '../../../../../plugins/ui_actions/public/mocks';
import { managementPluginMock } from '../../../../../plugins/management/public/mocks';
import { usageCollectionPluginMock } from '../../../../../plugins/usage_collection/public/mocks';
import { chartPluginMock } from '../../../../../plugins/charts/public/mocks';
/* eslint-enable @kbn/eslint/no-restricted-paths */
export const pluginsMock = {
createSetup: () => ({
data: dataPluginMock.createSetupContract(),
charts: chartPluginMock.createSetupContract(),
navigation: navigationPluginMock.createSetupContract(),
embeddable: embeddablePluginMock.createSetupContract(),
inspector: inspectorPluginMock.createSetupContract(),
@ -41,6 +43,7 @@ export const pluginsMock = {
}),
createStart: () => ({
data: dataPluginMock.createStartContract(),
charts: chartPluginMock.createStartContract(),
navigation: navigationPluginMock.createStartContract(),
embeddable: embeddablePluginMock.createStartContract(),
inspector: inspectorPluginMock.createStartContract(),

View file

@ -141,6 +141,12 @@ export const npSetup = {
update: sinon.fake(),
},
},
charts: {
theme: {
chartsTheme$: mockObservable,
useChartsTheme: sinon.fake(),
},
},
},
};
@ -276,12 +282,21 @@ export const npStart = {
featureCatalogue: {
register: sinon.fake(),
},
environment: {
get: sinon.fake(),
},
},
navigation: {
ui: {
TopNavMenu: mockComponent,
},
},
charts: {
theme: {
chartsTheme$: mockObservable,
useChartsTheme: sinon.fake(),
},
},
},
};

View file

@ -27,7 +27,7 @@ import {
Setup as InspectorSetup,
Start as InspectorStart,
} from '../../../../plugins/inspector/public';
import { EuiUtilsStart } from '../../../../plugins/eui_utils/public';
import { ChartsPluginSetup, ChartsPluginStart } from '../../../../plugins/charts/public';
import { DevToolsSetup, DevToolsStart } from '../../../../plugins/dev_tools/public';
import { KibanaLegacySetup, KibanaLegacyStart } from '../../../../plugins/kibana_legacy/public';
import { HomePublicPluginSetup, HomePublicPluginStart } from '../../../../plugins/home/public';
@ -42,6 +42,7 @@ import {
export interface PluginsSetup {
bfetch: BfetchPublicSetup;
charts: ChartsPluginSetup;
data: ReturnType<DataPlugin['setup']>;
embeddable: IEmbeddableSetup;
expressions: ReturnType<ExpressionsPlugin['setup']>;
@ -57,9 +58,9 @@ export interface PluginsSetup {
export interface PluginsStart {
bfetch: BfetchPublicStart;
charts: ChartsPluginStart;
data: ReturnType<DataPlugin['start']>;
embeddable: IEmbeddableStart;
eui_utils: EuiUtilsStart;
expressions: ReturnType<ExpressionsPlugin['start']>;
home: HomePublicPluginStart;
inspector: InspectorStart;

View file

@ -1,343 +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 '@kbn/expect';
import ngMock from 'ng_mock';
import _ from 'lodash';
import d3 from 'd3';
import chrome from '../../../chrome';
import { seedColors } from '../../../vis/components/color/seed_colors';
import { vislibColor as getColors } from '../../components/color/color';
import { mappedColors } from '../../components/color/mapped_colors';
import { createColorPalette } from '../../components/color/color_palette';
const config = chrome.getUiSettingsClient();
describe('Vislib Color Module Test Suite', function() {
describe('Color (main)', function() {
let previousConfig;
const arr = ['good', 'better', 'best', 'never', 'let', 'it', 'rest'];
const arrayOfNumbers = [1, 2, 3, 4, 5];
const arrayOfUndefinedValues = [undefined, undefined, undefined];
const arrayOfObjects = [{}, {}, {}];
const arrayOfBooleans = [true, false, true];
const arrayOfNullValues = [null, null, null];
const emptyObject = {};
const nullValue = null;
let notAValue;
let color;
beforeEach(ngMock.module('kibana'));
beforeEach(() => {
previousConfig = config.get('visualization:colorMapping');
config.set('visualization:colorMapping', {});
color = getColors(arr, {});
});
afterEach(() => {
config.set('visualization:colorMapping', previousConfig);
});
it('should throw an error if input is not an array', function() {
expect(function() {
getColors(200);
}).to.throwError();
expect(function() {
getColors('help');
}).to.throwError();
expect(function() {
getColors(true);
}).to.throwError();
expect(function() {
getColors(notAValue);
}).to.throwError();
expect(function() {
getColors(nullValue);
}).to.throwError();
expect(function() {
getColors(emptyObject);
}).to.throwError();
});
describe('when array is not composed of numbers, strings, or undefined values', function() {
it('should throw an error', function() {
expect(function() {
getColors(arrayOfObjects);
}).to.throwError();
expect(function() {
getColors(arrayOfBooleans);
}).to.throwError();
expect(function() {
getColors(arrayOfNullValues);
}).to.throwError();
});
});
describe('when input is an array of strings, numbers, or undefined values', function() {
it('should not throw an error', function() {
expect(function() {
getColors(arr);
}).to.not.throwError();
expect(function() {
getColors(arrayOfNumbers);
}).to.not.throwError();
expect(function() {
getColors(arrayOfUndefinedValues);
}).to.not.throwError();
});
});
it('should be a function', function() {
expect(typeof getColors).to.be('function');
});
it('should return a function', function() {
expect(typeof color).to.be('function');
});
it('should return the first hex color in the seed colors array', function() {
expect(color(arr[0])).to.be(seedColors[0]);
});
it('should return the value from the mapped colors', function() {
expect(color(arr[1])).to.be(mappedColors.get(arr[1]));
});
it('should return the value from the specified color mapping overrides', function() {
const colorFn = getColors(arr, { good: 'red' });
expect(colorFn('good')).to.be('red');
});
});
describe('Seed Colors', function() {
it('should return an array', function() {
expect(seedColors instanceof Array).to.be(true);
});
});
describe('Mapped Colors', () => {
let previousConfig;
beforeEach(ngMock.module('kibana'));
beforeEach(() => {
previousConfig = config.get('visualization:colorMapping');
mappedColors.mapping = {};
});
afterEach(() => {
config.set('visualization:colorMapping', previousConfig);
});
it('should properly map keys to unique colors', () => {
config.set('visualization:colorMapping', {});
const arr = [1, 2, 3, 4, 5];
mappedColors.mapKeys(arr);
expect(
_(mappedColors.mapping)
.values()
.uniq()
.size()
).to.be(arr.length);
});
it('should not include colors used by the config', () => {
const newConfig = { bar: seedColors[0] };
config.set('visualization:colorMapping', newConfig);
const arr = ['foo', 'baz', 'qux'];
mappedColors.mapKeys(arr);
const colorValues = _(mappedColors.mapping).values();
expect(colorValues.contains(seedColors[0])).to.be(false);
expect(colorValues.uniq().size()).to.be(arr.length);
});
it('should create a unique array of colors even when config is set', () => {
const newConfig = { bar: seedColors[0] };
config.set('visualization:colorMapping', newConfig);
const arr = ['foo', 'bar', 'baz', 'qux'];
mappedColors.mapKeys(arr);
const expectedSize = _(arr)
.difference(_.keys(newConfig))
.size();
expect(
_(mappedColors.mapping)
.values()
.uniq()
.size()
).to.be(expectedSize);
expect(mappedColors.get(arr[0])).to.not.be(seedColors[0]);
});
it('should treat different formats of colors as equal', () => {
const color = d3.rgb(seedColors[0]);
const rgb = `rgb(${color.r}, ${color.g}, ${color.b})`;
const newConfig = { bar: rgb };
config.set('visualization:colorMapping', newConfig);
const arr = ['foo', 'bar', 'baz', 'qux'];
mappedColors.mapKeys(arr);
const expectedSize = _(arr)
.difference(_.keys(newConfig))
.size();
expect(
_(mappedColors.mapping)
.values()
.uniq()
.size()
).to.be(expectedSize);
expect(mappedColors.get(arr[0])).to.not.be(seedColors[0]);
expect(mappedColors.get('bar')).to.be(seedColors[0]);
});
it('should have a flush method that moves the current map to the old map', function() {
const arr = [1, 2, 3, 4, 5];
mappedColors.mapKeys(arr);
expect(_.keys(mappedColors.mapping).length).to.be(5);
expect(_.keys(mappedColors.oldMap).length).to.be(0);
mappedColors.flush();
expect(_.keys(mappedColors.oldMap).length).to.be(5);
expect(_.keys(mappedColors.mapping).length).to.be(0);
mappedColors.flush();
expect(_.keys(mappedColors.oldMap).length).to.be(0);
expect(_.keys(mappedColors.mapping).length).to.be(0);
});
it('should use colors in the oldMap if they are available', function() {
const arr = [1, 2, 3, 4, 5];
mappedColors.mapKeys(arr);
expect(_.keys(mappedColors.mapping).length).to.be(5);
expect(_.keys(mappedColors.oldMap).length).to.be(0);
mappedColors.flush();
mappedColors.mapKeys([3, 4, 5]);
expect(_.keys(mappedColors.oldMap).length).to.be(5);
expect(_.keys(mappedColors.mapping).length).to.be(3);
expect(mappedColors.mapping[1]).to.be(undefined);
expect(mappedColors.mapping[2]).to.be(undefined);
expect(mappedColors.mapping[3]).to.equal(mappedColors.oldMap[3]);
expect(mappedColors.mapping[4]).to.equal(mappedColors.oldMap[4]);
expect(mappedColors.mapping[5]).to.equal(mappedColors.oldMap[5]);
});
it('should have a purge method that clears both maps', function() {
const arr = [1, 2, 3, 4, 5];
mappedColors.mapKeys(arr);
mappedColors.flush();
mappedColors.mapKeys(arr);
expect(_.keys(mappedColors.mapping).length).to.be(5);
expect(_.keys(mappedColors.oldMap).length).to.be(5);
mappedColors.purge();
expect(_.keys(mappedColors.mapping).length).to.be(0);
expect(_.keys(mappedColors.oldMap).length).to.be(0);
});
});
describe('Color Palette', function() {
const num1 = 45;
const num2 = 72;
const num3 = 90;
const string = 'Welcome';
const bool = true;
const nullValue = null;
const emptyArr = [];
const emptyObject = {};
let notAValue;
let colorPalette;
beforeEach(ngMock.module('kibana'));
beforeEach(
ngMock.inject(function() {
colorPalette = createColorPalette(num1);
})
);
it('should throw an error if input is not a number', function() {
expect(function() {
createColorPalette(string);
}).to.throwError();
expect(function() {
createColorPalette(bool);
}).to.throwError();
expect(function() {
createColorPalette(nullValue);
}).to.throwError();
expect(function() {
createColorPalette(emptyArr);
}).to.throwError();
expect(function() {
createColorPalette(emptyObject);
}).to.throwError();
expect(function() {
createColorPalette(notAValue);
}).to.throwError();
});
it('should be a function', function() {
expect(typeof createColorPalette).to.be('function');
});
it('should return an array', function() {
expect(colorPalette instanceof Array).to.be(true);
});
it('should return an array of the same length as the input', function() {
expect(colorPalette.length).to.be(num1);
});
it('should return the seed color array when input length is 72', function() {
expect(createColorPalette(num2)[71]).to.be(seedColors[71]);
});
it('should return an array of the same length as the input when input is greater than 72', function() {
expect(createColorPalette(num3).length).to.be(num3);
});
it('should create new darker colors when input is greater than 72', function() {
expect(createColorPalette(num3)[72]).not.to.equal(seedColors[0]);
});
});
});

View file

@ -1,47 +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 _ from 'lodash';
import { mappedColors } from './mapped_colors';
/*
* Accepts an array of strings or numbers that are used to create a
* a lookup table that associates the values (key) with a hex color (value).
* Returns a function that accepts a value (i.e. a string or number)
* and returns a hex color associated with that value.
*/
export function vislibColor(arrayOfStringsOrNumbers, colorMapping) {
colorMapping = colorMapping || {};
if (!Array.isArray(arrayOfStringsOrNumbers)) {
throw new Error('ColorUtil expects an array');
}
arrayOfStringsOrNumbers.forEach(function(val) {
if (!_.isString(val) && !_.isNumber(val) && !_.isUndefined(val)) {
throw new TypeError('ColorUtil expects an array of strings, numbers, or undefined values');
}
});
mappedColors.mapKeys(arrayOfStringsOrNumbers);
return function(value) {
return colorMapping[value] || mappedColors.get(value);
};
}

View file

@ -0,0 +1,97 @@
# Charts
The Charts plugin is a way to create easier integration of shared colors, themes, types and other utilities across all Kibana charts and visualizations.
## Static methods
### `vislibColorMaps`
Color mappings related to vislib visualizations
### `truncatedColorMaps`
Color mappings subset of `vislibColorMaps`
### `colorSchemas`
Color mappings in `value`/`text` form
### `getHeatmapColors`
Funciton to retrive heatmap related colors based on `value` and `colorSchemaName`
### `truncatedColorSchemas`
Truncated color mappings in `value`/`text` form
## Theme
the `theme` service offers utilities to interact with theme of kibana. EUI provides a light and dark theme object to work with Elastic-Charts. However, every instance of a Chart would need to pass down this the correctly EUI theme depending on Kibana's light or dark mode. There are several ways you can use the `theme` service to get the correct theme.
> The current theme (light or dark) of Kibana is typically taken into account for the functions below.
### `useChartsTheme`
The simple fetching of the correct EUI theme; a **React hook**.
```js
import { npStart } from 'ui/new_platform';
import { Chart, Settings } from '@elastic/charts';
export const YourComponent = () => (
<Chart>
<Settings theme={npStart.plugins.charts.theme.useChartsTheme()} />
</Chart>
);
```
### `chartsTheme$`
An **observable** of the current charts theme. Use this implementation for more flexible updates to the chart theme without full page refreshes.
```tsx
import { npStart } from 'ui/new_platform';
import { EuiChartThemeType } from '@elastic/eui/src/themes/charts/themes';
import { Subscription } from 'rxjs';
import { Chart, Settings } from '@elastic/charts';
interface YourComponentProps {};
interface YourComponentState {
chartsTheme: EuiChartThemeType['theme'];
}
export class YourComponent extends Component<YourComponentProps, YourComponentState> {
private subscription?: Subscription;
public state = {
chartsTheme: npStart.plugins.charts.theme.chartsDefaultTheme,
};
componentDidMount() {
this.subscription = npStart.plugins.charts.theme
.chartsTheme$
.subscribe(chartsTheme => this.setState({ chartsTheme }));
}
componentWillUnmount() {
if (this.subscription) {
this.subscription.unsubscribe();
this.subscription = undefined;
}
}
public render() {
const { chartsTheme } = this.state;
return (
<Chart>
<Settings theme={chartsTheme} />
</Chart>
);
}
}
```
### `chartsDefaultTheme`
Returns default charts theme (i.e. light).

View file

@ -1,5 +1,5 @@
{
"id": "eui_utils",
"id": "charts",
"version": "kibana",
"server": false,
"ui": true

View file

@ -0,0 +1,27 @@
/*
* 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 { ChartsPlugin } from './plugin';
export const plugin = () => new ChartsPlugin();
export type ChartsPluginSetup = ReturnType<ChartsPlugin['setup']>;
export type ChartsPluginStart = ReturnType<ChartsPlugin['start']>;
export * from './static';

View file

@ -0,0 +1,42 @@
/*
* 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 { ChartsPlugin } from './plugin';
import { themeServiceMock } from './services/theme/mock';
import { colorsServiceMock } from './services/colors/mock';
export type Setup = jest.Mocked<ReturnType<ChartsPlugin['setup']>>;
export type Start = jest.Mocked<ReturnType<ChartsPlugin['start']>>;
const createSetupContract = (): Setup => ({
colors: colorsServiceMock,
theme: themeServiceMock,
});
const createStartContract = (): Start => ({
colors: colorsServiceMock,
theme: themeServiceMock,
});
export { colorMapsMock } from './static/color_maps/mock';
export const chartPluginMock = {
createSetupContract,
createStartContract,
};

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 { Plugin, CoreSetup } from 'kibana/public';
import { ThemeService, ColorsService } from './services';
export type Theme = Omit<ThemeService, 'init'>;
export type Color = Omit<ColorsService, 'init'>;
/** @public */
export interface ChartsPluginSetup {
colors: Color;
theme: Theme;
}
/** @public */
export type ChartsPluginStart = ChartsPluginSetup;
/** @public */
export class ChartsPlugin implements Plugin<ChartsPluginSetup, ChartsPluginStart> {
private readonly themeService = new ThemeService();
private readonly colorsService = new ColorsService();
public setup({ uiSettings }: CoreSetup): ChartsPluginSetup {
this.themeService.init(uiSettings);
this.colorsService.init(uiSettings);
return {
colors: this.colorsService,
theme: this.themeService,
};
}
public start(): ChartsPluginStart {
return {
colors: this.colorsService,
theme: this.themeService,
};
}
}

View file

@ -19,18 +19,13 @@
import d3 from 'd3';
import _ from 'lodash';
import { seedColors } from './seed_colors';
/*
* Generates an array of hex colors the length of the input number.
* If the number is greater than the length of seed colors available,
* new colors are generated up to the value of the input number.
*/
import { seedColors } from './seed_colors';
const offset = 300; // Hue offset to start at
const fraction = function(goal) {
const walkTree = function(numerator, denominator, bytes) {
const fraction = function(goal: number) {
const walkTree = (numerator: number, denominator: number, bytes: number[]): number => {
if (bytes.length) {
return walkTree(numerator * 2 + (bytes.pop() ? 1 : -1), denominator * 2, bytes);
} else {
@ -49,13 +44,17 @@ const fraction = function(goal) {
return walkTree(1, 2, b);
};
export function createColorPalette(num) {
/**
* Generates an array of hex colors the length of the input number.
* If the number is greater than the length of seed colors available,
* new colors are generated up to the value of the input number.
*/
export function createColorPalette(num?: any): string[] {
if (!_.isNumber(num)) {
throw new TypeError('ColorPaletteUtilService expects a number');
}
const colors = seedColors;
const seedLength = seedColors.length;
_.times(num - seedLength, function(i) {

View file

@ -0,0 +1,140 @@
/*
* 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 { coreMock } from '../../../../../core/public/mocks';
import { seedColors } from './seed_colors';
import { ColorsService } from './colors';
// Local state for config
const config = new Map<string, any>();
describe('Vislib Color Service', () => {
const colors = new ColorsService();
const mockUiSettings = coreMock.createSetup().uiSettings;
mockUiSettings.get.mockImplementation(a => config.get(a));
mockUiSettings.set.mockImplementation((...a) => config.set(...a) as any);
colors.init(mockUiSettings);
let color: any;
let previousConfig: any;
const arr = ['good', 'better', 'best', 'never', 'let', 'it', 'rest'];
const arrayOfNumbers = [1, 2, 3, 4, 5];
const arrayOfUndefinedValues = [undefined, undefined, undefined];
const arrayOfObjects = [{}, {}, {}];
const arrayOfBooleans = [true, false, true];
const arrayOfNullValues = [null, null, null];
const emptyObject = {};
const nullValue = null;
beforeEach(() => {
previousConfig = config.get('visualization:colorMapping');
config.set('visualization:colorMapping', {});
color = colors.createColorLookupFunction(arr, {});
});
afterEach(() => {
config.set('visualization:colorMapping', previousConfig);
});
it('should throw error if not initialized', () => {
const colorsBad = new ColorsService();
expect(() => colorsBad.createColorLookupFunction(arr, {})).toThrowError();
});
it('should throw an error if input is not an array', () => {
expect(() => {
colors.createColorLookupFunction(200);
}).toThrowError();
expect(() => {
colors.createColorLookupFunction('help');
}).toThrowError();
expect(() => {
colors.createColorLookupFunction(true);
}).toThrowError();
expect(() => {
colors.createColorLookupFunction();
}).toThrowError();
expect(() => {
colors.createColorLookupFunction(nullValue);
}).toThrowError();
expect(() => {
colors.createColorLookupFunction(emptyObject);
}).toThrowError();
});
describe('when array is not composed of numbers, strings, or undefined values', () => {
it('should throw an error', () => {
expect(() => {
colors.createColorLookupFunction(arrayOfObjects);
}).toThrowError();
expect(() => {
colors.createColorLookupFunction(arrayOfBooleans);
}).toThrowError();
expect(() => {
colors.createColorLookupFunction(arrayOfNullValues);
}).toThrowError();
});
});
describe('when input is an array of strings, numbers, or undefined values', () => {
it('should not throw an error', () => {
expect(() => {
colors.createColorLookupFunction(arr);
}).not.toThrowError();
expect(() => {
colors.createColorLookupFunction(arrayOfNumbers);
}).not.toThrowError();
expect(() => {
colors.createColorLookupFunction(arrayOfUndefinedValues);
}).not.toThrowError();
});
});
it('should be a function', () => {
expect(typeof colors.createColorLookupFunction).toBe('function');
});
it('should return a function', () => {
expect(typeof color).toBe('function');
});
it('should return the first hex color in the seed colors array', () => {
expect(color(arr[0])).toBe(seedColors[0]);
});
it('should return the value from the mapped colors', () => {
expect(color(arr[1])).toBe(colors.mappedColors.get(arr[1]));
});
it('should return the value from the specified color mapping overrides', () => {
const colorFn = colors.createColorLookupFunction(arr, { good: 'red' });
expect(colorFn('good')).toBe('red');
});
});

View file

@ -0,0 +1,74 @@
/*
* 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 _ from 'lodash';
import { CoreSetup } from 'kibana/public';
import { MappedColors } from './mapped_colors';
import { seedColors } from './seed_colors';
/**
* Accepts an array of strings or numbers that are used to create a
* a lookup table that associates the values (key) with a hex color (value).
* Returns a function that accepts a value (i.e. a string or number)
* and returns a hex color associated with that value.
*/
export class ColorsService {
private _mappedColors?: MappedColors;
public readonly seedColors = seedColors;
public get mappedColors() {
if (!this._mappedColors) {
throw new Error('ColorService not yet initialized');
}
return this._mappedColors;
}
init(uiSettings: CoreSetup['uiSettings']) {
this._mappedColors = new MappedColors(uiSettings);
}
createColorLookupFunction(
arrayOfStringsOrNumbers?: any,
colorMapping: Partial<Record<string, string>> = {}
) {
if (!Array.isArray(arrayOfStringsOrNumbers)) {
throw new Error(
`createColorLookupFunction expects an array but recived: ${typeof arrayOfStringsOrNumbers}`
);
}
arrayOfStringsOrNumbers.forEach(function(val) {
if (!_.isString(val) && !_.isNumber(val) && !_.isUndefined(val)) {
throw new TypeError(
'createColorLookupFunction expects an array of strings, numbers, or undefined values'
);
}
});
this.mappedColors.mapKeys(arrayOfStringsOrNumbers);
return (value: string) => {
return colorMapping[value] || this.mappedColors.get(value);
};
}
}

View file

@ -0,0 +1,87 @@
/*
* 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 { seedColors } from './seed_colors';
import { createColorPalette } from './color_palette';
describe('Color Palette', () => {
const num1 = 45;
const num2 = 72;
const num3 = 90;
const string = 'Welcome';
const bool = true;
const nullValue = null;
const emptyArr: [] = [];
const emptyObject = {};
let colorPalette: string[];
beforeEach(() => {
colorPalette = createColorPalette(num1);
});
it('should throw an error if input is not a number', () => {
expect(() => {
createColorPalette(string);
}).toThrowError();
expect(() => {
createColorPalette(bool);
}).toThrowError();
expect(() => {
createColorPalette(nullValue);
}).toThrowError();
expect(() => {
createColorPalette(emptyArr);
}).toThrowError();
expect(() => {
createColorPalette(emptyObject);
}).toThrowError();
expect(() => {
createColorPalette();
}).toThrowError();
});
it('should be a function', () => {
expect(typeof createColorPalette).toBe('function');
});
it('should return an array', () => {
expect(colorPalette).toBeInstanceOf(Array);
});
it('should return an array of the same length as the input', () => {
expect(colorPalette.length).toBe(num1);
});
it('should return the seed color array when input length is 72', () => {
expect(createColorPalette(num2)[71]).toBe(seedColors[71]);
});
it('should return an array of the same length as the input when input is greater than 72', () => {
expect(createColorPalette(num3).length).toBe(num3);
});
it('should create new darker colors when input is greater than 72', () => {
expect(createColorPalette(num3)[72]).not.toEqual(seedColors[0]);
});
});

View file

@ -17,7 +17,4 @@
* under the License.
*/
import { EuiUtils } from './eui_utils';
export const plugin = () => new EuiUtils();
export type EuiUtilsStart = ReturnType<EuiUtils['start']>;
export { ColorsService } from './colors';

View file

@ -0,0 +1,163 @@
/*
* 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 _ from 'lodash';
import d3 from 'd3';
import { coreMock } from '../../../../../core/public/mocks';
import { seedColors } from './seed_colors';
import { MappedColors } from './mapped_colors';
// Local state for config
const config = new Map<string, any>();
describe('Mapped Colors', () => {
const mockUiSettings = coreMock.createSetup().uiSettings;
mockUiSettings.get.mockImplementation(a => config.get(a));
mockUiSettings.set.mockImplementation((...a) => config.set(...a) as any);
const mappedColors = new MappedColors(mockUiSettings);
let previousConfig: any;
beforeEach(() => {
previousConfig = config.get('visualization:colorMapping');
mappedColors.purge();
});
afterEach(() => {
config.set('visualization:colorMapping', previousConfig);
});
it('should properly map keys to unique colors', () => {
config.set('visualization:colorMapping', {});
const arr = [1, 2, 3, 4, 5];
mappedColors.mapKeys(arr);
expect(
_(mappedColors.mapping)
.values()
.uniq()
.size()
).toBe(arr.length);
});
it('should not include colors used by the config', () => {
const newConfig = { bar: seedColors[0] };
config.set('visualization:colorMapping', newConfig);
const arr = ['foo', 'baz', 'qux'];
mappedColors.mapKeys(arr);
const colorValues = _(mappedColors.mapping).values();
expect(colorValues.contains(seedColors[0])).toBe(false);
expect(colorValues.uniq().size()).toBe(arr.length);
});
it('should create a unique array of colors even when config is set', () => {
const newConfig = { bar: seedColors[0] };
config.set('visualization:colorMapping', newConfig);
const arr = ['foo', 'bar', 'baz', 'qux'];
mappedColors.mapKeys(arr);
const expectedSize = _(arr)
.difference(_.keys(newConfig))
.size();
expect(
_(mappedColors.mapping)
.values()
.uniq()
.size()
).toBe(expectedSize);
expect(mappedColors.get(arr[0])).not.toBe(seedColors[0]);
});
it('should treat different formats of colors as equal', () => {
const color = d3.rgb(seedColors[0]);
const rgb = `rgb(${color.r}, ${color.g}, ${color.b})`;
const newConfig = { bar: rgb };
config.set('visualization:colorMapping', newConfig);
const arr = ['foo', 'bar', 'baz', 'qux'];
mappedColors.mapKeys(arr);
const expectedSize = _(arr)
.difference(_.keys(newConfig))
.size();
expect(
_(mappedColors.mapping)
.values()
.uniq()
.size()
).toBe(expectedSize);
expect(mappedColors.get(arr[0])).not.toBe(seedColors[0]);
expect(mappedColors.get('bar')).toBe(seedColors[0]);
});
it('should have a flush method that moves the current map to the old map', function() {
const arr = [1, 2, 3, 4, 5];
mappedColors.mapKeys(arr);
expect(_.keys(mappedColors.mapping).length).toBe(5);
expect(_.keys(mappedColors.oldMap).length).toBe(0);
mappedColors.flush();
expect(_.keys(mappedColors.oldMap).length).toBe(5);
expect(_.keys(mappedColors.mapping).length).toBe(0);
mappedColors.flush();
expect(_.keys(mappedColors.oldMap).length).toBe(0);
expect(_.keys(mappedColors.mapping).length).toBe(0);
});
it('should use colors in the oldMap if they are available', function() {
const arr = [1, 2, 3, 4, 5];
mappedColors.mapKeys(arr);
expect(_.keys(mappedColors.mapping).length).toBe(5);
expect(_.keys(mappedColors.oldMap).length).toBe(0);
mappedColors.flush();
mappedColors.mapKeys([3, 4, 5]);
expect(_.keys(mappedColors.oldMap).length).toBe(5);
expect(_.keys(mappedColors.mapping).length).toBe(3);
expect(mappedColors.mapping[1]).toBe(undefined);
expect(mappedColors.mapping[2]).toBe(undefined);
expect(mappedColors.mapping[3]).toEqual(mappedColors.oldMap[3]);
expect(mappedColors.mapping[4]).toEqual(mappedColors.oldMap[4]);
expect(mappedColors.mapping[5]).toEqual(mappedColors.oldMap[5]);
});
it('should have a purge method that clears both maps', function() {
const arr = [1, 2, 3, 4, 5];
mappedColors.mapKeys(arr);
mappedColors.flush();
mappedColors.mapKeys(arr);
expect(_.keys(mappedColors.mapping).length).toBe(5);
expect(_.keys(mappedColors.oldMap).length).toBe(5);
mappedColors.purge();
expect(_.keys(mappedColors.mapping).length).toBe(0);
expect(_.keys(mappedColors.oldMap).length).toBe(0);
});
});

View file

@ -19,64 +19,75 @@
import _ from 'lodash';
import d3 from 'd3';
import chrome from '../../../chrome';
import { CoreSetup } from 'kibana/public';
import { createColorPalette } from './color_palette';
const standardizeColor = color => d3.rgb(color).toString();
const standardizeColor = (color: string) => d3.rgb(color).toString();
const config = chrome.getUiSettingsClient();
function getConfigColorMapping() {
return _.mapValues(config.get('visualization:colorMapping'), standardizeColor);
}
/*
/**
* Maintains a lookup table that associates the value (key) with a hex color (value)
* across the visualizations.
* Provides functions to interact with the lookup table
*/
class MappedColors {
constructor() {
this.oldMap = {};
this.mapping = {};
export class MappedColors {
private _oldMap: any;
private _mapping: any;
constructor(private uiSettings: CoreSetup['uiSettings']) {
this._oldMap = {};
this._mapping = {};
}
get(key) {
return getConfigColorMapping()[key] || this.mapping[key];
private getConfigColorMapping() {
return _.mapValues(this.uiSettings.get('visualization:colorMapping'), standardizeColor);
}
public get oldMap(): any {
return this._oldMap;
}
public get mapping(): any {
return this._mapping;
}
get(key: string | number) {
return this.getConfigColorMapping()[key] || this._mapping[key];
}
flush() {
this.oldMap = _.clone(this.mapping);
this.mapping = {};
this._oldMap = _.clone(this._mapping);
this._mapping = {};
}
purge() {
this.oldMap = {};
this.mapping = {};
this._oldMap = {};
this._mapping = {};
}
mapKeys(keys) {
const configMapping = getConfigColorMapping();
mapKeys(keys: Array<string | number>) {
const configMapping = this.getConfigColorMapping();
const configColors = _.values(configMapping);
const oldColors = _.values(this.oldMap);
const oldColors = _.values(this._oldMap);
const keysToMap = [];
const keysToMap: Array<string | number> = [];
_.each(keys, key => {
// If this key is mapped in the config, it's unnecessary to have it mapped here
if (configMapping[key]) delete this.mapping[key];
if (configMapping[key]) delete this._mapping[key];
// If this key is mapped to a color used by the config color mapping, we need to remap it
if (_.contains(configColors, this.mapping[key])) keysToMap.push(key);
if (_.contains(configColors, this._mapping[key])) keysToMap.push(key);
// if key exist in oldMap, move it to mapping
if (this.oldMap[key]) this.mapping[key] = this.oldMap[key];
if (this._oldMap[key]) this._mapping[key] = this._oldMap[key];
// If this key isn't mapped, we need to map it
if (this.get(key) == null) keysToMap.push(key);
});
// Generate a color palette big enough that all new keys can have unique color values
const allColors = _(this.mapping)
const allColors = _(this._mapping)
.values()
.union(configColors)
.union(oldColors)
@ -88,8 +99,6 @@ class MappedColors {
newColors = newColors.concat(_.sample(allColors, keysToMap.length - newColors.length));
}
_.merge(this.mapping, _.zipObject(keysToMap, newColors));
_.merge(this._mapping, _.zipObject(keysToMap, newColors));
}
}
export const mappedColors = new MappedColors();

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 { ColorsService } from './colors';
import { coreMock } from '../../../../../core/public/mocks';
const colors = new ColorsService();
colors.init(coreMock.createSetup().uiSettings);
export const colorsServiceMock: ColorsService = {
createColorLookupFunction: jest.fn(colors.createColorLookupFunction),
} as any;

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 { seedColors } from './seed_colors';
describe('Seed Colors', function() {
it('should return an array', function() {
expect(seedColors).toBeInstanceOf(Array);
});
});

View file

@ -17,13 +17,12 @@
* under the License.
*/
/*
/**
* Using a random color generator presented awful colors and unpredictable color schemes.
* So we needed to come up with a color scheme of our own that creates consistent, pleasing color patterns.
* The order allows us to guarantee that 1st, 2nd, 3rd, etc values always get the same color.
*/
export const seedColors = [
export const seedColors: string[] = [
'#00a69b',
'#57c17b',
'#6f87d8',

View file

@ -17,8 +17,5 @@
* under the License.
*/
export * from './color_maps';
// @ts-ignore
export { getHeatmapColors } from './heatmap_color';
// @ts-ignore
export * from './truncated_color_maps';
export { ColorsService } from './colors';
export { ThemeService } from './theme';

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 { ThemeService } from './theme';

View file

@ -0,0 +1,29 @@
/*
* 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 { EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
import { ThemeService } from './theme';
export const themeServiceMock: ThemeService = {
chartsTheme$: jest.fn(() => ({
subsribe: jest.fn(),
})),
chartsDefaultTheme: EUI_CHARTS_THEME_LIGHT.theme,
useChartsTheme: jest.fn(),
} as any;

View file

@ -18,49 +18,53 @@
*/
import { BehaviorSubject } from 'rxjs';
import { take } from 'rxjs/operators';
import { renderHook, act } from '@testing-library/react-hooks';
import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
import { EuiUtils } from './eui_utils';
import { coreMock } from '../../../core/public/mocks';
import { take } from 'rxjs/operators';
const startMock = coreMock.createStart();
describe('EuiUtils', () => {
describe('getChartsTheme()', () => {
import { ThemeService } from './theme';
import { coreMock } from '../../../../../core/public/mocks';
const { uiSettings: setupMockUiSettings } = coreMock.createSetup();
describe('ThemeService', () => {
describe('chartsTheme$', () => {
it('should throw error if service has not been initialized', () => {
const themeService = new ThemeService();
expect(() => themeService.chartsTheme$).toThrowError();
});
it('returns the light theme when not in dark mode', async () => {
startMock.uiSettings.get$.mockReturnValue(new BehaviorSubject(false));
setupMockUiSettings.get$.mockReturnValue(new BehaviorSubject(false));
const themeService = new ThemeService();
themeService.init(setupMockUiSettings);
expect(
await new EuiUtils()
.start(startMock)
.getChartsTheme$()
.pipe(take(1))
.toPromise()
).toEqual(EUI_CHARTS_THEME_LIGHT.theme);
expect(await themeService.chartsTheme$.pipe(take(1)).toPromise()).toEqual(
EUI_CHARTS_THEME_LIGHT.theme
);
});
describe('in dark mode', () => {
it(`returns the dark theme`, async () => {
// Fake dark theme turned returning true
startMock.uiSettings.get$.mockReturnValue(new BehaviorSubject(true));
setupMockUiSettings.get$.mockReturnValue(new BehaviorSubject(true));
const themeService = new ThemeService();
themeService.init(setupMockUiSettings);
expect(
await new EuiUtils()
.start(startMock)
.getChartsTheme$()
.pipe(take(1))
.toPromise()
).toEqual(EUI_CHARTS_THEME_DARK.theme);
expect(await themeService.chartsTheme$.pipe(take(1)).toPromise()).toEqual(
EUI_CHARTS_THEME_DARK.theme
);
});
});
});
describe('useChartsTheme()', () => {
describe('useChartsTheme', () => {
it('updates when the uiSettings change', () => {
const darkMode$ = new BehaviorSubject(false);
startMock.uiSettings.get$.mockReturnValue(darkMode$);
const { useChartsTheme } = new EuiUtils().start(startMock);
setupMockUiSettings.get$.mockReturnValue(darkMode$);
const themeService = new ThemeService();
themeService.init(setupMockUiSettings);
const { useChartsTheme } = themeService;
const { result } = renderHook(() => useChartsTheme());
expect(result.current).toBe(EUI_CHARTS_THEME_LIGHT.theme);

View file

@ -0,0 +1,63 @@
/*
* 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 { useEffect, useState } from 'react';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { CoreSetup } from 'kibana/public';
import { RecursivePartial, Theme } from '@elastic/charts';
import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
export class ThemeService {
private _chartsTheme$?: Observable<RecursivePartial<Theme>>;
/** Returns default charts theme */
public readonly chartsDefaultTheme = EUI_CHARTS_THEME_LIGHT.theme;
/** An observable of the current charts theme */
public get chartsTheme$(): Observable<RecursivePartial<Theme>> {
if (!this._chartsTheme$) {
throw new Error('ThemeService not initialized');
}
return this._chartsTheme$;
}
/** A React hook for consuming the charts theme */
public useChartsTheme = () => {
const [value, update] = useState(this.chartsDefaultTheme);
useEffect(() => {
const s = this.chartsTheme$.subscribe(update);
return () => s.unsubscribe();
}, []);
return value;
};
/** initialize service with uiSettings */
public init(uiSettings: CoreSetup['uiSettings']) {
this._chartsTheme$ = uiSettings
.get$('theme:darkMode')
.pipe(
map(darkMode => (darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme))
);
}
}

View file

@ -33,11 +33,21 @@ export interface ColorSchema {
text: string;
}
export const vislibColorMaps = {
export interface RawColorSchema {
id: ColorSchemas;
label: string;
value: Array<[number, number[]]>;
}
export interface ColorMap {
[key: string]: RawColorSchema;
}
export const vislibColorMaps: ColorMap = {
// Sequential
[ColorSchemas.Blues]: {
id: ColorSchemas.Blues,
label: i18n.translate('common.ui.vislib.colormaps.bluesText', {
label: i18n.translate('charts.colormaps.bluesText', {
defaultMessage: 'Blues',
}),
value: [
@ -557,7 +567,7 @@ export const vislibColorMaps = {
},
[ColorSchemas.Greens]: {
id: ColorSchemas.Greens,
label: i18n.translate('common.ui.vislib.colormaps.greensText', {
label: i18n.translate('charts.colormaps.greensText', {
defaultMessage: 'Greens',
}),
value: [
@ -1077,7 +1087,7 @@ export const vislibColorMaps = {
},
[ColorSchemas.Greys]: {
id: ColorSchemas.Greys,
label: i18n.translate('common.ui.vislib.colormaps.greysText', {
label: i18n.translate('charts.colormaps.greysText', {
defaultMessage: 'Greys',
}),
value: [
@ -1597,7 +1607,7 @@ export const vislibColorMaps = {
},
[ColorSchemas.Reds]: {
id: ColorSchemas.Reds,
label: i18n.translate('common.ui.vislib.colormaps.redsText', {
label: i18n.translate('charts.colormaps.redsText', {
defaultMessage: 'Reds',
}),
value: [
@ -2117,7 +2127,7 @@ export const vislibColorMaps = {
},
[ColorSchemas.YellowToRed]: {
id: ColorSchemas.YellowToRed,
label: i18n.translate('common.ui.vislib.colormaps.yellowToRedText', {
label: i18n.translate('charts.colormaps.yellowToRedText', {
defaultMessage: 'Yellow to Red',
}),
value: [
@ -2638,7 +2648,7 @@ export const vislibColorMaps = {
[ColorSchemas.GreenToRed]: {
id: ColorSchemas.GreenToRed,
label: i18n.translate('common.ui.vislib.colormaps.greenToRedText', {
label: i18n.translate('charts.colormaps.greenToRedText', {
defaultMessage: 'Green to Red',
}),
value: [
@ -3158,7 +3168,7 @@ export const vislibColorMaps = {
},
};
export const colorSchemas = Object.values(vislibColorMaps).map(({ id, label }) => ({
export const colorSchemas: ColorSchema[] = Object.values(vislibColorMaps).map(({ id, label }) => ({
value: id,
text: label,
}));

View file

@ -17,76 +17,83 @@
* under the License.
*/
import expect from '@kbn/expect';
import { getHeatmapColors } from './heatmap_color';
import { getHeatmapColors } from '../../../legacy_imports';
describe('Vislib Heatmap Color Module Test Suite', function() {
describe('Vislib Heatmap Color Module Test Suite', () => {
const emptyObject = {};
const nullValue = null;
let notAValue;
it('should throw an error if schema is invalid', function() {
expect(function() {
it('should throw an error if schema is invalid', () => {
expect(() => {
getHeatmapColors(4, 'invalid schema');
}).to.throwError();
}).toThrowError();
});
it('should throw an error if input is not a number', function() {
expect(function() {
it('should throw an error if input is not a number', () => {
expect(() => {
getHeatmapColors([200], 'Greens');
}).to.throwError();
}).toThrowError();
expect(function() {
expect(() => {
getHeatmapColors('help', 'Greens');
}).to.throwError();
}).toThrowError();
expect(function() {
expect(() => {
getHeatmapColors(true, 'Greens');
}).to.throwError();
}).toThrowError();
expect(function() {
getHeatmapColors(notAValue, 'Greens');
}).to.throwError();
expect(() => {
getHeatmapColors(undefined, 'Greens');
}).toThrowError();
expect(function() {
expect(() => {
getHeatmapColors(nullValue, 'Greens');
}).to.throwError();
}).toThrowError();
expect(function() {
expect(() => {
getHeatmapColors(emptyObject, 'Greens');
}).to.throwError();
}).toThrowError();
});
it('should throw an error if input is less than 0', function() {
expect(function() {
it('should throw an error if input is less than 0', () => {
expect(() => {
getHeatmapColors(-2, 'Greens');
}).to.throwError();
}).toThrowError();
});
it('should throw an error if input is greater than 1', function() {
expect(function() {
it('should throw an error if input is greater than 1', () => {
expect(() => {
getHeatmapColors(2, 'Greens');
}).to.throwError();
}).toThrowError();
});
it('should be a function', function() {
expect(typeof getHeatmapColors).to.be('function');
it('should be a function', () => {
expect(typeof getHeatmapColors).toBe('function');
});
it('should return a color for 10 numbers from 0 to 1', function() {
it('should return a color for 10 numbers from 0 to 1', () => {
const colorRegex = /^rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)$/;
const schema = 'Greens';
for (let i = 0; i < 10; i++) {
expect(getHeatmapColors(i / 10, schema)).to.match(colorRegex);
expect(getHeatmapColors(i / 10, schema)).toMatch(colorRegex);
}
});
describe('drawColormap function', () => {
const canvasElement = {
getContext: jest.fn(() => ({
fillStyle: null,
fillRect: jest.fn(),
})),
};
beforeEach(() => {
jest.spyOn(document, 'createElement').mockImplementation(() => canvasElement as any);
});
it('should return canvas element', () => {
const response = getHeatmapColors.prototype.drawColormap('Greens');
expect(typeof response).to.equal('object');
expect(response instanceof window.HTMLElement).to.equal(true);
expect(typeof response).toEqual('object');
expect(response).toBe(canvasElement);
});
});
});

View file

@ -18,9 +18,10 @@
*/
import _ from 'lodash';
import { vislibColorMaps } from './color_maps';
function enforceBounds(x) {
import { vislibColorMaps, RawColorSchema } from './color_maps';
function enforceBounds(x: number) {
if (x < 0) {
return 0;
} else if (x > 1) {
@ -30,12 +31,12 @@ function enforceBounds(x) {
}
}
function interpolateLinearly(x, values) {
function interpolateLinearly(x: number, values: RawColorSchema['value']) {
// Split values into four lists
const xValues = [];
const rValues = [];
const gValues = [];
const bValues = [];
const xValues: number[] = [];
const rValues: number[] = [];
const gValues: number[] = [];
const bValues: number[] = [];
values.forEach(value => {
xValues.push(value[0]);
rValues.push(value[1][0]);
@ -54,11 +55,12 @@ function interpolateLinearly(x, values) {
return [enforceBounds(r), enforceBounds(g), enforceBounds(b)];
}
export function getHeatmapColors(value, colorSchemaName) {
export function getHeatmapColors(value: any, colorSchemaName: string) {
if (!_.isNumber(value) || value < 0 || value > 1) {
throw new Error('heatmap_color expects a number from 0 to 1 as first parameter');
}
// @ts-ignore
const colorSchema = vislibColorMaps[colorSchemaName].value;
if (!colorSchema) {
throw new Error('invalid colorSchemaName provided');
@ -71,11 +73,16 @@ export function getHeatmapColors(value, colorSchemaName) {
return `rgb(${r},${g},${b})`;
}
function drawColormap(colorSchema, width = 100, height = 10) {
function drawColormap(colorSchema: string, width = 100, height = 10) {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (ctx === null) {
throw new Error('no HeatmapColors canvas context found');
}
for (let i = 0; i <= width; i++) {
ctx.fillStyle = getHeatmapColors(i / width, colorSchema);
ctx.fillRect(i, 0, 1, height);

View file

@ -0,0 +1,29 @@
/*
* 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 {
ColorSchemas,
ColorSchema,
RawColorSchema,
ColorMap,
vislibColorMaps,
colorSchemas,
} from './color_maps';
export { getHeatmapColors } from './heatmap_color';
export { truncatedColorMaps, truncatedColorSchemas } from './truncated_color_maps';

View file

@ -0,0 +1,31 @@
/*
* 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 { colorSchemas, vislibColorMaps } from './color_maps';
import { getHeatmapColors } from './heatmap_color';
import { truncatedColorMaps, truncatedColorSchemas } from './truncated_color_maps';
// Note: Using actual values due to existing test dependencies
export const colorMapsMock = {
getHeatmapColors: jest.fn(getHeatmapColors),
vislibColorMaps,
colorSchemas,
truncatedColorMaps,
truncatedColorSchemas,
} as any;

View file

@ -17,22 +17,26 @@
* under the License.
*/
import { vislibColorMaps } from './color_maps';
import { vislibColorMaps, ColorMap, ColorSchema } from './color_maps';
export const truncatedColorMaps = {};
export const truncatedColorMaps: ColorMap = {};
const colormaps = vislibColorMaps;
const colormaps: ColorMap = vislibColorMaps;
for (const key in colormaps) {
if (colormaps.hasOwnProperty(key)) {
// slice off lightest colors
// @ts-ignore
const color = colormaps[key];
truncatedColorMaps[key] = {
...colormaps[key],
value: colormaps[key].value.slice(Math.floor(colormaps[key].value.length / 4)),
...color,
value: color.value.slice(Math.floor(color.value.length / 4)),
};
}
}
export const truncatedColorSchemas = Object.values(truncatedColorMaps).map(({ id, label }) => ({
value: id,
text: label,
}));
export const truncatedColorSchemas: ColorSchema[] = Object.values(truncatedColorMaps).map(
({ id, label }) => ({
value: id,
text: label,
})
);

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { vislibColor } from './color';
export * from './color_maps';

View file

@ -1,67 +0,0 @@
# EuiUtils
The EuiUtils plugin is a way to create easier integration of EUI colors, themes, and other utilities with Kibana. They usually take into account the current theme (light or dark) of Kibana and return the correct object that was asked for.
## EUI plus Elastic-Charts
EUI provides a light and dark theme object to work with Elastic-Charts. However, every instance of a Chart would need to pass down this the correctly EUI theme depending on Kibana's light or dark mode. There are several ways you can use EuiUtils to grab the correct theme.
### `useChartsTheme`
The simple fetching of the correct EUI theme; a **React hook**.
```js
import { npStart } from 'ui/new_platform';
import { Chart, Settings } from '@elastic/charts';
export const YourComponent = () => (
<Chart>
<Settings theme={npStart.plugins.eui_utils.useChartsTheme()} />
</Chart>
);
```
### `getChartsTheme$`
An **observable** of the current charts theme. Use this implementation for more flexible updates to the chart theme without full page refreshes.
```ts
import { npStart } from 'ui/new_platform';
import { EuiChartThemeType } from '@elastic/eui/src/themes/charts/themes';
import { Subscription } from 'rxjs';
import { Chart, Settings } from '@elastic/charts';
interface YourComponentState {
chartsTheme: EuiChartThemeType['theme'];
}
export class YourComponent extends Component<YourComponentProps, YourComponentState> {
private subscription?: Subscription;
public state = {
chartsTheme: npStart.plugins.eui_utils.getChartsThemeDefault(),
};
componentDidMount() {
this.subscription = npStart.plugins.eui_utils
.getChartsTheme$()
.subscribe(chartsTheme => this.setState({ chartsTheme }));
}
componentWillUnmount() {
if (this.subscription) {
this.subscription.unsubscribe();
this.subscription = undefined;
}
}
public render() {
const { chartsTheme } = this.state;
return (
<Chart>
<Settings theme={chartsTheme} />
</Chart>
);
}
}
```

View file

@ -1,61 +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 { useEffect, useState } from 'react';
import { CoreStart, CoreSetup } from 'src/core/public';
import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
import { map } from 'rxjs/operators';
export class EuiUtils {
public setup(core: CoreSetup) {}
public start(core: CoreStart) {
const getChartsThemeDefault = () => EUI_CHARTS_THEME_LIGHT.theme;
const getChartsTheme$ = () => {
return core.uiSettings
.get$('theme:darkMode')
.pipe(
map(darkMode => (darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme))
);
};
const useChartsTheme = () => {
const [value, update] = useState(getChartsThemeDefault());
useEffect(() => {
const s = getChartsTheme$().subscribe(update);
return () => s.unsubscribe();
}, [false]);
return value;
};
return {
/** The default charts theme */
getChartsThemeDefault,
/** An observable of the current charts theme */
getChartsTheme$,
/** A React hook for consuming the charts theme */
useChartsTheme,
};
}
}

View file

@ -30,7 +30,6 @@ import 'uiExports/shareContextMenuExtensions';
import _ from 'lodash';
import 'ui/autoload/all';
import 'ui/kbn_top_nav';
import 'ui/color_maps';
import 'ui/agg_response';
import 'ui/agg_types';
import 'leaflet';

View file

@ -6,13 +6,13 @@
import { InnerJoin } from './inner_join';
jest.mock('ui/new_platform');
jest.mock('ui/vis/editors/default/schemas', () => {
class MockSchemas {}
return {
Schemas: MockSchemas,
};
});
jest.mock('../../kibana_services', () => {});
jest.mock('ui/agg_types', () => {});
jest.mock('ui/timefilter', () => {});
jest.mock('../vector_layer', () => {});

View file

@ -6,7 +6,7 @@
import { EMSFileSource } from './ems_file_source';
jest.mock('../../../kibana_services', () => {});
jest.mock('ui/new_platform');
jest.mock('../../vector_layer', () => {});
function makeEMSFileSource(tooltipProperties) {

View file

@ -6,11 +6,11 @@
import { ESTermSource, extractPropertiesMap } from './es_term_source';
jest.mock('ui/new_platform');
jest.mock('../vector_layer', () => {});
jest.mock('ui/vis/editors/default/schemas', () => ({
Schemas: function() {},
}));
jest.mock('../../kibana_services', () => {});
jest.mock('ui/agg_types', () => {});
jest.mock('ui/timefilter', () => {});

View file

@ -5,14 +5,16 @@
*/
import React from 'react';
import { vislibColorMaps } from 'ui/color_maps';
import { getLegendColors, getColor } from 'ui/vis/map/color_util';
import { ColorGradient } from './components/color_gradient';
import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
import tinycolor from 'tinycolor2';
import chroma from 'chroma-js';
import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
import { getLegendColors, getColor } from 'ui/vis/map/color_util';
import { ColorGradient } from './components/color_gradient';
import { COLOR_PALETTE_MAX_SIZE } from '../../../common/constants';
import { vislibColorMaps } from '../../../../../../../src/plugins/charts/public';
const GRADIENT_INTERVALS = 8;

View file

@ -13,6 +13,8 @@ import {
getRGBColorRangeStrings,
} from './color_utils';
jest.mock('ui/new_platform');
describe('COLOR_GRADIENTS', () => {
it('Should contain EuiSuperSelect options list of color ramps', () => {
expect(COLOR_GRADIENTS.length).toBe(6);

View file

@ -9,6 +9,8 @@ import { shallow } from 'enzyme';
import { HeatmapStyleEditor } from './heatmap_style_editor';
jest.mock('ui/new_platform');
describe('HeatmapStyleEditor', () => {
test('is rendered', () => {
const component = shallow(

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
jest.mock('ui/new_platform');
jest.mock('../components/vector_style_editor', () => ({
VectorStyleEditor: () => {
return <div>mockVectorStyleEditor</div>;

View file

@ -9,6 +9,8 @@ import { DataRequest } from '../../util/data_request';
import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types';
import { FIELD_ORIGIN } from '../../../../common/constants';
jest.mock('ui/new_platform');
class MockField {
constructor({ fieldName }) {
this._fieldName = fieldName;

View file

@ -100,7 +100,7 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({ alert }
const [error, setError] = useState<undefined | any>(undefined);
const [visualizationData, setVisualizationData] = useState<Record<string, any>>([]);
const chartsTheme = npStart.plugins.eui_utils.useChartsTheme();
const chartsTheme = npStart.plugins.charts.theme.useChartsTheme();
const {
index,
timeField,

View file

@ -506,12 +506,12 @@
"common.ui.vis.editors.sidebar.tabs.optionsLabel": "オプション",
"common.ui.vis.kibanaMap.leaflet.fitDataBoundsAriaLabel": "データバウンドを合わせる",
"common.ui.vis.kibanaMap.zoomWarning": "ズームレベルが最大に達しました。完全にズームインするには、Elasticsearch と Kibana の {defaultDistribution} にアップグレードしてください。{ems} でより多くのズームレベルが利用できます。または、独自のマップサーバーを構成できます。詳細は { wms } または { configSettings} をご覧ください。",
"common.ui.vislib.colormaps.bluesText": "青",
"common.ui.vislib.colormaps.greensText": "緑",
"common.ui.vislib.colormaps.greenToRedText": "緑から赤",
"common.ui.vislib.colormaps.greysText": "グレー",
"common.ui.vislib.colormaps.redsText": "赤",
"common.ui.vislib.colormaps.yellowToRedText": "黄色から赤",
"charts.colormaps.bluesText": "青",
"charts.colormaps.greensText": "緑",
"charts.colormaps.greenToRedText": "緑から赤",
"charts.colormaps.greysText": "グレー",
"charts.colormaps.redsText": "赤",
"charts.colormaps.yellowToRedText": "黄色から赤",
"common.ui.visualize.queryGeohashBounds.unableToGetBoundErrorTitle": "バウンドを取得できませんでした",
"console.autocomplete.addMethodMetaText": "メソド",
"console.consoleDisplayName": "コンソール",

Some files were not shown because too many files have changed in this diff Show more