[Step 1] Move CustomizablePalette from Lens into @kbn/coloring package (#129291)

* [Step 1] Move Coloring into Shared-ux-Components

* Fix Joe's comments

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Alexey Antonov 2022-04-07 14:49:36 +05:00 committed by GitHub
parent 8a0cc6efcd
commit ac50a30eb6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
116 changed files with 2369 additions and 1697 deletions

View file

@ -66,6 +66,7 @@
"share": "src/plugins/share",
"sharedUX": "src/plugins/shared_ux",
"sharedUXComponents": "packages/kbn-shared-ux-components/src",
"coloring": "packages/kbn-coloring/src",
"statusPage": "src/legacy/core_plugins/status_page",
"telemetry": [
"src/plugins/telemetry",
@ -94,4 +95,4 @@
"unifiedSearch": "src/plugins/unified_search"
},
"translations": []
}
}

View file

@ -136,6 +136,7 @@
"@kbn/analytics": "link:bazel-bin/packages/kbn-analytics",
"@kbn/apm-config-loader": "link:bazel-bin/packages/kbn-apm-config-loader",
"@kbn/apm-utils": "link:bazel-bin/packages/kbn-apm-utils",
"@kbn/coloring": "link:bazel-bin/packages/kbn-coloring",
"@kbn/config": "link:bazel-bin/packages/kbn-config",
"@kbn/config-schema": "link:bazel-bin/packages/kbn-config-schema",
"@kbn/crypto": "link:bazel-bin/packages/kbn-crypto",
@ -590,6 +591,7 @@
"@types/kbn__axe-config": "link:bazel-bin/packages/kbn-axe-config/npm_module_types",
"@types/kbn__bazel-packages": "link:bazel-bin/packages/kbn-bazel-packages/npm_module_types",
"@types/kbn__cli-dev-mode": "link:bazel-bin/packages/kbn-cli-dev-mode/npm_module_types",
"@types/kbn__coloring": "link:bazel-bin/packages/kbn-coloring/npm_module_types",
"@types/kbn__config": "link:bazel-bin/packages/kbn-config/npm_module_types",
"@types/kbn__config-schema": "link:bazel-bin/packages/kbn-config-schema/npm_module_types",
"@types/kbn__crypto": "link:bazel-bin/packages/kbn-crypto/npm_module_types",

View file

@ -26,6 +26,7 @@ filegroup(
"//packages/kbn-babel-preset:build",
"//packages/kbn-bazel-packages:build",
"//packages/kbn-cli-dev-mode:build",
"//packages/kbn-coloring:build",
"//packages/kbn-config-schema:build",
"//packages/kbn-config:build",
"//packages/kbn-crypto:build",
@ -111,6 +112,7 @@ filegroup(
"//packages/kbn-axe-config:build_types",
"//packages/kbn-bazel-packages:build_types",
"//packages/kbn-cli-dev-mode:build_types",
"//packages/kbn-coloring:build_types",
"//packages/kbn-config-schema:build_types",
"//packages/kbn-config:build_types",
"//packages/kbn-crypto:build_types",

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = require('@kbn/storybook').defaultConfig;

View file

@ -0,0 +1,146 @@
load("@npm//@bazel/typescript:index.bzl", "ts_config")
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")
PKG_DIRNAME = "kbn-coloring"
PKG_REQUIRE_NAME = "@kbn/coloring"
SOURCE_FILES = glob(
[
"src/**/*.scss",
"src/**/*.ts",
"src/**/*.tsx",
],
exclude = [
"**/*.test.*",
"**/*.stories.*",
],
)
SRCS = SOURCE_FILES
filegroup(
name = "srcs",
srcs = SRCS,
)
NPM_MODULE_EXTRA_FILES = [
"package.json",
]
# In this array place runtime dependencies, including other packages and NPM packages
# which must be available for this code to run.
#
# To reference other packages use:
# "//repo/relative/path/to/package"
# eg. "//packages/kbn-utils"
#
# To reference a NPM package use:
# "@npm//name-of-package"
# eg. "@npm//lodash"
RUNTIME_DEPS = [
"//packages/kbn-i18n",
"//packages/kbn-i18n-react",
"//packages/kbn-shared-ux-storybook",
"//packages/kbn-interpreter",
"//packages/kbn-utility-types",
"//packages/kbn-shared-ux-utility",
"@npm//chroma-js",
"@npm//@elastic/eui",
"@npm//react-use",
"@npm//react",
]
# In this array place dependencies necessary to build the types, which will include the
# :npm_module_types target of other packages and packages from NPM, including @types/*
# packages.
#
# To reference the types for another package use:
# "//repo/relative/path/to/package:npm_module_types"
# eg. "//packages/kbn-utils:npm_module_types"
#
# References to NPM packages work the same as RUNTIME_DEPS
TYPES_DEPS = [
"//packages/kbn-i18n:npm_module_types",
"//packages/kbn-i18n-react:npm_module_types",
"//packages/kbn-shared-ux-storybook:npm_module_types",
"//packages/kbn-interpreter:npm_module_types",
"//packages/kbn-utility-types:npm_module_types",
"//packages/kbn-shared-ux-utility:npm_module_types",
"@npm//@types/chroma-js",
"@npm//@types/react",
"@npm//@elastic/eui",
"@npm//react-use",
]
jsts_transpiler(
name = "target_node",
srcs = SRCS,
build_pkg_name = package_name(),
)
jsts_transpiler(
name = "target_web",
srcs = SRCS,
build_pkg_name = package_name(),
web = True,
additional_args = [
"--copy-files",
"--quiet"
],
)
ts_config(
name = "tsconfig",
src = "tsconfig.json",
deps = [
"//:tsconfig.base.json",
"//:tsconfig.bazel.json",
],
)
ts_project(
name = "tsc_types",
args = ['--pretty'],
srcs = SRCS,
deps = TYPES_DEPS,
declaration = True,
emit_declaration_only = True,
out_dir = "target_types",
root_dir = "src",
tsconfig = ":tsconfig",
)
js_library(
name = PKG_DIRNAME,
srcs = NPM_MODULE_EXTRA_FILES,
deps = RUNTIME_DEPS + [":target_node", ":target_web"],
package_name = PKG_REQUIRE_NAME,
visibility = ["//visibility:public"],
)
pkg_npm(
name = "npm_module",
deps = [":" + PKG_DIRNAME],
)
filegroup(
name = "build",
srcs = [":npm_module"],
visibility = ["//visibility:public"],
)
pkg_npm_types(
name = "npm_module_types",
srcs = SRCS,
deps = [":tsc_types"],
package_name = PKG_REQUIRE_NAME,
tsconfig = ":tsconfig",
visibility = ["//visibility:public"],
)
filegroup(
name = "build_types",
srcs = [":npm_module_types"],
visibility = ["//visibility:public"],
)

View file

@ -0,0 +1,3 @@
# @kbn/coloring
This package contains the types, utility methods, and components needed to make Color Palette work.

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-coloring'],
};

View file

@ -0,0 +1,8 @@
{
"name": "@kbn/coloring",
"private": true,
"version": "1.0.0",
"main": "./target_node/index.js",
"browser": "./target_web/index.js",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './palettes';
export * from './shared_components';

View file

@ -5,26 +5,12 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { RequiredPaletteParamTypes } from '../common';
export const DEFAULT_PALETTE_NAME = 'positive';
export const FIXED_PROGRESSION = 'fixed' as const;
export const CUSTOM_PALETTE = 'custom';
export const DEFAULT_CONTINUITY = 'above';
export const DEFAULT_PALETTE_NAME = 'gray';
export const DEFAULT_COLOR_STEPS = 3;
export const DEFAULT_RANGE_TYPE = 'percent';
export const DEFAULT_MIN_STOP = 0;
export const DEFAULT_MAX_STOP = 100;
export const defaultPaletteParams: RequiredPaletteParamTypes & { maxSteps: number } = {
name: DEFAULT_PALETTE_NAME,
colorStops: [],
continuity: DEFAULT_CONTINUITY,
reverse: false,
rangeType: 'percent',
rangeMin: DEFAULT_MIN_STOP,
rangeMax: DEFAULT_MAX_STOP,
progression: FIXED_PROGRESSION,
stops: [],
steps: DEFAULT_COLOR_STEPS,
maxSteps: 5,
};
export const DEFAULT_COLOR_STEPS = 5;

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './utils';
export * from './types';
export * from './constants';

View file

@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { PaletteDefinition, SeriesLayer } from '../types';
export const getPaletteRegistry = () => {
const mockPalette1: jest.Mocked<PaletteDefinition> = {
id: 'default',
title: 'My Palette',
getCategoricalColor: jest.fn((_: SeriesLayer[]) => 'black'),
getCategoricalColors: jest.fn((num: number) => ['red', 'black']),
toExpression: jest.fn(() => ({
type: 'expression',
chain: [
{
type: 'function',
function: 'system_palette',
arguments: {
name: ['default'],
},
},
],
})),
};
const mockPalette2: jest.Mocked<PaletteDefinition> = {
id: 'mocked',
title: 'Mocked Palette',
getCategoricalColor: jest.fn((_: SeriesLayer[]) => 'blue'),
getCategoricalColors: jest.fn((num: number) => ['blue', 'yellow']),
toExpression: jest.fn(() => ({
type: 'expression',
chain: [
{
type: 'function',
function: 'system_palette',
arguments: {
name: ['mocked'],
},
},
],
})),
};
const mockPalette3: jest.Mocked<PaletteDefinition> = {
id: 'custom',
title: 'Custom Mocked Palette',
getCategoricalColor: jest.fn((_: SeriesLayer[]) => 'blue'),
getCategoricalColors: jest.fn((num: number) => ['blue', 'yellow']),
getColorForValue: jest.fn(
(num: number | undefined, state: unknown, minMax: { min: number; max: number }) =>
num == null || num < 1 ? undefined : 'blue'
),
canDynamicColoring: true,
toExpression: jest.fn(() => ({
type: 'expression',
chain: [
{
type: 'function',
function: 'system_palette',
arguments: {
name: ['mocked'],
},
},
],
})),
};
return {
get: (name: string) =>
name === 'custom' ? mockPalette3 : name !== 'default' ? mockPalette2 : mockPalette1,
getAll: () => [mockPalette1, mockPalette2, mockPalette3],
};
};

View file

@ -6,7 +6,24 @@
* Side Public License, v 1.
*/
import { ExpressionAstExpression } from '../../../../expressions/common/ast';
import { Ast } from '@kbn/interpreter';
import { Assign } from '@kbn/utility-types';
/** @public **/
export type PaletteContinuity = 'above' | 'below' | 'none' | 'all';
/** @public **/
export interface DataBounds {
min: number;
max: number;
fallback?: boolean;
}
export interface PaletteOutput<T = { [key: string]: unknown }> {
type: 'palette' | 'system_palette';
name: string;
params?: T;
}
/**
* Information about a series in a chart used to determine its color.
@ -78,7 +95,7 @@ export interface PaletteDefinition<T = unknown> {
* This function should be used to pass the palette to the expression function applying color and other styles
* @param state The internal state of the palette
*/
toExpression: (state?: T) => ExpressionAstExpression;
toExpression: (state?: T) => Ast;
/**
* Color a series according to the internal rules of the palette.
* @param series The current series along with its ancestors.
@ -110,6 +127,30 @@ export interface PaletteDefinition<T = unknown> {
}
export interface PaletteRegistry {
get: (name: string) => PaletteDefinition<unknown>;
getAll: () => Array<PaletteDefinition<unknown>>;
get: (name: string) => PaletteDefinition;
getAll: () => PaletteDefinition[];
}
export interface CustomPaletteParams {
name?: string;
reverse?: boolean;
rangeType?: 'number' | 'percent';
continuity?: PaletteContinuity;
progression?: 'fixed';
rangeMin?: number;
rangeMax?: number;
stops?: ColorStop[];
colorStops?: ColorStop[];
steps?: number;
maxSteps?: number | undefined;
}
export type RequiredPaletteParamTypes = Assign<
Required<CustomPaletteParams>,
{ maxSteps?: number }
>;
export interface ColorStop {
color: string;
stop: number;
}

View file

@ -0,0 +1,313 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getPaletteRegistry } from './mocks/palettes_registry';
import {
getDataMinMax,
getPaletteStops,
getStepValue,
remapStopsByNewInterval,
reversePalette,
} from './utils';
describe('remapStopsByNewInterval', () => {
it('should correctly remap the current palette from 0..1 to 0...100', () => {
expect(
remapStopsByNewInterval(
[
{ color: 'black', stop: 0 },
{ color: 'green', stop: 0.5 },
{ color: 'red', stop: 0.9 },
],
{ newInterval: 100, oldInterval: 1, newMin: 0, oldMin: 0 }
)
).toEqual([
{ color: 'black', stop: 0 },
{ color: 'green', stop: 50 },
{ color: 'red', stop: 90 },
]);
// now test the other way around
expect(
remapStopsByNewInterval(
[
{ color: 'black', stop: 0 },
{ color: 'green', stop: 50 },
{ color: 'red', stop: 90 },
],
{ newInterval: 1, oldInterval: 100, newMin: 0, oldMin: 0 }
)
).toEqual([
{ color: 'black', stop: 0 },
{ color: 'green', stop: 0.5 },
{ color: 'red', stop: 0.9 },
]);
});
it('should correctly handle negative numbers to/from', () => {
expect(
remapStopsByNewInterval(
[
{ color: 'black', stop: -100 },
{ color: 'green', stop: -50 },
{ color: 'red', stop: -1 },
],
{ newInterval: 100, oldInterval: 100, newMin: 0, oldMin: -100 }
)
).toEqual([
{ color: 'black', stop: 0 },
{ color: 'green', stop: 50 },
{ color: 'red', stop: 99 },
]);
// now map the other way around
expect(
remapStopsByNewInterval(
[
{ color: 'black', stop: 0 },
{ color: 'green', stop: 50 },
{ color: 'red', stop: 99 },
],
{ newInterval: 100, oldInterval: 100, newMin: -100, oldMin: 0 }
)
).toEqual([
{ color: 'black', stop: -100 },
{ color: 'green', stop: -50 },
{ color: 'red', stop: -1 },
]);
// and test also palettes that also contains negative values
expect(
remapStopsByNewInterval(
[
{ color: 'black', stop: -50 },
{ color: 'green', stop: 0 },
{ color: 'red', stop: 50 },
],
{ newInterval: 100, oldInterval: 100, newMin: 0, oldMin: -50 }
)
).toEqual([
{ color: 'black', stop: 0 },
{ color: 'green', stop: 50 },
{ color: 'red', stop: 100 },
]);
});
});
describe('getDataMinMax', () => {
it('should pick the correct min/max based on the current range type', () => {
expect(getDataMinMax('percent', { min: -100, max: 0 })).toEqual({ min: 0, max: 100 });
});
it('should pick the correct min/max apply percent by default', () => {
expect(getDataMinMax(undefined, { min: -100, max: 0 })).toEqual({ min: 0, max: 100 });
});
});
describe('getPaletteStops', () => {
const paletteRegistry = getPaletteRegistry();
it('should correctly compute a predefined palette stops definition from only the name', () => {
expect(
getPaletteStops(paletteRegistry, { name: 'mock' }, { dataBounds: { min: 0, max: 100 } })
).toEqual([
{ color: 'blue', stop: 20 },
{ color: 'yellow', stop: 70 },
]);
});
it('should correctly compute a predefined palette stops definition from explicit prevPalette (override)', () => {
expect(
getPaletteStops(
paletteRegistry,
{ name: 'default' },
{ dataBounds: { min: 0, max: 100 }, prevPalette: 'mock' }
)
).toEqual([
{ color: 'blue', stop: 20 },
{ color: 'yellow', stop: 70 },
]);
});
it('should infer the domain from dataBounds but start from 0', () => {
expect(
getPaletteStops(
paletteRegistry,
{ name: 'default', rangeType: 'number' },
{ dataBounds: { min: 1, max: 11 }, prevPalette: 'mock' }
)
).toEqual([
{ color: 'blue', stop: 2 },
{ color: 'yellow', stop: 7 },
]);
});
it('should override the minStop when requested', () => {
expect(
getPaletteStops(
paletteRegistry,
{ name: 'default', rangeType: 'number' },
{ dataBounds: { min: 1, max: 11 }, mapFromMinValue: true }
)
).toEqual([
{ color: 'red', stop: 1 },
{ color: 'black', stop: 6 },
]);
});
it('should compute a display stop palette from custom colorStops defined by the user', () => {
expect(
getPaletteStops(
paletteRegistry,
{
name: 'custom',
rangeType: 'number',
colorStops: [
{ color: 'green', stop: 0 },
{ color: 'blue', stop: 40 },
{ color: 'red', stop: 80 },
],
},
{ dataBounds: { min: 0, max: 100 } }
)
).toEqual([
{ color: 'green', stop: 40 },
{ color: 'blue', stop: 80 },
{ color: 'red', stop: 100 },
]);
});
it('should compute a display stop palette from custom colorStops defined by the user - handle stop at the end', () => {
expect(
getPaletteStops(
paletteRegistry,
{
name: 'custom',
rangeType: 'number',
colorStops: [
{ color: 'green', stop: 0 },
{ color: 'blue', stop: 40 },
{ color: 'red', stop: 100 },
],
},
{ dataBounds: { min: 0, max: 100 } }
)
).toEqual([
{ color: 'green', stop: 40 },
{ color: 'blue', stop: 100 },
{ color: 'red', stop: 101 },
]);
});
it('should compute a display stop palette from custom colorStops defined by the user - handle stop at the end (fractional)', () => {
expect(
getPaletteStops(
paletteRegistry,
{
name: 'custom',
rangeType: 'number',
colorStops: [
{ color: 'green', stop: 0 },
{ color: 'blue', stop: 0.4 },
{ color: 'red', stop: 1 },
],
},
{ dataBounds: { min: 0, max: 1 } }
)
).toEqual([
{ color: 'green', stop: 0.4 },
{ color: 'blue', stop: 1 },
{ color: 'red', stop: 2 },
]);
});
it('should compute a display stop palette from custom colorStops defined by the user - stretch the stops to 100% percent', () => {
expect(
getPaletteStops(
paletteRegistry,
{
name: 'custom',
colorStops: [
{ color: 'green', stop: 0 },
{ color: 'blue', stop: 0.4 },
{ color: 'red', stop: 1 },
],
},
{ dataBounds: { min: 0, max: 1 } }
)
).toEqual([
{ color: 'green', stop: 0.4 },
{ color: 'blue', stop: 1 },
{ color: 'red', stop: 100 }, // default rangeType is percent, hence stretch to 100%
]);
});
});
describe('reversePalette', () => {
it('should correctly reverse color and stops', () => {
expect(
reversePalette([
{ color: 'red', stop: 0 },
{ color: 'green', stop: 0.5 },
{ color: 'blue', stop: 0.9 },
])
).toEqual([
{ color: 'blue', stop: 0 },
{ color: 'green', stop: 0.5 },
{ color: 'red', stop: 0.9 },
]);
});
});
describe('getStepValue', () => {
it('should compute the next step based on the last 2 stops', () => {
expect(
getStepValue(
// first arg is taken as max reference
[
{ color: 'red', stop: 0 },
{ color: 'red', stop: 50 },
],
[
{ color: 'red', stop: 0 },
{ color: 'red', stop: 50 },
],
100
)
).toBe(50);
expect(
getStepValue(
// first arg is taken as max reference
[
{ color: 'red', stop: 0 },
{ color: 'red', stop: 80 },
],
[
{ color: 'red', stop: 0 },
{ color: 'red', stop: 50 },
],
90
)
).toBe(10); // 90 - 80
expect(
getStepValue(
// first arg is taken as max reference
[
{ color: 'red', stop: 0 },
{ color: 'red', stop: 100 },
],
[
{ color: 'red', stop: 0 },
{ color: 'red', stop: 50 },
],
100
)
).toBe(1);
});
});

View file

@ -0,0 +1,191 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
PaletteContinuity,
PaletteRegistry,
CustomPaletteParams,
DataBounds,
ColorStop,
} from '../palettes/types';
import {
DEFAULT_COLOR_STEPS,
DEFAULT_PALETTE_NAME,
DEFAULT_MAX_STOP,
DEFAULT_MIN_STOP,
} from './constants';
/** @internal **/
export function calculateStop(
stopValue: number,
newMin: number,
oldMin: number,
oldInterval: number,
newInterval: number
) {
if (oldInterval === 0) {
return newInterval + newMin;
}
return roundValue(newMin + ((stopValue - oldMin) * newInterval) / oldInterval);
}
/**
* This is a generic function to compute stops from the current parameters.
*/
export function getPaletteStops(
palettes: PaletteRegistry,
activePaletteParams: CustomPaletteParams,
// used to customize color resolution
{
prevPalette,
dataBounds,
mapFromMinValue,
defaultPaletteName,
}: {
prevPalette?: string;
dataBounds: DataBounds;
mapFromMinValue?: boolean;
defaultPaletteName?: string;
}
) {
const { min: minValue, max: maxValue } = getOverallMinMax(activePaletteParams, dataBounds);
const interval = maxValue - minValue;
const { stops: currentStops, ...otherParams } = activePaletteParams || {};
if (activePaletteParams.name === 'custom' && activePaletteParams?.colorStops) {
// need to generate the palette from the existing controlStops
return shiftPalette(activePaletteParams.colorStops, maxValue);
}
const steps = activePaletteParams?.steps || DEFAULT_COLOR_STEPS;
// generate a palette from predefined ones and customize the domain
const colorStopsFromPredefined = palettes
.get(prevPalette || activePaletteParams?.name || defaultPaletteName || DEFAULT_PALETTE_NAME)
.getCategoricalColors(steps, otherParams);
const newStopsMin = mapFromMinValue || interval === 0 ? minValue : interval / steps;
return remapStopsByNewInterval(
colorStopsFromPredefined.map((color, index) => ({ color, stop: index })),
{
newInterval: interval,
oldInterval: colorStopsFromPredefined.length,
newMin: newStopsMin,
oldMin: 0,
}
);
}
// Utility to remap color stops within new domain
export function remapStopsByNewInterval(
controlStops: ColorStop[],
{
newInterval,
oldInterval,
newMin,
oldMin,
}: { newInterval: number; oldInterval: number; newMin: number; oldMin: number }
) {
return (controlStops || []).map(({ color, stop }) => {
return {
color,
stop: calculateStop(stop, newMin, oldMin, oldInterval, newInterval),
};
});
}
// Need to shift the Custom palette in order to correctly visualize it when in display mode
export function shiftPalette(stops: ColorStop[], max: number) {
// shift everything right and add an additional stop at the end
const result = stops.map((entry, i, array) => ({
...entry,
stop: i + 1 < array.length ? array[i + 1].stop : max,
}));
if (stops[stops.length - 1].stop === max) {
// extends the range by a fair amount to make it work the extra case for the last stop === max
const computedStep = getStepValue(stops, result, max) || 1;
// do not go beyond the unit step in this case
const step = Math.min(1, computedStep);
result[stops.length - 1].stop = max + step;
}
return result;
}
function getOverallMinMax(params: CustomPaletteParams | undefined, dataBounds: DataBounds) {
const { min: dataMin, max: dataMax } = getDataMinMax(params?.rangeType, dataBounds);
const minStopValue = params?.colorStops?.[0]?.stop ?? Number.POSITIVE_INFINITY;
const maxStopValue =
params?.colorStops?.[params.colorStops.length - 1]?.stop ?? Number.NEGATIVE_INFINITY;
const overallMin = Math.min(dataMin, minStopValue);
const overallMax = Math.max(dataMax, maxStopValue);
return { min: overallMin, max: overallMax };
}
export function roundValue(value: number, fractionDigits: number = 2) {
return Number((Math.floor(value * 100) / 100).toFixed(fractionDigits));
}
// very simple heuristic: pick last two stops and compute a new stop based on the same distance
// if the new stop is above max, then reduce the step to reach max, or if zero then just 1.
//
// it accepts two series of stops as the function is used also when computing stops from colorStops
export function getStepValue(colorStops: ColorStop[], newColorStops: ColorStop[], max: number) {
const length = newColorStops.length;
// workout the steps from the last 2 items
const dataStep =
length > 1 ? newColorStops[length - 1].stop - newColorStops[length - 2].stop || 1 : 1;
let step = Number(dataStep.toFixed(2));
if (max < colorStops[length - 1].stop + step) {
const diffToMax = max - colorStops[length - 1].stop;
// if the computed step goes way out of bound, fallback to 1, otherwise reach max
step = diffToMax > 0 ? diffToMax : 1;
}
return step;
}
export function getDataMinMax(
rangeType: CustomPaletteParams['rangeType'] | undefined,
dataBounds: DataBounds
) {
const dataMin = rangeType === 'number' ? dataBounds.min : DEFAULT_MIN_STOP;
const dataMax = rangeType === 'number' ? dataBounds.max : DEFAULT_MAX_STOP;
return { min: dataMin, max: dataMax };
}
export const checkIsMinContinuity = (continuity: PaletteContinuity | undefined) =>
Boolean(continuity && ['below', 'all'].includes(continuity));
export const checkIsMaxContinuity = (continuity: PaletteContinuity | undefined) =>
Boolean(continuity && ['above', 'all'].includes(continuity));
export const getFallbackDataBounds = (
rangeType: CustomPaletteParams['rangeType'] = 'percent'
): DataBounds =>
rangeType === 'percent'
? {
min: 0,
max: 100,
fallback: true,
}
: {
min: 1,
max: 1,
fallback: true,
};
export function reversePalette(paletteColorRepresentation: ColorStop[] = []) {
const stops = paletteColorRepresentation.map(({ stop }) => stop);
return paletteColorRepresentation
.map(({ color }, i) => ({
color,
stop: stops[paletteColorRepresentation.length - i - 1],
}))
.reverse();
}

View file

@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { FC } from 'react';
import { EuiForm } from '@elastic/eui';
import { ComponentStory } from '@storybook/react';
import { CustomizablePalette, CustomizablePaletteProps } from '../palette_configuration';
import { getPaletteRegistry } from './palettes';
export default {
title: 'CustomizablePalette',
component: CustomizablePalette,
decorators: [(story: Function) => <EuiForm>{story()}</EuiForm>],
};
const Template: ComponentStory<FC<CustomizablePaletteProps>> = (args) => (
<CustomizablePalette {...args} />
);
export const Default = Template.bind({});
Default.args = {
palettes: getPaletteRegistry(),
activePalette: {
type: 'palette',
name: 'default',
params: {
steps: 1,
maxSteps: 10,
continuity: 'none',
},
},
dataBounds: {
min: 0,
max: 100,
},
showExtraActions: true,
showRangeTypeSelector: true,
disableSwitchingContinuity: false,
setPalette: () => {},
};

View file

@ -0,0 +1,95 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { PaletteDefinition, SeriesLayer } from '../../../palettes/types';
const getMockedPalette = (
id: string,
title: string,
colors: string[],
canDynamicColoring: boolean = true
): PaletteDefinition => {
let counter = 0;
return {
id,
title,
getCategoricalColor: (_: SeriesLayer[]) => {
counter++;
if (counter > colors.length - 1) counter = 0;
return colors[counter];
},
canDynamicColoring,
internal: true,
getCategoricalColors: (num: number) => colors,
toExpression: () => ({
type: 'expression',
chain: [
{
type: 'function',
function: 'system_palette',
arguments: {
name: ['default'],
},
},
],
}),
};
};
export const getPaletteRegistry = () => {
const getMockedPalettes = (): Record<string, PaletteDefinition> => ({
default: getMockedPalette('default', 'Default Palette', [
'#54B399',
'#6092C0',
'#D36086',
'#9170B8',
'#CA8EAE',
'#D6BF57',
'#B9A888',
'#DA8B45',
'#AA6556',
'#E7664C',
]),
positive: getMockedPalette('positive', 'Grey Palette', [
'#222',
'#333',
'#444',
'#555',
'#666',
'#777',
'#888',
'#999',
'#AAA',
'#BBB',
]),
foo: getMockedPalette('foo', 'Foo Palette', [
'#7E7',
'#7D7',
'#7A7',
'#797',
'#787',
'#777',
'#767',
'#757',
'#747',
'#737',
]),
custom: getMockedPalette('custom', 'Custom Palette', [], false),
});
return {
get: (name: string) => getMockedPalettes()[name],
getAll: () => Object.values(getMockedPalettes()),
};
};
export const palettes = {
getPalettes: async () => {
return getPaletteRegistry();
},
};

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { mountWithIntl } from '@kbn/test-jest-helpers';
@ -10,8 +11,8 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import { ColorRanges, ColorRangesProps } from './color_ranges';
import { ReactWrapper } from 'enzyme';
import { PaletteRegistry } from 'src/plugins/charts/public';
import { ColorRangesContext } from './color_ranges_context';
import type { PaletteRegistry } from '../../../palettes';
const extraActionSelectors = {
addColor: '[data-test-subj^="lnsPalettePanel_dynamicColoring_addColor"]',

View file

@ -1,12 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useState, useEffect, Dispatch } from 'react';
import { camelCase } from 'lodash';
import { EuiFlexGroup, EuiTextColor, EuiFlexItem } from '@elastic/eui';
import { CustomPaletteParams, DEFAULT_CONTINUITY, DEFAULT_RANGE_TYPE } from '../../../palettes';
import { ColorRangesExtraActions } from './color_ranges_extra_actions';
import { ColorRangeItem } from './color_ranges_item';
@ -16,23 +19,20 @@ import {
ColorRangeValidation,
} from './color_ranges_validation';
import type { CustomPaletteParamsConfig } from '../../../../common';
import type { ColorRange } from './types';
import type { PaletteConfigurationActions } from '../types';
import { defaultPaletteParams } from '../constants';
export interface ColorRangesProps {
colorRanges: ColorRange[];
paletteConfiguration: CustomPaletteParamsConfig | undefined;
showExtraActions?: boolean;
paletteConfiguration: CustomPaletteParams | undefined;
showExtraActions: boolean;
dispatch: Dispatch<PaletteConfigurationActions>;
}
export function ColorRanges({
colorRanges,
paletteConfiguration,
showExtraActions = true,
showExtraActions,
dispatch,
}: ColorRangesProps) {
const [colorRangesValidity, setColorRangesValidity] = useState<
@ -41,8 +41,8 @@ export function ColorRanges({
const lastColorRange = colorRanges[colorRanges.length - 1];
const errors = getErrorMessages(colorRangesValidity);
const continuity = paletteConfiguration?.continuity ?? defaultPaletteParams.continuity;
const rangeType = paletteConfiguration?.rangeType ?? defaultPaletteParams.rangeType;
const continuity = paletteConfiguration?.continuity ?? DEFAULT_CONTINUITY;
const rangeType = paletteConfiguration?.rangeType ?? DEFAULT_RANGE_TYPE;
useEffect(() => {
setColorRangesValidity(validateColorRanges(colorRanges));

View file

@ -1,13 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import type { PaletteRegistry } from 'src/plugins/charts/public';
import type { DataBounds } from '../types';
import type { PaletteRegistry, DataBounds } from '../../../palettes';
interface ColorRangesContextType {
dataBounds: DataBounds;

View file

@ -1,16 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useCallback, Dispatch, useContext } from 'react';
import { EuiFlexGroup, EuiButtonEmpty, EuiFlexItem } from '@elastic/eui';
import { DistributeEquallyIcon } from '../../../assets/distribute_equally';
import { TooltipWrapper } from '../../index';
import { DistributeEquallyIcon } from '../assets/distribute_equally';
import { TooltipWrapper } from '../tooltip_wrapper';
import type { ColorRangesActions } from './types';
import { ColorRangesContext } from './color_ranges_context';
@ -44,19 +46,16 @@ export function ColorRangesExtraActions({
dispatch({ type: 'distributeEqually', payload: { dataBounds, palettes } });
}, [dataBounds, dispatch, palettes]);
const oneColorRangeWarn = i18n.translate(
'xpack.lens.dynamicColoring.customPalette.oneColorRange',
{
defaultMessage: `Requires more than one color`,
}
);
const oneColorRangeWarn = i18n.translate('coloring.dynamicColoring.customPalette.oneColorRange', {
defaultMessage: `Requires more than one color`,
});
return (
<EuiFlexGroup justifyContent="flexStart" gutterSize="none" wrap={true}>
<EuiFlexItem grow={false}>
<TooltipWrapper
tooltipContent={i18n.translate(
'xpack.lens.dynamicColoring.customPalette.maximumStepsApplied',
'coloring.dynamicColoring.customPalette.maximumStepsApplied',
{
defaultMessage: `You've applied the maximum number of steps`,
}
@ -69,19 +68,16 @@ export function ColorRangesExtraActions({
data-test-subj={`lnsPalettePanel_dynamicColoring_addColor`}
iconType="plusInCircle"
color="primary"
aria-label={i18n.translate(
'xpack.lens.dynamicColoring.customPalette.addColorAriaLabel',
{
defaultMessage: 'Add color',
}
)}
aria-label={i18n.translate('coloring.dynamicColoring.customPalette.addColorAriaLabel', {
defaultMessage: 'Add color',
})}
size="xs"
flush="left"
disabled={shouldDisableAdd}
onClick={onAddColorRange}
>
<FormattedMessage
id="xpack.lens.dynamicColoring.customPalette.addColor"
id="coloring.dynamicColoring.customPalette.addColor"
defaultMessage="Add color"
/>
</EuiButtonEmpty>
@ -98,7 +94,7 @@ export function ColorRangesExtraActions({
data-test-subj={`lnsPalettePanel_dynamicColoring_reverseColors`}
iconType="sortable"
color="primary"
aria-label={i18n.translate('xpack.lens.dynamicColoring.customPaletteAriaLabel', {
aria-label={i18n.translate('coloring.dynamicColoring.customPaletteAriaLabel', {
defaultMessage: 'Reverse colors',
})}
size="xs"
@ -107,7 +103,7 @@ export function ColorRangesExtraActions({
disabled={shouldDisableReverse}
>
<FormattedMessage
id="xpack.lens.dynamicColoring.customPalette.reverseColors"
id="coloring.dynamicColoring.customPalette.reverseColors"
defaultMessage="Reverse colors"
/>
</EuiButtonEmpty>
@ -125,7 +121,7 @@ export function ColorRangesExtraActions({
iconType={DistributeEquallyIcon}
color="primary"
aria-label={i18n.translate(
'xpack.lens.dynamicColoring.customPalette.distributeValuesAriaLabel',
'coloring.dynamicColoring.customPalette.distributeValuesAriaLabel',
{
defaultMessage: 'Distribute values',
}
@ -136,7 +132,7 @@ export function ColorRangesExtraActions({
onClick={onDistributeValues}
>
<FormattedMessage
id="xpack.lens.dynamicColoring.customPalette.distributeValues"
id="coloring.dynamicColoring.customPalette.distributeValues"
defaultMessage="Distribute values"
/>
</EuiButtonEmpty>

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
@ -20,7 +21,14 @@ import {
EuiFieldNumberProps,
} from '@elastic/eui';
import { RelatedIcon } from '../../../assets/related';
import {
PaletteContinuity,
checkIsMaxContinuity,
checkIsMinContinuity,
CustomPaletteParams,
} from '../../../palettes';
import { RelatedIcon } from '../assets/related';
import { isLastItem } from './utils';
import { isValidColor } from '../utils';
import {
@ -32,12 +40,6 @@ import {
import type { ColorRange, ColorRangeAccessor, ColorRangesActions } from './types';
import { ColorRangesContext } from './color_ranges_context';
import type { ColorRangeValidation } from './color_ranges_validation';
import type { CustomPaletteParams } from '../../../../common';
import {
PaletteContinuity,
checkIsMaxContinuity,
checkIsMinContinuity,
} from '../../../../../../../src/plugins/charts/common';
export interface ColorRangesItemProps {
colorRange: ColorRange;
@ -65,10 +67,10 @@ const getMode = (
const getPlaceholderForAutoMode = (isLast: boolean) =>
isLast
? i18n.translate('xpack.lens.dynamicColoring.customPalette.maxValuePlaceholder', {
? i18n.translate('coloring.dynamicColoring.customPalette.maxValuePlaceholder', {
defaultMessage: 'Max. value',
})
: i18n.translate('xpack.lens.dynamicColoring.customPalette.minValuePlaceholder', {
: i18n.translate('coloring.dynamicColoring.customPalette.minValuePlaceholder', {
defaultMessage: 'Min. value',
});
@ -154,14 +156,14 @@ export function ColorRangeItem({
}, [localValue, colorRange, accessor]);
const selectNewColorText = i18n.translate(
'xpack.lens.dynamicColoring.customPalette.selectNewColor',
'coloring.dynamicColoring.customPalette.selectNewColor',
{
defaultMessage: 'Select a new color',
}
);
return (
<EuiFlexGroup alignItems="center" gutterSize="s" wrap={false}>
<EuiFlexGroup alignItems="center" gutterSize="s" wrap={false} responsive={false}>
<EuiFlexItem grow={false}>
{!isLast ? (
<EuiColorPicker
@ -206,7 +208,7 @@ export function ColorRangeItem({
onBlur={onLeaveFocus}
data-test-subj={`lnsPalettePanel_dynamicColoring_range_value_${index}`}
prepend={<span className="euiFormLabel">{isLast ? '\u2264' : '\u2265'}</span>}
aria-label={i18n.translate('xpack.lens.dynamicColoring.customPalette.rangeAriaLabel', {
aria-label={i18n.translate('coloring.dynamicColoring.customPalette.rangeAriaLabel', {
defaultMessage: 'Range {index}',
values: {
index: index + 1,

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { Dispatch, useCallback, useContext } from 'react';
@ -10,15 +11,15 @@ import { i18n } from '@kbn/i18n';
import { EuiButtonIcon } from '@elastic/eui';
import { ValueMaxIcon } from '../../../assets/value_max';
import { ValueMinIcon } from '../../../assets/value_min';
import type { PaletteContinuity, CustomPaletteParams } from '../../../palettes';
import { ValueMaxIcon } from '../assets/value_max';
import { ValueMinIcon } from '../assets/value_min';
import { isLastItem } from './utils';
import { TooltipWrapper } from '../../index';
import { TooltipWrapper } from '../tooltip_wrapper';
import type { ColorRangesActions, ColorRange, ColorRangeAccessor } from './types';
import { ColorRangesContext } from './color_ranges_context';
import type { CustomPaletteParams } from '../../../../common';
import type { PaletteContinuity } from '../../../../../../../src/plugins/charts/common';
export interface ColorRangesItemButtonProps {
index: number;
@ -48,7 +49,7 @@ export function ColorRangeDeleteButton({ index, dispatch }: ColorRangesItemButto
dispatch({ type: 'deleteColorRange', payload: { index, dataBounds, palettes } });
}, [dispatch, index, dataBounds, palettes]);
const title = i18n.translate('xpack.lens.dynamicColoring.customPalette.deleteButtonAriaLabel', {
const title = i18n.translate('coloring.dynamicColoring.customPalette.deleteButtonAriaLabel', {
defaultMessage: 'Delete',
});
@ -83,16 +84,16 @@ export function ColorRangeEditButton({
}, [isLast, dispatch, continuity, dataBounds, palettes]);
let tooltipContent = isLast
? i18n.translate('xpack.lens.dynamicColoring.customPalette.setCustomMinValue', {
? i18n.translate('coloring.dynamicColoring.customPalette.setCustomMinValue', {
defaultMessage: `Set custom maximum value`,
})
: i18n.translate('xpack.lens.dynamicColoring.customPalette.setCustomMaxValue', {
: i18n.translate('coloring.dynamicColoring.customPalette.setCustomMaxValue', {
defaultMessage: `Set custom minimum value`,
});
if (disableSwitchingContinuity) {
tooltipContent = i18n.translate(
'xpack.lens.dynamicColoring.customPalette.disallowedEditMinMaxValues',
'coloring.dynamicColoring.customPalette.disallowedEditMinMaxValues',
{
defaultMessage: `You cannot set custom value for current configuration`,
}
@ -130,10 +131,10 @@ export function ColorRangeAutoDetectButton({
}, [continuity, dataBounds, dispatch, isLast, palettes]);
const tooltipContent = isLast
? i18n.translate('xpack.lens.dynamicColoring.customPalette.useAutoMaxValue', {
? i18n.translate('coloring.dynamicColoring.customPalette.useAutoMaxValue', {
defaultMessage: `Use maximum data value`,
})
: i18n.translate('xpack.lens.dynamicColoring.customPalette.useAutoMinValue', {
: i18n.translate('coloring.dynamicColoring.customPalette.useAutoMinValue', {
defaultMessage: `Use minimum data value`,
});

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { validateColorRanges, isAllColorRangesValid } from './color_ranges_validation';

View file

@ -1,9 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { isValidColor } from '../utils';
@ -20,8 +22,8 @@ export interface ColorRangeValidation {
/** @internal **/
export const getErrorMessages = (colorRangesValidity: Record<string, ColorRangeValidation>) => {
return [
...new Set(
return Array.from(
new Set(
Object.values(colorRangesValidity)
.map((item) => item.errors)
.flat()
@ -29,22 +31,19 @@ export const getErrorMessages = (colorRangesValidity: Record<string, ColorRangeV
switch (item) {
case 'invalidColor':
case 'invalidValue':
return i18n.translate(
'xpack.lens.dynamicColoring.customPalette.invalidValueOrColor',
{
defaultMessage: 'At least one color range contains the wrong value or color',
}
);
return i18n.translate('coloring.dynamicColoring.customPalette.invalidValueOrColor', {
defaultMessage: 'At least one color range contains the wrong value or color',
});
case 'greaterThanMaxValue':
return i18n.translate('xpack.lens.dynamicColoring.customPalette.invalidMaxValue', {
return i18n.translate('coloring.dynamicColoring.customPalette.invalidMaxValue', {
defaultMessage: 'Maximum value must be greater than preceding values',
});
default:
return '';
}
})
),
];
)
);
};
/** @internal **/

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { ColorRanges } from './color_ranges';

View file

@ -1,13 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PaletteRegistry } from 'src/plugins/charts/public';
import type { CustomPaletteParams } from '../../../../common';
import type { PaletteContinuity } from '../../../../../../../src/plugins/charts/common';
import type { DataBounds } from '../types';
import type {
PaletteContinuity,
PaletteRegistry,
DataBounds,
CustomPaletteParams,
} from '../../../palettes';
export interface ColorRange {
color: string;

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {

View file

@ -1,14 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getDataMinMax, roundValue } from '../../utils';
import { DataBounds, roundValue, getDataMinMax, CustomPaletteParams } from '../../../../palettes';
import type { ColorRange, ColorRangeAccessor } from '../types';
import type { DataBounds } from '../../types';
import type { CustomPaletteParamsConfig } from '../../../../../common';
/**
* Allows to update a ColorRange
@ -30,7 +29,7 @@ const updateColorRangeItem = (
*/
export const addColorRange = (
colorRanges: ColorRange[],
rangeType: CustomPaletteParamsConfig['rangeType'],
rangeType: CustomPaletteParams['rangeType'],
dataBounds: DataBounds
) => {
let newColorRanges = [...colorRanges];

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { distributeEqually, reversePalette } from './color_ranges_extra_actions';

View file

@ -1,20 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getDataMinMax, roundValue } from '../../utils';
import {
checkIsMaxContinuity,
checkIsMinContinuity,
PaletteContinuity,
DataBounds,
roundValue,
getDataMinMax,
CustomPaletteParams,
} from '../../../../palettes';
import type { ColorRange } from '../types';
import type { DataBounds } from '../../types';
import type { CustomPaletteParamsConfig } from '../../../../../common';
import {
PaletteContinuity,
checkIsMinContinuity,
checkIsMaxContinuity,
} from '../../../../../../../../src/plugins/charts/common';
/**
* Distribute equally
@ -22,7 +24,7 @@ import {
*/
export const distributeEqually = (
colorRanges: ColorRange[],
rangeType: CustomPaletteParamsConfig['rangeType'],
rangeType: CustomPaletteParams['rangeType'],
continuity: PaletteContinuity,
dataBounds: DataBounds
) => {

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './utils';

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { sortColorRanges, calculateMaxStep, toColorStops, getValueForContinuity } from './utils';

View file

@ -1,18 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { roundValue, getDataMinMax } from '../../utils';
import {
PaletteContinuity,
checkIsMaxContinuity,
checkIsMinContinuity,
} from '../../../../../../../../src/plugins/charts/common';
import type { CustomPaletteParams } from '../../../../../common';
DataBounds,
roundValue,
getDataMinMax,
CustomPaletteParams,
} from '../../../../palettes';
import type { ColorRange, ColorRangeAccessor } from '../types';
import type { DataBounds } from '../../types';
/**
* Check if item is last

View file

@ -6,10 +6,8 @@
* Side Public License, v 1.
*/
import { PaletteContinuity } from '../../types';
import { CustomizablePalette } from './palette_configuration';
export const checkIsMinContinuity = (continuity: PaletteContinuity | undefined) =>
Boolean(continuity && ['below', 'all'].includes(continuity));
export const checkIsMaxContinuity = (continuity: PaletteContinuity | undefined) =>
Boolean(continuity && ['above', 'all'].includes(continuity));
// React.lazy support
// eslint-disable-next-line import/no-default-export
export default CustomizablePalette;

View file

@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { PaletteDefinition, SeriesLayer } from '../../../palettes/types';
export const getPaletteRegistry = () => {
const mockPalette1: jest.Mocked<PaletteDefinition> = {
id: 'default',
title: 'My Palette',
getCategoricalColor: jest.fn((_: SeriesLayer[]) => 'black'),
getCategoricalColors: jest.fn((num: number) => ['red', 'black']),
toExpression: jest.fn(() => ({
type: 'expression',
chain: [
{
type: 'function',
function: 'system_palette',
arguments: {
name: ['default'],
},
},
],
})),
};
const mockPalette2: jest.Mocked<PaletteDefinition> = {
id: 'mocked',
title: 'Mocked Palette',
getCategoricalColor: jest.fn((_: SeriesLayer[]) => 'blue'),
getCategoricalColors: jest.fn((num: number) => ['blue', 'yellow']),
toExpression: jest.fn(() => ({
type: 'expression',
chain: [
{
type: 'function',
function: 'system_palette',
arguments: {
name: ['mocked'],
},
},
],
})),
};
const mockPalette3: jest.Mocked<PaletteDefinition> = {
id: 'custom',
title: 'Custom Mocked Palette',
getCategoricalColor: jest.fn((_: SeriesLayer[]) => 'blue'),
getCategoricalColors: jest.fn((num: number) => ['blue', 'yellow']),
getColorForValue: jest.fn(
(num: number | undefined, state: unknown, minMax: { min: number; max: number }) =>
num == null || num < 1 ? undefined : 'blue'
),
canDynamicColoring: true,
toExpression: jest.fn(() => ({
type: 'expression',
chain: [
{
type: 'function',
function: 'system_palette',
arguments: {
name: ['mocked'],
},
},
],
})),
};
return {
get: (name: string) =>
name === 'custom' ? mockPalette3 : name !== 'default' ? mockPalette2 : mockPalette1,
getAll: () => [mockPalette1, mockPalette2, mockPalette3],
};
};

View file

@ -1,20 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { EuiButtonGroup, EuiColorPalettePickerPaletteProps } from '@elastic/eui';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { chartPluginMock } from 'src/plugins/charts/public/mocks';
import type { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
import { ReactWrapper } from 'enzyme';
import type { CustomPaletteParams } from '../../../common';
import type {
PaletteOutput,
PaletteRegistry,
DataBounds,
CustomPaletteParams,
} from '../../palettes';
import { CustomizablePalette } from './palette_configuration';
import { getPaletteRegistry } from './mocks/palettes_registry';
import { act } from 'react-dom/test-utils';
import type { DataBounds } from './types';
// mocking random id generator function
jest.mock('@elastic/eui', () => {
@ -40,7 +44,7 @@ jest.mock('./color_ranges/color_ranges_validation', () => {
});
describe('palette panel', () => {
const paletteRegistry = chartPluginMock.createPaletteRegistry();
const paletteRegistry = getPaletteRegistry();
let props: {
palettes: PaletteRegistry;
activePalette: PaletteOutput<CustomPaletteParams>;

View file

@ -1,41 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import React, { useReducer, useMemo } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import type { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
import { EuiFormRow, htmlIdGenerator, EuiButtonGroup, EuiIconTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { PalettePicker } from './palette_picker';
import type { DataBounds } from './types';
import {
PaletteOutput,
PaletteRegistry,
DataBounds,
getFallbackDataBounds,
CustomPaletteParams,
RequiredPaletteParamTypes,
} from '../../palettes';
import './palette_configuration.scss';
import type { CustomPaletteParams, RequiredPaletteParamTypes } from '../../../common';
import { toColorRanges, getFallbackDataBounds } from './utils';
import { toColorRanges } from './utils';
import { ColorRanges, ColorRangesContext } from './color_ranges';
import { isAllColorRangesValid } from './color_ranges/color_ranges_validation';
import { paletteConfigurationReducer } from './palette_configuration_reducer';
export function CustomizablePalette({
palettes,
activePalette,
setPalette,
dataBounds = getFallbackDataBounds(activePalette.params?.rangeType),
showRangeTypeSelector = true,
disableSwitchingContinuity = false,
}: {
export interface CustomizablePaletteProps {
palettes: PaletteRegistry;
activePalette: PaletteOutput<CustomPaletteParams>;
setPalette: (palette: PaletteOutput<CustomPaletteParams>) => void;
dataBounds?: DataBounds;
showRangeTypeSelector?: boolean;
disableSwitchingContinuity?: boolean;
}) {
showExtraActions?: boolean;
}
export const CustomizablePalette = ({
palettes,
activePalette,
setPalette,
dataBounds = getFallbackDataBounds(activePalette.params?.rangeType),
showExtraActions = true,
showRangeTypeSelector = true,
disableSwitchingContinuity = false,
}: CustomizablePaletteProps) => {
const idPrefix = useMemo(() => htmlIdGenerator()(), []);
const colorRangesToShow = toColorRanges(
palettes,
@ -67,7 +76,7 @@ export function CustomizablePalette({
<div className="lnsPalettePanel__section lnsPalettePanel__section--shaded">
<EuiFormRow
display="rowCompressed"
label={i18n.translate('xpack.lens.palettePicker.label', {
label={i18n.translate('coloring.dynamicColoring.palettePicker.label', {
defaultMessage: 'Color palette',
})}
fullWidth
@ -93,12 +102,12 @@ export function CustomizablePalette({
<EuiFormRow
label={
<>
{i18n.translate('xpack.lens.table.dynamicColoring.rangeType.label', {
{i18n.translate('coloring.dynamicColoring.rangeType.label', {
defaultMessage: 'Value type',
})}{' '}
<EuiIconTip
content={i18n.translate(
'xpack.lens.table.dynamicColoring.customPalette.colorStopsHelpPercentage',
'coloring.dynamicColoring.customPalette.colorStopsHelpPercentage',
{
defaultMessage:
'Percent value types are relative to the full range of available data values.',
@ -112,7 +121,7 @@ export function CustomizablePalette({
display="rowCompressed"
>
<EuiButtonGroup
legend={i18n.translate('xpack.lens.table.dynamicColoring.rangeType.label', {
legend={i18n.translate('coloring.dynamicColoring.rangeType.label', {
defaultMessage: 'Value type',
})}
data-test-subj="lnsPalettePanel_dynamicColoring_custom_range_groups"
@ -121,14 +130,14 @@ export function CustomizablePalette({
options={[
{
id: `${idPrefix}percent`,
label: i18n.translate('xpack.lens.table.dynamicColoring.rangeType.percent', {
label: i18n.translate('coloring.dynamicColoring.rangeType.percent', {
defaultMessage: 'Percent',
}),
'data-test-subj': 'lnsPalettePanel_dynamicColoring_rangeType_groups_percent',
},
{
id: `${idPrefix}number`,
label: i18n.translate('xpack.lens.table.dynamicColoring.rangeType.number', {
label: i18n.translate('coloring.dynamicColoring.rangeType.number', {
defaultMessage: 'Number',
}),
'data-test-subj': 'lnsPalettePanel_dynamicColoring_rangeType_groups_number',
@ -155,7 +164,7 @@ export function CustomizablePalette({
</EuiFormRow>
)}
<EuiFormRow
label={i18n.translate('xpack.lens.palettePicker.colorRangesLabel', {
label={i18n.translate('coloring.dynamicColoring.palettePicker.colorRangesLabel', {
defaultMessage: 'Color Ranges',
})}
display="rowCompressed"
@ -169,6 +178,7 @@ export function CustomizablePalette({
}}
>
<ColorRanges
showExtraActions={showExtraActions}
paletteConfiguration={localState.activePalette?.params}
colorRanges={localState.colorRanges}
dispatch={dispatch}
@ -177,4 +187,4 @@ export function CustomizablePalette({
</EuiFormRow>
</div>
);
}
};

View file

@ -1,11 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { Reducer } from 'react';
import { DEFAULT_CONTINUITY, DEFAULT_RANGE_TYPE } from '../../palettes';
import {
addColorRange,
deleteColorRange,
@ -16,7 +18,6 @@ import {
updateColorRangeValue,
getValueForContinuity,
} from './color_ranges/utils';
import { DEFAULT_CONTINUITY, DEFAULT_RANGE_TYPE } from './constants';
import {
mergePaletteParams,

View file

@ -1,20 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { EuiColorPalettePicker, EuiColorPalettePickerPaletteProps } from '@elastic/eui';
import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
import {
PaletteOutput,
PaletteRegistry,
CustomPaletteParams,
CUSTOM_PALETTE,
DEFAULT_COLOR_STEPS,
FIXED_PROGRESSION,
defaultPaletteParams,
} from './constants';
import type { CustomPaletteParams } from '../../../common';
DEFAULT_PALETTE_NAME,
} from '../../palettes';
function getCustomPaletteConfig(
palettes: PaletteRegistry,
@ -105,7 +107,7 @@ export function PalettePicker({
name: newPalette,
});
}}
valueOfSelected={activePalette?.name || defaultPaletteParams.name}
valueOfSelected={activePalette?.name || DEFAULT_PALETTE_NAME}
selectionDisplay="palette"
{...rest}
/>

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { EuiToolTip, EuiToolTipProps } from '@elastic/eui';
export type TooltipWrapperProps = Partial<Omit<EuiToolTipProps, 'content'>> & {
tooltipContent: string;
/** When the condition is truthy, the tooltip will be shown */
condition: boolean;
};
export const TooltipWrapper: React.FunctionComponent<TooltipWrapperProps> = ({
children,
condition,
tooltipContent,
...tooltipProps
}) => {
return (
<>
{condition ? (
<EuiToolTip content={tooltipContent} delay="long" {...tooltipProps}>
<>{children}</>
</EuiToolTip>
) : (
children
)}
</>
);
};

View file

@ -1,26 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
import type { CustomPaletteParams } from '../../../common';
import type {
PaletteOutput,
PaletteRegistry,
DataBounds,
CustomPaletteParams,
} from '../../palettes';
import type { ColorRange, ColorRangesActions } from './color_ranges';
/** @internal **/
export interface PaletteConfigurationState {
activePalette: PaletteOutput<CustomPaletteParams>;
colorRanges: ColorRange[];
}
/** @internal **/
export interface DataBounds {
min: number;
max: number;
fallback?: boolean;
}
/** @internal **/
export interface UpdateRangeTypePayload {
rangeType: CustomPaletteParams['rangeType'];
@ -36,6 +35,7 @@ export interface ChangeColorPalettePayload {
disableSwitchingContinuity: boolean;
}
/** @internal **/
export type PaletteConfigurationActions =
| ColorRangesActions
| { type: 'updateRangeType'; payload: UpdateRangeTypePayload }

View file

@ -0,0 +1,427 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
getColorStops,
isValidColor,
mergePaletteParams,
updateRangeType,
changeColorPalette,
} from './utils';
import { getPaletteRegistry } from './mocks/palettes_registry';
describe('getColorStops', () => {
const paletteRegistry = getPaletteRegistry();
it('should return the same colorStops if a custom palette is passed, avoiding recomputation', () => {
const colorStops = [
{ stop: 0, color: 'red' },
{ stop: 100, color: 'blue' },
];
expect(
getColorStops(
paletteRegistry,
colorStops,
{ name: 'custom', type: 'palette' },
{ min: 0, max: 100 }
)
).toBe(colorStops);
});
it('should get a fresh list of colors', () => {
expect(
getColorStops(
paletteRegistry,
[
{ stop: 0, color: 'red' },
{ stop: 100, color: 'blue' },
],
{ name: 'mocked', type: 'palette' },
{ min: 0, max: 100 }
)
).toEqual([
{ color: 'blue', stop: 0 },
{ color: 'yellow', stop: 50 },
]);
});
it('should get a fresh list of colors even if custom palette but empty colorStops', () => {
expect(
getColorStops(paletteRegistry, [], { name: 'mocked', type: 'palette' }, { min: 0, max: 100 })
).toEqual([
{ color: 'blue', stop: 0 },
{ color: 'yellow', stop: 50 },
]);
});
it('should correctly map the new colorStop to the current data bound and minValue', () => {
expect(
getColorStops(
paletteRegistry,
[],
{ name: 'mocked', type: 'palette', params: { rangeType: 'number' } },
{ min: 100, max: 1000 }
)
).toEqual([
{ color: 'blue', stop: 100 },
{ color: 'yellow', stop: 550 },
]);
});
it('should reverse the colors', () => {
expect(
getColorStops(
paletteRegistry,
[],
{ name: 'mocked', type: 'palette', params: { reverse: true } },
{ min: 100, max: 1000 }
)
).toEqual([
{ color: 'yellow', stop: 0 },
{ color: 'blue', stop: 50 },
]);
});
});
describe('mergePaletteParams', () => {
it('should return a full palette', () => {
expect(mergePaletteParams({ type: 'palette', name: 'myPalette' }, { reverse: true })).toEqual({
type: 'palette',
name: 'myPalette',
params: { reverse: true },
});
});
});
describe('isValidColor', () => {
it('should return ok for valid hex color notation', () => {
expect(isValidColor('#fff')).toBe(true);
expect(isValidColor('#ffffff')).toBe(true);
expect(isValidColor('#ffffffaa')).toBe(true);
});
it('should return false for non valid strings', () => {
expect(isValidColor('')).toBe(false);
expect(isValidColor('#')).toBe(false);
expect(isValidColor('#ff')).toBe(false);
expect(isValidColor('123')).toBe(false);
expect(isValidColor('rgb(1, 1, 1)')).toBe(false);
expect(isValidColor('rgba(1, 1, 1, 0)')).toBe(false);
expect(isValidColor('#ffffffgg')).toBe(false);
expect(isValidColor('#fff00')).toBe(false);
// this version of chroma does not support hex4 format
expect(isValidColor('#fffa')).toBe(false);
});
});
describe('updateRangeType', () => {
const paletteRegistry = getPaletteRegistry();
const colorRanges = [
{
start: 0,
end: 40,
color: 'green',
},
{
start: 40,
end: 80,
color: 'blue',
},
{
start: 80,
end: 100,
color: 'red',
},
];
it('should correctly update palette params with new range type if continuity is none', () => {
const newPaletteParams = updateRangeType(
'number',
{
type: 'palette',
name: 'custom',
params: {
continuity: 'none',
name: 'custom',
rangeType: 'percent',
rangeMax: 100,
rangeMin: 0,
colorStops: [
{ color: 'green', stop: 0 },
{ color: 'blue', stop: 40 },
{ color: 'red', stop: 80 },
],
},
},
{ min: 0, max: 200 },
paletteRegistry,
colorRanges
);
expect(newPaletteParams).toEqual({
rangeType: 'number',
rangeMin: 0,
rangeMax: 200,
colorStops: [
{
color: 'green',
stop: 0,
},
{
color: 'blue',
stop: 80,
},
{
color: 'red',
stop: 160,
},
],
stops: [
{
color: 'green',
stop: 80,
},
{
color: 'blue',
stop: 160,
},
{
color: 'red',
stop: 200,
},
],
});
});
it('should correctly update palette params with new range type if continuity is all', () => {
const newPaletteParams = updateRangeType(
'number',
{
type: 'palette',
name: 'custom',
params: {
continuity: 'all',
name: 'custom',
rangeType: 'percent',
rangeMax: 100,
rangeMin: 0,
colorStops: [
{ color: 'green', stop: 0 },
{ color: 'blue', stop: 40 },
{ color: 'red', stop: 80 },
],
},
},
{ min: 0, max: 200 },
paletteRegistry,
colorRanges
);
expect(newPaletteParams).toEqual({
rangeType: 'number',
rangeMin: Number.NEGATIVE_INFINITY,
rangeMax: Number.POSITIVE_INFINITY,
colorStops: [
{
color: 'green',
stop: 0,
},
{
color: 'blue',
stop: 80,
},
{
color: 'red',
stop: 160,
},
],
stops: [
{
color: 'green',
stop: 80,
},
{
color: 'blue',
stop: 160,
},
{
color: 'red',
stop: 200,
},
],
});
});
it('should correctly update palette params with new range type if continuity is below', () => {
const newPaletteParams = updateRangeType(
'number',
{
type: 'palette',
name: 'custom',
params: {
continuity: 'below',
name: 'custom',
rangeType: 'percent',
rangeMax: 100,
rangeMin: 0,
colorStops: [
{ color: 'green', stop: 0 },
{ color: 'blue', stop: 40 },
{ color: 'red', stop: 80 },
],
},
},
{ min: 0, max: 200 },
paletteRegistry,
colorRanges
);
expect(newPaletteParams).toEqual({
rangeType: 'number',
rangeMin: Number.NEGATIVE_INFINITY,
rangeMax: 200,
colorStops: [
{
color: 'green',
stop: 0,
},
{
color: 'blue',
stop: 80,
},
{
color: 'red',
stop: 160,
},
],
stops: [
{
color: 'green',
stop: 80,
},
{
color: 'blue',
stop: 160,
},
{
color: 'red',
stop: 200,
},
],
});
});
it('should correctly update palette params with new range type if continuity is above', () => {
const newPaletteParams = updateRangeType(
'number',
{
type: 'palette',
name: 'custom',
params: {
continuity: 'above',
name: 'custom',
rangeType: 'percent',
rangeMax: 100,
rangeMin: 0,
colorStops: [
{ color: 'green', stop: 0 },
{ color: 'blue', stop: 40 },
{ color: 'red', stop: 80 },
],
},
},
{ min: 0, max: 200 },
paletteRegistry,
colorRanges
);
expect(newPaletteParams).toEqual({
rangeType: 'number',
rangeMin: 0,
rangeMax: Number.POSITIVE_INFINITY,
colorStops: [
{
color: 'green',
stop: 0,
},
{
color: 'blue',
stop: 80,
},
{
color: 'red',
stop: 160,
},
],
stops: [
{
color: 'green',
stop: 80,
},
{
color: 'blue',
stop: 160,
},
{
color: 'red',
stop: 200,
},
],
});
});
});
describe('changeColorPalette', () => {
const paletteRegistry = getPaletteRegistry();
it('should correct update params for new palette', () => {
const newPaletteParams = changeColorPalette(
{
type: 'palette',
name: 'default',
},
{
type: 'palette',
name: 'custom',
params: {
continuity: 'above',
name: 'custom',
rangeType: 'percent',
rangeMax: 100,
rangeMin: 0,
colorStops: [
{ color: 'green', stop: 0 },
{ color: 'blue', stop: 40 },
{ color: 'red', stop: 80 },
],
},
},
paletteRegistry,
{ min: 0, max: 200 },
false
);
expect(newPaletteParams).toEqual({
name: 'default',
type: 'palette',
params: {
rangeType: 'percent',
name: 'default',
continuity: 'above',
rangeMin: 0,
rangeMax: Number.POSITIVE_INFINITY,
reverse: false,
colorStops: undefined,
stops: [
{
color: 'red',
stop: 0,
},
{
color: 'black',
stop: 50,
},
],
},
});
});
});

View file

@ -0,0 +1,371 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import chroma from 'chroma-js';
import {
DataBounds,
PaletteOutput,
PaletteRegistry,
checkIsMinContinuity,
reversePalette,
checkIsMaxContinuity,
calculateStop,
roundValue,
getPaletteStops,
getDataMinMax,
CustomPaletteParams,
CUSTOM_PALETTE,
DEFAULT_RANGE_TYPE,
DEFAULT_COLOR_STEPS,
DEFAULT_CONTINUITY,
} from '../../palettes';
import type { ColorRange } from './color_ranges';
import { toColorStops, sortColorRanges } from './color_ranges/utils';
import type { PaletteConfigurationState } from './types';
/**
* Some name conventions here:
* * `displayStops` => It's an additional transformation of `stops` into a [0, N] domain for the EUIPaletteDisplay component.
* * `stops` => final steps used to table coloring. It is a rightShift of the colorStops
* * `colorStops` => user's color stop inputs. Used to compute range min.
*
* When the user inputs the colorStops, they are designed to be the initial part of the color segment,
* so the next stops indicate where the previous stop ends.
* Both table coloring logic and EuiPaletteDisplay format implementation works differently than our current `colorStops`,
* by having the stop values at the end of each color segment rather than at the beginning: `stops` values are computed by a rightShift of `colorStops`.
* EuiPaletteDisplay has an additional requirement as it is always mapped against a domain [0, N]: from `stops` the `displayStops` are computed with
* some continuity enrichment and a remap against a [0, 100] domain to make the palette component work ok.
*
* These naming conventions would be useful to track the code flow in this feature as multiple transformations are happening
* for a single change.
*/
export function updateRangeType(
newRangeType: CustomPaletteParams['rangeType'],
activePalette: PaletteConfigurationState['activePalette'],
dataBounds: DataBounds,
palettes: PaletteRegistry,
colorRanges: PaletteConfigurationState['colorRanges']
) {
const continuity = activePalette.params?.continuity ?? DEFAULT_CONTINUITY;
const params: CustomPaletteParams = { rangeType: newRangeType };
const { min: newMin, max: newMax } = getDataMinMax(newRangeType, dataBounds);
const { min: oldMin, max: oldMax } = getDataMinMax(activePalette.params?.rangeType, dataBounds);
const newColorStops = getStopsFromColorRangesByNewInterval(colorRanges, {
oldInterval: oldMax - oldMin,
newInterval: newMax - newMin,
newMin,
oldMin,
});
if (activePalette.name === CUSTOM_PALETTE) {
const stops = getPaletteStops(
palettes,
{ ...activePalette.params, colorStops: newColorStops, ...params },
{ dataBounds }
);
params.colorStops = newColorStops;
params.stops = stops;
} else {
params.stops = getPaletteStops(
palettes,
{ ...activePalette.params, ...params },
{ prevPalette: activePalette.name, dataBounds }
);
}
const lastStop =
activePalette.name === CUSTOM_PALETTE
? newColorStops[newColorStops.length - 1].stop
: params.stops[params.stops.length - 1].stop;
params.rangeMin = checkIsMinContinuity(continuity)
? Number.NEGATIVE_INFINITY
: activePalette.name === CUSTOM_PALETTE
? newColorStops[0].stop
: params.stops[0].stop;
params.rangeMax = checkIsMaxContinuity(continuity)
? Number.POSITIVE_INFINITY
: activePalette.params?.rangeMax
? calculateStop(activePalette.params.rangeMax, newMin, oldMin, oldMax - oldMin, newMax - newMin)
: lastStop > newMax
? lastStop + 1
: newMax;
return params;
}
export function changeColorPalette(
newPalette: PaletteConfigurationState['activePalette'],
activePalette: PaletteConfigurationState['activePalette'],
palettes: PaletteRegistry,
dataBounds: DataBounds,
disableSwitchingContinuity: boolean
) {
const isNewPaletteCustom = newPalette.name === CUSTOM_PALETTE;
const newParams: CustomPaletteParams = {
...activePalette.params,
name: newPalette.name,
colorStops: undefined,
continuity: disableSwitchingContinuity
? activePalette.params?.continuity ?? DEFAULT_CONTINUITY
: DEFAULT_CONTINUITY,
reverse: false, // restore the reverse flag
};
// we should pass colorStops so that correct calculate new color stops (if there was before) for custom palette
const newColorStops = getColorStops(
palettes,
activePalette.params?.colorStops || [],
activePalette,
dataBounds
);
if (isNewPaletteCustom) {
newParams.colorStops = newColorStops;
}
return {
...newPalette,
params: {
...newParams,
stops: getPaletteStops(palettes, newParams, {
prevPalette:
isNewPaletteCustom || activePalette.name === CUSTOM_PALETTE ? undefined : newPalette.name,
dataBounds,
mapFromMinValue: true,
}),
rangeMin: checkIsMinContinuity(newParams.continuity)
? Number.NEGATIVE_INFINITY
: Math.min(dataBounds.min, newColorStops[0].stop),
rangeMax: checkIsMaxContinuity(newParams.continuity)
? Number.POSITIVE_INFINITY
: Math.min(dataBounds.max, newColorStops[newColorStops.length - 1].stop),
},
};
}
export function withUpdatingPalette(
palettes: PaletteRegistry,
activePalette: PaletteConfigurationState['activePalette'],
colorRanges: ColorRange[],
dataBounds: DataBounds,
continuity?: CustomPaletteParams['continuity']
) {
const currentContinuity = continuity ?? activePalette.params?.continuity ?? DEFAULT_CONTINUITY;
let sortedColorRanges = colorRanges;
if (
colorRanges.some((value, index) =>
index !== colorRanges.length - 1 ? value.start > colorRanges[index + 1].start : false
)
) {
sortedColorRanges = sortColorRanges(colorRanges);
}
const { max, colorStops } = toColorStops(sortedColorRanges, currentContinuity);
const newPallete = getSwitchToCustomParams(
palettes,
activePalette!,
{
continuity: currentContinuity,
colorStops,
steps: activePalette!.params?.steps || DEFAULT_COLOR_STEPS,
reverse: activePalette!.params?.reverse,
rangeMin: colorStops[0]?.stop,
rangeMax: max,
},
dataBounds!
);
return {
activePalette: newPallete,
colorRanges,
};
}
export function withUpdatingColorRanges(
palettes: PaletteRegistry,
activePalette: PaletteConfigurationState['activePalette'],
dataBounds: DataBounds
) {
return {
colorRanges: toColorRanges(
palettes,
activePalette.params?.colorStops || [],
activePalette,
dataBounds
),
activePalette,
};
}
// Utility to remap color stops within new domain
export function getStopsFromColorRangesByNewInterval(
colorRanges: ColorRange[],
{
newInterval,
oldInterval,
newMin,
oldMin,
}: { newInterval: number; oldInterval: number; newMin: number; oldMin: number }
) {
return (colorRanges || []).map(({ color, start }) => {
let stop = calculateStop(start, newMin, oldMin, oldInterval, newInterval);
if (oldInterval === 0) {
stop = newInterval + newMin;
}
return {
color,
stop: roundValue(stop),
};
});
}
export function mergePaletteParams(
activePalette: PaletteOutput<CustomPaletteParams>,
newParams: CustomPaletteParams
): PaletteOutput<CustomPaletteParams> {
return {
...activePalette,
params: {
...activePalette.params,
...newParams,
},
};
}
function isValidPonyfill(colorString: string) {
// we're using an old version of chroma without the valid function
try {
chroma(colorString);
return true;
} catch (e) {
return false;
}
}
export function isValidColor(colorString: string) {
// chroma can handle also hex values with alpha channel/transparency
// chroma accepts also hex without #, so test for it
return colorString !== '' && /^#/.test(colorString) && isValidPonyfill(colorString);
}
function getSwitchToCustomParams(
palettes: PaletteRegistry,
activePalette: PaletteOutput<CustomPaletteParams>,
newParams: CustomPaletteParams,
dataBounds: DataBounds
) {
// if it's already a custom palette just return the params
if (activePalette?.params?.name === CUSTOM_PALETTE) {
const stops = getPaletteStops(
palettes,
{
steps: DEFAULT_COLOR_STEPS,
...activePalette.params,
...newParams,
},
{
dataBounds,
}
);
return mergePaletteParams(activePalette, {
...newParams,
stops,
});
}
// prepare everything to switch to custom palette
const newPaletteParams = {
steps: DEFAULT_COLOR_STEPS,
...activePalette.params,
...newParams,
name: CUSTOM_PALETTE,
};
const stops = getPaletteStops(palettes, newPaletteParams, {
prevPalette: newPaletteParams.colorStops ? undefined : activePalette.name,
dataBounds,
});
return mergePaletteParams(
{ name: CUSTOM_PALETTE, type: 'palette' },
{
...newPaletteParams,
stops,
}
);
}
export function getColorStops(
palettes: PaletteRegistry,
colorStops: Required<CustomPaletteParams>['stops'],
activePalette: PaletteOutput<CustomPaletteParams>,
dataBounds: DataBounds
) {
// just forward the current stops if custom
if (activePalette?.name === CUSTOM_PALETTE && colorStops?.length) {
return colorStops;
}
// for predefined palettes create some stops, then drop the last one.
// we're using these as starting point for the user
let freshColorStops = getPaletteStops(
palettes,
{ ...activePalette?.params },
// mapFromMinValue is a special flag to offset the stops values
// used here to avoid a new remap/left shift
{ dataBounds, mapFromMinValue: true, defaultPaletteName: activePalette.name }
);
if (activePalette?.params?.reverse) {
freshColorStops = reversePalette(freshColorStops);
}
return freshColorStops;
}
/**
* Both table coloring logic and EuiPaletteDisplay format implementation works differently than our current `colorStops`,
* by having the stop values at the end of each color segment rather than at the beginning: `stops` values are computed by a rightShift of `colorStops`.
* EuiPaletteDisplay has an additional requirement as it is always mapped against a domain [0, N]: from `stops` the `displayStops` are computed with
* some continuity enrichment and a remap against a [0, 100] domain to make the palette component work ok.
*
* These naming conventions would be useful to track the code flow in this feature as multiple transformations are happening
* for a single change.
*/
export function toColorRanges(
palettes: PaletteRegistry,
colorStops: CustomPaletteParams['colorStops'],
activePalette: PaletteOutput<CustomPaletteParams>,
dataBounds: DataBounds
) {
const { continuity = DEFAULT_CONTINUITY, rangeType = DEFAULT_RANGE_TYPE } =
activePalette.params ?? {};
const { min: dataMin, max: dataMax } = getDataMinMax(rangeType, dataBounds);
return getColorStops(palettes, colorStops || [], activePalette, dataBounds).map(
(colorStop, index, array) => {
const isFirst = index === 0;
const isLast = index === array.length - 1;
return {
color: colorStop.color,
start:
isFirst && checkIsMinContinuity(continuity)
? Number.NEGATIVE_INFINITY
: colorStop.stop ?? activePalette.params?.rangeMin ?? dataMin,
end:
isLast && checkIsMaxContinuity(continuity)
? Number.POSITIVE_INFINITY
: array[index + 1]?.stop ?? activePalette.params?.rangeMax ?? dataMax,
};
}
);
}

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { withSuspense } from '@kbn/shared-ux-utility';
/**
* The Lazily-loaded `CustomizablePalette` component. Consumers should use `React.Suspense` or
* the withSuspense` HOC to load this component.
*/
export const CustomizablePaletteLazy = React.lazy(() => import('./coloring'));
/**
* A `CustomizablePalette` component that is wrapped by the `withSuspense` HOC. This component can
* be used directly by consumers and will load the `KibanaPageTemplateSolutionNavAvatarLazy` component lazily with
* a predefined fallback and error boundary.
*/
export const CustomizablePalette = withSuspense(CustomizablePaletteLazy);

View file

@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.bazel.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "target_types",
"rootDir": "src",
"stripInternal": false,
"types": [
"jest",
"node",
"react",
"@emotion/react/types/css-prop"
]
},
"include": [
"src/**/*"
]
}

View file

@ -8,6 +8,7 @@
// Please also add new aliases to test/scripts/jenkins_storybook.sh
export const storybookAliases = {
coloring: 'packages/kbn-coloring/.storybook',
apm: 'x-pack/plugins/apm/.storybook',
canvas: 'x-pack/plugins/canvas/storybook',
ci_composite: '.ci/.storybook',

View file

@ -9,12 +9,8 @@
export const COLOR_MAPPING_SETTING = 'visualization:colorMapping';
export const LEGACY_TIME_AXIS = 'visualization:useLegacyTimeAxis';
export type {
CustomPaletteArguments,
CustomPaletteState,
SystemPaletteArguments,
PaletteOutput,
} from './palette';
export type { CustomPaletteArguments, CustomPaletteState, SystemPaletteArguments } from './palette';
export { defaultCustomColors, palette, systemPalette } from './palette';
export { paletteIds } from './constants';
@ -30,8 +26,14 @@ export {
LabelRotation,
defaultCountLabel,
MULTILAYER_TIME_AXIS_STYLE,
checkIsMinContinuity,
checkIsMaxContinuity,
} from './static';
export type { ColorSchemaParams, Labels, Style, PaletteContinuity } from './types';
export type { ColorSchemaParams, Labels, Style } from './types';
/** @deprecated **/
/** Please import directly from @kbn/coloring **/
export { checkIsMinContinuity, checkIsMaxContinuity } from '@kbn/coloring';
/** @deprecated **/
/** Please import directly from @kbn/coloring **/
export type { PaletteOutput, PaletteContinuity } from '@kbn/coloring';

View file

@ -5,12 +5,11 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { PaletteOutput } from '@kbn/coloring';
import {
palette,
defaultCustomColors,
systemPalette,
PaletteOutput,
CustomPaletteState,
CustomPaletteArguments,
} from './palette';

View file

@ -7,12 +7,11 @@
*/
import type { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import type { PaletteContinuity } from '@kbn/coloring';
import { i18n } from '@kbn/i18n';
import { checkIsMaxContinuity, checkIsMinContinuity } from '@kbn/coloring';
import { last } from 'lodash';
import { paletteIds } from './constants';
import { checkIsMaxContinuity, checkIsMinContinuity } from './static';
import type { PaletteContinuity } from './types';
export interface CustomPaletteArguments {
color?: string[];

View file

@ -17,5 +17,4 @@ export {
} from './color_maps';
export { ColorMode, LabelRotation, defaultCountLabel } from './components';
export { checkIsMaxContinuity, checkIsMinContinuity } from './palette';
export * from './styles';

View file

@ -8,8 +8,6 @@
import { ColorSchemas, LabelRotation } from './static';
export type PaletteContinuity = 'above' | 'below' | 'none' | 'all';
export interface ColorSchemaParams {
colorSchema: ColorSchemas;
invertColors: boolean;

View file

@ -16,12 +16,10 @@ export const plugin = () => new ChartsPlugin();
export type { ChartsPluginSetup, ChartsPluginStart } from './plugin';
export * from './static';
export * from './services/palettes/types';
export { lightenColor } from './services/palettes/lighten_color';
export { useActiveCursor } from './services/active_cursor';
export type {
PaletteOutput,
CustomPaletteArguments,
CustomPaletteState,
SystemPaletteArguments,
@ -44,3 +42,7 @@ export {
LabelRotation,
defaultCountLabel,
} from '../common';
/** @deprecated **/
/** Please import directly from @kbn/coloring **/
export type { SeriesLayer, PaletteRegistry, PaletteOutput, PaletteDefinition } from '@kbn/coloring';

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { checkIsMinContinuity, checkIsMaxContinuity } from '../../../common';
import { checkIsMaxContinuity, checkIsMinContinuity } from '@kbn/coloring';
import type { CustomPaletteState } from '../..';
function findColorSegment(

View file

@ -6,9 +6,9 @@
* Side Public License, v 1.
*/
import { PublicMethodsOf } from '@kbn/utility-types';
import { PaletteService } from './service';
import { PaletteDefinition, SeriesLayer } from './types';
import type { PublicMethodsOf } from '@kbn/utility-types';
import type { PaletteDefinition, SeriesLayer } from '@kbn/coloring';
import type { PaletteService } from './service';
export const getPaletteRegistry = () => {
const mockPalette1: jest.Mocked<PaletteDefinition> = {

View file

@ -6,8 +6,8 @@
* Side Public License, v 1.
*/
import type { PaletteDefinition } from '@kbn/coloring';
import { createColorPalette as createLegacyColorPalette } from '../../../../../../src/plugins/charts/public';
import { PaletteDefinition } from './types';
import { buildPalettes } from './palettes';
import { colorsServiceMock } from '../legacy_colors/mock';
import { euiPaletteColorBlind, euiPaletteColorBlindBehindText } from '@elastic/eui';

View file

@ -21,13 +21,13 @@ import {
euiPaletteComplimentary,
euiPaletteColorBlindBehindText,
} from '@elastic/eui';
import type { ChartColorConfiguration, PaletteDefinition, SeriesLayer } from '@kbn/coloring';
import { flatten, zip } from 'lodash';
import {
ChartsPluginSetup,
createColorPalette as createLegacyColorPalette,
} from '../../../../../../src/plugins/charts/public';
import { lightenColor } from './lighten_color';
import { ChartColorConfiguration, PaletteDefinition, SeriesLayer } from './types';
import { LegacyColorsService } from '../legacy_colors';
import { MappedColors } from '../mapped_colors';
import { workoutColorForValue } from './helpers';

View file

@ -6,13 +6,10 @@
* Side Public License, v 1.
*/
import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public';
import {
ChartsPluginSetup,
PaletteDefinition,
PaletteRegistry,
} from '../../../../../../src/plugins/charts/public';
import { LegacyColorsService } from '../legacy_colors';
import type { PaletteRegistry, PaletteDefinition } from '@kbn/coloring';
import type { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public';
import type { ChartsPluginSetup } from '../../../../../../src/plugins/charts/public';
import type { LegacyColorsService } from '../legacy_colors';
export interface PaletteSetupPlugins {
expressions: ExpressionsSetup;

View file

@ -7,12 +7,7 @@
*/
import { ChartsServerPlugin } from './plugin';
export type {
PaletteOutput,
CustomPaletteArguments,
CustomPaletteState,
SystemPaletteArguments,
} from '../common';
export type { CustomPaletteArguments, CustomPaletteState, SystemPaletteArguments } from '../common';
export { paletteIds } from '../common';
export const plugin = () => new ChartsServerPlugin();

View file

@ -6,16 +6,13 @@
*/
import type { Direction } from '@elastic/eui';
import type { PaletteOutput, CustomPaletteParams } from '@kbn/coloring';
import { SortingHint } from '../..';
import type {
CustomPaletteState,
PaletteOutput,
} from '../../../../../../src/plugins/charts/common';
import type { CustomPaletteState } from '../../../../../../src/plugins/charts/common';
import type {
ExpressionFunctionDefinition,
DatatableColumn,
} from '../../../../../../src/plugins/expressions/common';
import type { CustomPaletteParams } from '../../types';
export type LensGridDirection = 'none' | Direction;

View file

@ -8,16 +8,13 @@
import type { Filter, FilterMeta } from '@kbn/es-query';
import { Position } from '@elastic/charts';
import { $Values } from '@kbn/utility-types';
import type { CustomPaletteParams, PaletteOutput } from '@kbn/coloring';
import type {
IFieldFormat,
SerializedFieldFormat,
} from '../../../../src/plugins/field_formats/common';
import type { Datatable } from '../../../../src/plugins/expressions/common';
import type {
PaletteContinuity,
PaletteOutput,
ColorMode,
} from '../../../../src/plugins/charts/common';
import type { ColorMode } from '../../../../src/plugins/charts/common';
import {
CategoryDisplay,
layerTypes,
@ -55,33 +52,12 @@ export interface LensMultiTable {
};
}
export interface ColorStop {
color: string;
stop: number;
}
export type SortingHint = 'version';
export interface CustomPaletteParams {
name?: string;
reverse?: boolean;
rangeType?: 'number' | 'percent';
continuity?: PaletteContinuity;
progression?: 'fixed';
rangeMin?: number;
rangeMax?: number;
stops?: ColorStop[];
colorStops?: ColorStop[];
steps?: number;
}
export type CustomPaletteParamsConfig = CustomPaletteParams & {
maxSteps?: number;
};
export type RequiredPaletteParamTypes = Required<CustomPaletteParams> & {
maxSteps?: number;
};
export type LayerType = typeof layerTypes[keyof typeof layerTypes];
// Shared by XY Chart and Heatmap as for now

View file

@ -7,6 +7,7 @@
import React from 'react';
import { EuiButtonGroup, EuiComboBox, EuiFieldText } from '@elastic/eui';
import type { PaletteRegistry } from '@kbn/coloring';
import {
FramePublicAPI,
OperationDescriptor,
@ -17,7 +18,6 @@ import { createMockDatasource, createMockFramePublicAPI } from '../../mocks';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { TableDimensionEditor } from './dimension_editor';
import { chartPluginMock } from 'src/plugins/charts/public/mocks';
import { PaletteRegistry } from 'src/plugins/charts/public';
import { act } from 'react-dom/test-utils';
import { PalettePanelContainer } from '../../shared_components';
import { layerTypes } from '../../../common';

View file

@ -19,14 +19,13 @@ import {
EuiFieldText,
EuiComboBox,
} from '@elastic/eui';
import { PaletteRegistry } from 'src/plugins/charts/public';
import { CustomizablePalette, PaletteRegistry, FIXED_PROGRESSION } from '@kbn/coloring';
import { VisualizationDimensionEditorProps } from '../../types';
import { DatatableVisualizationState } from '../visualization';
import {
CustomizablePalette,
applyPaletteParams,
defaultPaletteParams,
FIXED_PROGRESSION,
useDebouncedValue,
PalettePanelContainer,
findMinMaxByColumnId,

View file

@ -6,7 +6,7 @@
*/
import './table_basic.scss';
import { CUSTOM_PALETTE } from '@kbn/coloring';
import React, { useCallback, useMemo, useRef, useState, useContext, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import useDeepCompareEffect from 'react-use/lib/useDeepCompareEffect';
@ -42,7 +42,6 @@ import {
createGridSortingConfig,
createTransposeColumnFilterHandler,
} from './table_actions';
import { CUSTOM_PALETTE } from '../../shared_components/coloring/constants';
import { getOriginalId, getFinalSummaryConfiguration } from '../../../common/expressions';
export const DataContext = React.createContext<DataContextType>({});

View file

@ -6,7 +6,8 @@
*/
import { IUiSettingsClient } from 'kibana/public';
import { CustomPaletteState, PaletteRegistry } from 'src/plugins/charts/public';
import type { PaletteRegistry } from '@kbn/coloring';
import { CustomPaletteState } from 'src/plugins/charts/public';
import type { IAggType } from 'src/plugins/data/public';
import type { Datatable, RenderMode } from 'src/plugins/expressions';
import type { ILensInterpreterRenderHandlers, LensEditEvent } from '../../types';

View file

@ -9,9 +9,8 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n-react';
import type { PaletteRegistry } from '@kbn/coloring';
import type { IAggType } from 'src/plugins/data/public';
import { PaletteRegistry } from 'src/plugins/charts/public';
import { IUiSettingsClient, ThemeServiceStart } from 'kibana/public';
import { ExpressionRenderDefinition } from 'src/plugins/expressions';
import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public';

View file

@ -10,7 +10,7 @@ import { render } from 'react-dom';
import { Ast } from '@kbn/interpreter';
import { I18nProvider } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import type { PaletteRegistry } from 'src/plugins/charts/public';
import { PaletteRegistry, CUSTOM_PALETTE } from '@kbn/coloring';
import { ThemeServiceStart } from 'kibana/public';
import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public';
import type {
@ -21,12 +21,12 @@ import type {
} from '../types';
import { LensIconChartDatatable } from '../assets/chart_datatable';
import { TableDimensionEditor } from './components/dimension_editor';
import { CUSTOM_PALETTE } from '../shared_components/coloring/constants';
import { LayerType, layerTypes } from '../../common';
import { getDefaultSummaryLabel, PagingState } from '../../common/expressions';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public';
import type { ColumnState, SortingState } from '../../common/expressions';
import { DataTableToolbar } from './components/toolbar';
export interface DatatableVisualizationState {
columns: ColumnState[];
layerId: string;

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import type { PaletteOutput } from '@kbn/coloring';
import { getSuggestions, getTopSuggestionForField } from './suggestion_helpers';
import { createMockVisualization, createMockDatasource, DatasourceMock } from '../../mocks';
import {
@ -13,7 +14,6 @@ import {
Visualization,
VisualizeEditorContext,
} from '../../types';
import { PaletteOutput } from 'src/plugins/charts/public';
import { DatasourceStates } from '../../state_management';
const generateSuggestion = (state = {}, layerId: string = 'first'): DatasourceSuggestion => ({

View file

@ -6,7 +6,7 @@
*/
import { Datatable } from 'src/plugins/expressions';
import { PaletteOutput } from 'src/plugins/charts/public';
import type { PaletteOutput } from '@kbn/coloring';
import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public';
import {
Visualization,

View file

@ -7,6 +7,7 @@
import React from 'react';
import { ReactWrapper } from 'enzyme';
import type { PaletteOutput } from '@kbn/coloring';
import {
createMockVisualization,
mockStoreDeps,
@ -30,7 +31,6 @@ jest.mock('react-virtualized-auto-sizer', () => {
import { Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../../types';
import { ChartSwitch } from './chart_switch';
import { PaletteOutput } from 'src/plugins/charts/public';
import { applyChanges } from '../../../state_management';
describe('chart_switch', () => {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { PaletteDefinition } from 'src/plugins/charts/public';
import type { PaletteDefinition } from '@kbn/coloring';
import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public';
import { embeddablePluginMock } from '../../../../../src/plugins/embeddable/public/mocks';
import { expressionsPluginMock } from '../../../../../src/plugins/expressions/public/mocks';

View file

@ -10,6 +10,7 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { render, unmountComponentAtNode } from 'react-dom';
import { DataViewBase, Filter } from '@kbn/es-query';
import type { PaletteOutput } from '@kbn/coloring';
import {
ExecutionContextSearch,
Query,
@ -17,7 +18,6 @@ import {
TimeRange,
FilterManager,
} from 'src/plugins/data/public';
import type { PaletteOutput } from 'src/plugins/charts/public';
import type { Start as InspectorStart } from 'src/plugins/inspector/public';
import { Subscription } from 'rxjs';

View file

@ -14,13 +14,9 @@ import {
EuiFlexGroup,
EuiButtonEmpty,
} from '@elastic/eui';
import type { PaletteRegistry } from 'src/plugins/charts/public';
import { CustomizablePalette, FIXED_PROGRESSION, PaletteRegistry } from '@kbn/coloring';
import type { VisualizationDimensionEditorProps } from '../types';
import {
CustomizablePalette,
FIXED_PROGRESSION,
PalettePanelContainer,
} from '../shared_components/';
import { PalettePanelContainer } from '../shared_components/';
import './dimension_editor.scss';
import type { HeatmapVisualizationState } from './types';
import { getSafePaletteParams } from './utils';

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import type { PaletteOutput } from '../../../../../src/plugins/charts/common';
import type { PaletteOutput, CustomPaletteParams } from '@kbn/coloring';
import type { HeatmapArguments } from '../../../../../src/plugins/chart_expressions/expression_heatmap/common';
import type { CustomPaletteParams, LayerType } from '../../common';
import type { LayerType } from '../../common';
export type ChartShapes = 'heatmap';
export type HeatmapLayerState = HeatmapArguments & {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { PaletteRegistry } from 'src/plugins/charts/public';
import type { PaletteRegistry } from '@kbn/coloring';
import type { Datatable } from 'src/plugins/expressions';
import { applyPaletteParams, findMinMaxByColumnId } from '../shared_components';
import { DEFAULT_PALETTE_NAME } from './constants';

View file

@ -11,9 +11,9 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage, I18nProvider } from '@kbn/i18n-react';
import { Ast } from '@kbn/interpreter';
import { Position } from '@elastic/charts';
import { CUSTOM_PALETTE, PaletteRegistry, CustomPaletteParams } from '@kbn/coloring';
import { ThemeServiceStart } from 'kibana/public';
import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public';
import { PaletteRegistry } from '../../../../../src/plugins/charts/public';
import { HeatmapIcon } from '../../../../../src/plugins/chart_expressions/expression_heatmap/public';
import type { OperationMetadata, Visualization } from '../types';
import type { HeatmapVisualizationState } from './types';
@ -29,10 +29,8 @@ import {
LENS_HEATMAP_ID,
} from './constants';
import { HeatmapToolbar } from './toolbar_component';
import { CUSTOM_PALETTE } from '../shared_components';
import { HeatmapDimensionEditor } from './dimension_editor';
import { getSafePaletteParams } from './utils';
import type { CustomPaletteParams } from '../../common';
import { layerTypes } from '../../common';
const groupLabelForHeatmap = i18n.translate('xpack.lens.heatmapVisualization.heatmapGroupLabel', {

View file

@ -12,10 +12,17 @@ import { createMockDatasource, createMockFramePublicAPI } from '../mocks';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { MetricDimensionEditor } from './dimension_editor';
import { chartPluginMock } from 'src/plugins/charts/public/mocks';
import { ColorMode, PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
import { ColorMode } from 'src/plugins/charts/public';
import {
CustomizablePalette,
PaletteOutput,
PaletteRegistry,
CustomPaletteParams,
} from '@kbn/coloring';
import { act } from 'react-dom/test-utils';
import { CustomizablePalette, PalettePanelContainer } from '../shared_components';
import { CustomPaletteParams, layerTypes } from '../../common';
import { PalettePanelContainer } from '../shared_components';
import { layerTypes } from '../../common';
import type { MetricState } from '../../common/types';
// mocking random id generator function

View file

@ -13,19 +13,18 @@ import {
EuiFormRow,
htmlIdGenerator,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useState } from 'react';
import { ColorMode } from '../../../../../src/plugins/charts/common';
import type { PaletteRegistry } from '../../../../../src/plugins/charts/public';
import type { MetricState } from '../../common/types';
import { isNumericFieldForDatatable } from '../../common/expressions';
import {
applyPaletteParams,
PaletteRegistry,
CustomizablePalette,
CUSTOM_PALETTE,
FIXED_PROGRESSION,
PalettePanelContainer,
} from '../shared_components';
} from '@kbn/coloring';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useState } from 'react';
import { ColorMode } from '../../../../../src/plugins/charts/common';
import type { MetricState } from '../../common/types';
import { isNumericFieldForDatatable } from '../../common/expressions';
import { applyPaletteParams, PalettePanelContainer } from '../shared_components';
import type { VisualizationDimensionEditorProps } from '../types';
import { defaultPaletteParams } from './palette_config';

View file

@ -5,11 +5,12 @@
* 2.0.
*/
import { RequiredPaletteParamTypes } from '../../common';
import { RequiredPaletteParamTypes } from '@kbn/coloring';
import { defaultPaletteParams as sharedDefaultParams } from '../shared_components/';
export const DEFAULT_PALETTE_NAME = 'status';
export const DEFAULT_COLOR_STEPS = 3;
export const defaultPaletteParams: RequiredPaletteParamTypes = {
...sharedDefaultParams,
maxSteps: 5,

View file

@ -11,20 +11,15 @@ import { I18nProvider } from '@kbn/i18n-react';
import { euiThemeVars } from '@kbn/ui-theme';
import { render } from 'react-dom';
import { Ast } from '@kbn/interpreter';
import { PaletteOutput, PaletteRegistry, CUSTOM_PALETTE, shiftPalette } from '@kbn/coloring';
import { ThemeServiceStart } from 'kibana/public';
import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public';
import {
ColorMode,
CustomPaletteState,
PaletteOutput,
} from '../../../../../src/plugins/charts/common';
import { PaletteRegistry } from '../../../../../src/plugins/charts/public';
import { ColorMode, CustomPaletteState } from '../../../../../src/plugins/charts/common';
import { getSuggestions } from './metric_suggestions';
import { LensIconChartMetric } from '../assets/chart_metric';
import { Visualization, OperationMetadata, DatasourcePublicAPI } from '../types';
import type { MetricState } from '../../common/types';
import { layerTypes } from '../../common';
import { CUSTOM_PALETTE, shiftPalette } from '../shared_components';
import { MetricDimensionEditor } from './dimension_editor';
import { MetricToolbar } from './metric_config_panel';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { PaletteOutput } from 'src/plugins/charts/public';
import type { PaletteOutput } from '@kbn/coloring';
import { suggestions } from './suggestions';
import type { DataType, SuggestionRequest } from '../types';
import {

View file

@ -7,8 +7,8 @@
import type { Ast } from '@kbn/interpreter';
import { Position } from '@elastic/charts';
import type { PaletteOutput, PaletteRegistry } from '@kbn/coloring';
import type { PaletteOutput, PaletteRegistry } from '../../../../../src/plugins/charts/public';
import {
buildExpression,
buildExpressionFunction,

View file

@ -17,7 +17,7 @@ import {
EuiButtonGroup,
} from '@elastic/eui';
import type { Position } from '@elastic/charts';
import type { PaletteRegistry } from 'src/plugins/charts/public';
import type { PaletteRegistry } from '@kbn/coloring';
import { DEFAULT_PERCENT_DECIMALS } from './constants';
import { PartitionChartsMeta } from './partition_charts_meta';
import { LegendDisplay, PieVisualizationState, SharedPieLayerState } from '../../common';

View file

@ -9,7 +9,7 @@ import React from 'react';
import { render } from 'react-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, I18nProvider } from '@kbn/i18n-react';
import type { PaletteRegistry } from 'src/plugins/charts/public';
import type { PaletteRegistry } from '@kbn/coloring';
import { ThemeServiceStart } from 'kibana/public';
import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public';

View file

@ -5,20 +5,18 @@
* 2.0.
*/
import type { RequiredPaletteParamTypes } from '../../../common';
export const DEFAULT_PALETTE_NAME = 'positive';
export const FIXED_PROGRESSION = 'fixed' as const;
export const CUSTOM_PALETTE = 'custom';
export const DEFAULT_CONTINUITY = 'above';
export const DEFAULT_RANGE_TYPE = 'percent';
export const DEFAULT_MIN_STOP = 0;
export const DEFAULT_MAX_STOP = 100;
export const DEFAULT_COLOR_STEPS = 5;
export const DEFAULT_COLOR = '#6092C0'; // Same as EUI ColorStops default for new stops
import {
DEFAULT_PALETTE_NAME,
FIXED_PROGRESSION,
DEFAULT_CONTINUITY,
DEFAULT_RANGE_TYPE,
DEFAULT_MIN_STOP,
DEFAULT_MAX_STOP,
DEFAULT_COLOR_STEPS,
RequiredPaletteParamTypes,
} from '@kbn/coloring';
export const defaultPaletteParams: RequiredPaletteParamTypes = {
maxSteps: undefined,
name: DEFAULT_PALETTE_NAME,
reverse: false,
rangeType: DEFAULT_RANGE_TYPE,

View file

@ -5,9 +5,7 @@
* 2.0.
*/
export { CustomizablePalette } from './palette_configuration';
export { PalettePanelContainer } from './palette_panel_container';
export { ColorRanges } from './color_ranges';
export * from './utils';
export * from './constants';
export { defaultPaletteParams } from './constants';

View file

@ -6,20 +6,7 @@
*/
import { chartPluginMock } from 'src/plugins/charts/public/mocks';
import {
applyPaletteParams,
getColorStops,
getContrastColor,
getDataMinMax,
getPaletteStops,
getStepValue,
isValidColor,
mergePaletteParams,
remapStopsByNewInterval,
reversePalette,
updateRangeType,
changeColorPalette,
} from './utils';
import { applyPaletteParams, getContrastColor } from './utils';
describe('applyPaletteParams', () => {
const paletteRegistry = chartPluginMock.createPaletteRegistry();
@ -61,406 +48,6 @@ describe('applyPaletteParams', () => {
});
});
describe('getColorStops', () => {
const paletteRegistry = chartPluginMock.createPaletteRegistry();
it('should return the same colorStops if a custom palette is passed, avoiding recomputation', () => {
const colorStops = [
{ stop: 0, color: 'red' },
{ stop: 100, color: 'blue' },
];
expect(
getColorStops(
paletteRegistry,
colorStops,
{ name: 'custom', type: 'palette' },
{ min: 0, max: 100 }
)
).toBe(colorStops);
});
it('should get a fresh list of colors', () => {
expect(
getColorStops(
paletteRegistry,
[
{ stop: 0, color: 'red' },
{ stop: 100, color: 'blue' },
],
{ name: 'mocked', type: 'palette' },
{ min: 0, max: 100 }
)
).toEqual([
{ color: 'blue', stop: 0 },
{ color: 'yellow', stop: 50 },
]);
});
it('should get a fresh list of colors even if custom palette but empty colorStops', () => {
expect(
getColorStops(paletteRegistry, [], { name: 'mocked', type: 'palette' }, { min: 0, max: 100 })
).toEqual([
{ color: 'blue', stop: 0 },
{ color: 'yellow', stop: 50 },
]);
});
it('should correctly map the new colorStop to the current data bound and minValue', () => {
expect(
getColorStops(
paletteRegistry,
[],
{ name: 'mocked', type: 'palette', params: { rangeType: 'number' } },
{ min: 100, max: 1000 }
)
).toEqual([
{ color: 'blue', stop: 100 },
{ color: 'yellow', stop: 550 },
]);
});
it('should reverse the colors', () => {
expect(
getColorStops(
paletteRegistry,
[],
{ name: 'mocked', type: 'palette', params: { reverse: true } },
{ min: 100, max: 1000 }
)
).toEqual([
{ color: 'yellow', stop: 0 },
{ color: 'blue', stop: 50 },
]);
});
});
describe('remapStopsByNewInterval', () => {
it('should correctly remap the current palette from 0..1 to 0...100', () => {
expect(
remapStopsByNewInterval(
[
{ color: 'black', stop: 0 },
{ color: 'green', stop: 0.5 },
{ color: 'red', stop: 0.9 },
],
{ newInterval: 100, oldInterval: 1, newMin: 0, oldMin: 0 }
)
).toEqual([
{ color: 'black', stop: 0 },
{ color: 'green', stop: 50 },
{ color: 'red', stop: 90 },
]);
// now test the other way around
expect(
remapStopsByNewInterval(
[
{ color: 'black', stop: 0 },
{ color: 'green', stop: 50 },
{ color: 'red', stop: 90 },
],
{ newInterval: 1, oldInterval: 100, newMin: 0, oldMin: 0 }
)
).toEqual([
{ color: 'black', stop: 0 },
{ color: 'green', stop: 0.5 },
{ color: 'red', stop: 0.9 },
]);
});
it('should correctly handle negative numbers to/from', () => {
expect(
remapStopsByNewInterval(
[
{ color: 'black', stop: -100 },
{ color: 'green', stop: -50 },
{ color: 'red', stop: -1 },
],
{ newInterval: 100, oldInterval: 100, newMin: 0, oldMin: -100 }
)
).toEqual([
{ color: 'black', stop: 0 },
{ color: 'green', stop: 50 },
{ color: 'red', stop: 99 },
]);
// now map the other way around
expect(
remapStopsByNewInterval(
[
{ color: 'black', stop: 0 },
{ color: 'green', stop: 50 },
{ color: 'red', stop: 99 },
],
{ newInterval: 100, oldInterval: 100, newMin: -100, oldMin: 0 }
)
).toEqual([
{ color: 'black', stop: -100 },
{ color: 'green', stop: -50 },
{ color: 'red', stop: -1 },
]);
// and test also palettes that also contains negative values
expect(
remapStopsByNewInterval(
[
{ color: 'black', stop: -50 },
{ color: 'green', stop: 0 },
{ color: 'red', stop: 50 },
],
{ newInterval: 100, oldInterval: 100, newMin: 0, oldMin: -50 }
)
).toEqual([
{ color: 'black', stop: 0 },
{ color: 'green', stop: 50 },
{ color: 'red', stop: 100 },
]);
});
});
describe('getDataMinMax', () => {
it('should pick the correct min/max based on the current range type', () => {
expect(getDataMinMax('percent', { min: -100, max: 0 })).toEqual({ min: 0, max: 100 });
});
it('should pick the correct min/max apply percent by default', () => {
expect(getDataMinMax(undefined, { min: -100, max: 0 })).toEqual({ min: 0, max: 100 });
});
});
describe('getPaletteStops', () => {
const paletteRegistry = chartPluginMock.createPaletteRegistry();
it('should correctly compute a predefined palette stops definition from only the name', () => {
expect(
getPaletteStops(paletteRegistry, { name: 'mock' }, { dataBounds: { min: 0, max: 100 } })
).toEqual([
{ color: 'blue', stop: 20 },
{ color: 'yellow', stop: 70 },
]);
});
it('should correctly compute a predefined palette stops definition from explicit prevPalette (override)', () => {
expect(
getPaletteStops(
paletteRegistry,
{ name: 'default' },
{ dataBounds: { min: 0, max: 100 }, prevPalette: 'mock' }
)
).toEqual([
{ color: 'blue', stop: 20 },
{ color: 'yellow', stop: 70 },
]);
});
it('should infer the domain from dataBounds but start from 0', () => {
expect(
getPaletteStops(
paletteRegistry,
{ name: 'default', rangeType: 'number' },
{ dataBounds: { min: 1, max: 11 }, prevPalette: 'mock' }
)
).toEqual([
{ color: 'blue', stop: 2 },
{ color: 'yellow', stop: 7 },
]);
});
it('should override the minStop when requested', () => {
expect(
getPaletteStops(
paletteRegistry,
{ name: 'default', rangeType: 'number' },
{ dataBounds: { min: 1, max: 11 }, mapFromMinValue: true }
)
).toEqual([
{ color: 'red', stop: 1 },
{ color: 'black', stop: 6 },
]);
});
it('should compute a display stop palette from custom colorStops defined by the user', () => {
expect(
getPaletteStops(
paletteRegistry,
{
name: 'custom',
rangeType: 'number',
colorStops: [
{ color: 'green', stop: 0 },
{ color: 'blue', stop: 40 },
{ color: 'red', stop: 80 },
],
},
{ dataBounds: { min: 0, max: 100 } }
)
).toEqual([
{ color: 'green', stop: 40 },
{ color: 'blue', stop: 80 },
{ color: 'red', stop: 100 },
]);
});
it('should compute a display stop palette from custom colorStops defined by the user - handle stop at the end', () => {
expect(
getPaletteStops(
paletteRegistry,
{
name: 'custom',
rangeType: 'number',
colorStops: [
{ color: 'green', stop: 0 },
{ color: 'blue', stop: 40 },
{ color: 'red', stop: 100 },
],
},
{ dataBounds: { min: 0, max: 100 } }
)
).toEqual([
{ color: 'green', stop: 40 },
{ color: 'blue', stop: 100 },
{ color: 'red', stop: 101 },
]);
});
it('should compute a display stop palette from custom colorStops defined by the user - handle stop at the end (fractional)', () => {
expect(
getPaletteStops(
paletteRegistry,
{
name: 'custom',
rangeType: 'number',
colorStops: [
{ color: 'green', stop: 0 },
{ color: 'blue', stop: 0.4 },
{ color: 'red', stop: 1 },
],
},
{ dataBounds: { min: 0, max: 1 } }
)
).toEqual([
{ color: 'green', stop: 0.4 },
{ color: 'blue', stop: 1 },
{ color: 'red', stop: 2 },
]);
});
it('should compute a display stop palette from custom colorStops defined by the user - stretch the stops to 100% percent', () => {
expect(
getPaletteStops(
paletteRegistry,
{
name: 'custom',
colorStops: [
{ color: 'green', stop: 0 },
{ color: 'blue', stop: 0.4 },
{ color: 'red', stop: 1 },
],
},
{ dataBounds: { min: 0, max: 1 } }
)
).toEqual([
{ color: 'green', stop: 0.4 },
{ color: 'blue', stop: 1 },
{ color: 'red', stop: 100 }, // default rangeType is percent, hence stretch to 100%
]);
});
});
describe('reversePalette', () => {
it('should correctly reverse color and stops', () => {
expect(
reversePalette([
{ color: 'red', stop: 0 },
{ color: 'green', stop: 0.5 },
{ color: 'blue', stop: 0.9 },
])
).toEqual([
{ color: 'blue', stop: 0 },
{ color: 'green', stop: 0.5 },
{ color: 'red', stop: 0.9 },
]);
});
});
describe('mergePaletteParams', () => {
it('should return a full palette', () => {
expect(mergePaletteParams({ type: 'palette', name: 'myPalette' }, { reverse: true })).toEqual({
type: 'palette',
name: 'myPalette',
params: { reverse: true },
});
});
});
describe('isValidColor', () => {
it('should return ok for valid hex color notation', () => {
expect(isValidColor('#fff')).toBe(true);
expect(isValidColor('#ffffff')).toBe(true);
expect(isValidColor('#ffffffaa')).toBe(true);
});
it('should return false for non valid strings', () => {
expect(isValidColor('')).toBe(false);
expect(isValidColor('#')).toBe(false);
expect(isValidColor('#ff')).toBe(false);
expect(isValidColor('123')).toBe(false);
expect(isValidColor('rgb(1, 1, 1)')).toBe(false);
expect(isValidColor('rgba(1, 1, 1, 0)')).toBe(false);
expect(isValidColor('#ffffffgg')).toBe(false);
expect(isValidColor('#fff00')).toBe(false);
// this version of chroma does not support hex4 format
expect(isValidColor('#fffa')).toBe(false);
});
});
describe('getStepValue', () => {
it('should compute the next step based on the last 2 stops', () => {
expect(
getStepValue(
// first arg is taken as max reference
[
{ color: 'red', stop: 0 },
{ color: 'red', stop: 50 },
],
[
{ color: 'red', stop: 0 },
{ color: 'red', stop: 50 },
],
100
)
).toBe(50);
expect(
getStepValue(
// first arg is taken as max reference
[
{ color: 'red', stop: 0 },
{ color: 'red', stop: 80 },
],
[
{ color: 'red', stop: 0 },
{ color: 'red', stop: 50 },
],
90
)
).toBe(10); // 90 - 80
expect(
getStepValue(
// first arg is taken as max reference
[
{ color: 'red', stop: 0 },
{ color: 'red', stop: 100 },
],
[
{ color: 'red', stop: 0 },
{ color: 'red', stop: 50 },
],
100
)
).toBe(1);
});
});
describe('getContrastColor', () => {
it('should pick the light color when the passed one is dark', () => {
expect(getContrastColor('#000', true)).toBe('#ffffff');
@ -483,310 +70,3 @@ describe('getContrastColor', () => {
expect(getContrastColor('rgba(255,255,255,0)', false)).toBe('#000000');
});
});
describe('updateRangeType', () => {
const paletteRegistry = chartPluginMock.createPaletteRegistry();
const colorRanges = [
{
start: 0,
end: 40,
color: 'green',
},
{
start: 40,
end: 80,
color: 'blue',
},
{
start: 80,
end: 100,
color: 'red',
},
];
it('should correctly update palette params with new range type if continuity is none', () => {
const newPaletteParams = updateRangeType(
'number',
{
type: 'palette',
name: 'custom',
params: {
continuity: 'none',
name: 'custom',
rangeType: 'percent',
rangeMax: 100,
rangeMin: 0,
colorStops: [
{ color: 'green', stop: 0 },
{ color: 'blue', stop: 40 },
{ color: 'red', stop: 80 },
],
},
},
{ min: 0, max: 200 },
paletteRegistry,
colorRanges
);
expect(newPaletteParams).toEqual({
rangeType: 'number',
rangeMin: 0,
rangeMax: 200,
colorStops: [
{
color: 'green',
stop: 0,
},
{
color: 'blue',
stop: 80,
},
{
color: 'red',
stop: 160,
},
],
stops: [
{
color: 'green',
stop: 80,
},
{
color: 'blue',
stop: 160,
},
{
color: 'red',
stop: 200,
},
],
});
});
it('should correctly update palette params with new range type if continuity is all', () => {
const newPaletteParams = updateRangeType(
'number',
{
type: 'palette',
name: 'custom',
params: {
continuity: 'all',
name: 'custom',
rangeType: 'percent',
rangeMax: 100,
rangeMin: 0,
colorStops: [
{ color: 'green', stop: 0 },
{ color: 'blue', stop: 40 },
{ color: 'red', stop: 80 },
],
},
},
{ min: 0, max: 200 },
paletteRegistry,
colorRanges
);
expect(newPaletteParams).toEqual({
rangeType: 'number',
rangeMin: Number.NEGATIVE_INFINITY,
rangeMax: Number.POSITIVE_INFINITY,
colorStops: [
{
color: 'green',
stop: 0,
},
{
color: 'blue',
stop: 80,
},
{
color: 'red',
stop: 160,
},
],
stops: [
{
color: 'green',
stop: 80,
},
{
color: 'blue',
stop: 160,
},
{
color: 'red',
stop: 200,
},
],
});
});
it('should correctly update palette params with new range type if continuity is below', () => {
const newPaletteParams = updateRangeType(
'number',
{
type: 'palette',
name: 'custom',
params: {
continuity: 'below',
name: 'custom',
rangeType: 'percent',
rangeMax: 100,
rangeMin: 0,
colorStops: [
{ color: 'green', stop: 0 },
{ color: 'blue', stop: 40 },
{ color: 'red', stop: 80 },
],
},
},
{ min: 0, max: 200 },
paletteRegistry,
colorRanges
);
expect(newPaletteParams).toEqual({
rangeType: 'number',
rangeMin: Number.NEGATIVE_INFINITY,
rangeMax: 200,
colorStops: [
{
color: 'green',
stop: 0,
},
{
color: 'blue',
stop: 80,
},
{
color: 'red',
stop: 160,
},
],
stops: [
{
color: 'green',
stop: 80,
},
{
color: 'blue',
stop: 160,
},
{
color: 'red',
stop: 200,
},
],
});
});
it('should correctly update palette params with new range type if continuity is above', () => {
const newPaletteParams = updateRangeType(
'number',
{
type: 'palette',
name: 'custom',
params: {
continuity: 'above',
name: 'custom',
rangeType: 'percent',
rangeMax: 100,
rangeMin: 0,
colorStops: [
{ color: 'green', stop: 0 },
{ color: 'blue', stop: 40 },
{ color: 'red', stop: 80 },
],
},
},
{ min: 0, max: 200 },
paletteRegistry,
colorRanges
);
expect(newPaletteParams).toEqual({
rangeType: 'number',
rangeMin: 0,
rangeMax: Number.POSITIVE_INFINITY,
colorStops: [
{
color: 'green',
stop: 0,
},
{
color: 'blue',
stop: 80,
},
{
color: 'red',
stop: 160,
},
],
stops: [
{
color: 'green',
stop: 80,
},
{
color: 'blue',
stop: 160,
},
{
color: 'red',
stop: 200,
},
],
});
});
});
describe('changeColorPalette', () => {
const paletteRegistry = chartPluginMock.createPaletteRegistry();
it('should correct update params for new palette', () => {
const newPaletteParams = changeColorPalette(
{
type: 'palette',
name: 'default',
},
{
type: 'palette',
name: 'custom',
params: {
continuity: 'above',
name: 'custom',
rangeType: 'percent',
rangeMax: 100,
rangeMin: 0,
colorStops: [
{ color: 'green', stop: 0 },
{ color: 'blue', stop: 40 },
{ color: 'red', stop: 80 },
],
},
},
paletteRegistry,
{ min: 0, max: 200 },
false
);
expect(newPaletteParams).toEqual({
name: 'default',
type: 'palette',
params: {
rangeType: 'percent',
name: 'default',
continuity: 'above',
rangeMin: 0,
rangeMax: Number.POSITIVE_INFINITY,
reverse: false,
colorStops: undefined,
stops: [
{
color: 'red',
stop: 0,
},
{
color: 'black',
stop: 50,
},
],
},
});
});
});

View file

@ -6,535 +6,20 @@
*/
import chroma from 'chroma-js';
import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
import { euiLightVars, euiDarkVars } from '@kbn/ui-theme';
import { isColorDark } from '@elastic/eui';
import type { Datatable } from 'src/plugins/expressions/public';
import {
DataBounds,
PaletteRegistry,
PaletteOutput,
CustomPaletteParams,
getFallbackDataBounds,
reversePalette,
getPaletteStops,
CUSTOM_PALETTE,
defaultPaletteParams,
DEFAULT_COLOR_STEPS,
DEFAULT_MAX_STOP,
DEFAULT_MIN_STOP,
DEFAULT_CONTINUITY,
} from './constants';
import type { ColorRange } from './color_ranges';
import { toColorStops, sortColorRanges } from './color_ranges/utils';
import type { PaletteConfigurationState, DataBounds } from './types';
import type { CustomPaletteParams, ColorStop } from '../../../common';
import {
checkIsMinContinuity,
checkIsMaxContinuity,
} from '../../../../../../src/plugins/charts/common';
/**
* Some name conventions here:
* * `displayStops` => It's an additional transformation of `stops` into a [0, N] domain for the EUIPaletteDisplay component.
* * `stops` => final steps used to table coloring. It is a rightShift of the colorStops
* * `colorStops` => user's color stop inputs. Used to compute range min.
*
* When the user inputs the colorStops, they are designed to be the initial part of the color segment,
* so the next stops indicate where the previous stop ends.
* Both table coloring logic and EuiPaletteDisplay format implementation works differently than our current `colorStops`,
* by having the stop values at the end of each color segment rather than at the beginning: `stops` values are computed by a rightShift of `colorStops`.
* EuiPaletteDisplay has an additional requirement as it is always mapped against a domain [0, N]: from `stops` the `displayStops` are computed with
* some continuity enrichment and a remap against a [0, 100] domain to make the palette component work ok.
*
* These naming conventions would be useful to track the code flow in this feature as multiple transformations are happening
* for a single change.
*/
export function updateRangeType(
newRangeType: CustomPaletteParams['rangeType'],
activePalette: PaletteConfigurationState['activePalette'],
dataBounds: DataBounds,
palettes: PaletteRegistry,
colorRanges: PaletteConfigurationState['colorRanges']
) {
const continuity = activePalette.params?.continuity ?? DEFAULT_CONTINUITY;
const params: CustomPaletteParams = { rangeType: newRangeType };
const { min: newMin, max: newMax } = getDataMinMax(newRangeType, dataBounds);
const { min: oldMin, max: oldMax } = getDataMinMax(activePalette.params?.rangeType, dataBounds);
const newColorStops = getStopsFromColorRangesByNewInterval(colorRanges, {
oldInterval: oldMax - oldMin,
newInterval: newMax - newMin,
newMin,
oldMin,
});
if (activePalette.name === CUSTOM_PALETTE) {
const stops = getPaletteStops(
palettes,
{ ...activePalette.params, colorStops: newColorStops, ...params },
{ dataBounds }
);
params.colorStops = newColorStops;
params.stops = stops;
} else {
params.stops = getPaletteStops(
palettes,
{ ...activePalette.params, ...params },
{ prevPalette: activePalette.name, dataBounds }
);
}
const lastStop =
activePalette.name === CUSTOM_PALETTE
? newColorStops[newColorStops.length - 1].stop
: params.stops[params.stops.length - 1].stop;
params.rangeMin = checkIsMinContinuity(continuity)
? Number.NEGATIVE_INFINITY
: activePalette.name === CUSTOM_PALETTE
? newColorStops[0].stop
: params.stops[0].stop;
params.rangeMax = checkIsMaxContinuity(continuity)
? Number.POSITIVE_INFINITY
: activePalette.params?.rangeMax
? calculateStop(activePalette.params.rangeMax, newMin, oldMin, oldMax - oldMin, newMax - newMin)
: lastStop > newMax
? lastStop + 1
: newMax;
return params;
}
export function changeColorPalette(
newPalette: PaletteConfigurationState['activePalette'],
activePalette: PaletteConfigurationState['activePalette'],
palettes: PaletteRegistry,
dataBounds: DataBounds,
disableSwitchingContinuity: boolean
) {
const isNewPaletteCustom = newPalette.name === CUSTOM_PALETTE;
const newParams: CustomPaletteParams = {
...activePalette.params,
name: newPalette.name,
colorStops: undefined,
continuity: disableSwitchingContinuity
? activePalette.params?.continuity ?? DEFAULT_CONTINUITY
: DEFAULT_CONTINUITY,
reverse: false, // restore the reverse flag
};
// we should pass colorStops so that correct calculate new color stops (if there was before) for custom palette
const newColorStops = getColorStops(
palettes,
activePalette.params?.colorStops || [],
activePalette,
dataBounds
);
if (isNewPaletteCustom) {
newParams.colorStops = newColorStops;
}
return {
...newPalette,
params: {
...newParams,
stops: getPaletteStops(palettes, newParams, {
prevPalette:
isNewPaletteCustom || activePalette.name === CUSTOM_PALETTE ? undefined : newPalette.name,
dataBounds,
mapFromMinValue: true,
}),
rangeMin: checkIsMinContinuity(newParams.continuity)
? Number.NEGATIVE_INFINITY
: Math.min(dataBounds.min, newColorStops[0].stop),
rangeMax: checkIsMaxContinuity(newParams.continuity)
? Number.POSITIVE_INFINITY
: Math.min(dataBounds.max, newColorStops[newColorStops.length - 1].stop),
},
};
}
export function withUpdatingPalette(
palettes: PaletteRegistry,
activePalette: PaletteConfigurationState['activePalette'],
colorRanges: ColorRange[],
dataBounds: DataBounds,
continuity?: CustomPaletteParams['continuity']
) {
const currentContinuity = continuity ?? activePalette.params?.continuity ?? DEFAULT_CONTINUITY;
let sortedColorRanges = colorRanges;
if (
colorRanges.some((value, index) =>
index !== colorRanges.length - 1 ? value.start > colorRanges[index + 1].start : false
)
) {
sortedColorRanges = sortColorRanges(colorRanges);
}
const { max, colorStops } = toColorStops(sortedColorRanges, currentContinuity);
const newPallete = getSwitchToCustomParams(
palettes,
activePalette!,
{
continuity: currentContinuity,
colorStops,
steps: activePalette!.params?.steps || DEFAULT_COLOR_STEPS,
reverse: activePalette!.params?.reverse,
rangeMin: colorStops[0]?.stop,
rangeMax: max,
},
dataBounds!
);
return {
activePalette: newPallete,
colorRanges,
};
}
export function withUpdatingColorRanges(
palettes: PaletteRegistry,
activePalette: PaletteConfigurationState['activePalette'],
dataBounds: DataBounds
) {
return {
colorRanges: toColorRanges(
palettes,
activePalette.params?.colorStops || [],
activePalette,
dataBounds
),
activePalette,
};
}
export function applyPaletteParams<T extends PaletteOutput<CustomPaletteParams>>(
palettes: PaletteRegistry,
activePalette: T,
dataBounds: DataBounds
) {
// make a copy of it as they have to be manipulated later on
const displayStops = getPaletteStops(palettes, activePalette?.params || {}, {
dataBounds,
defaultPaletteName: activePalette?.name,
});
if (activePalette?.params?.reverse && activePalette?.params?.name !== CUSTOM_PALETTE) {
return reversePalette(displayStops);
}
return displayStops;
}
// Need to shift the Custom palette in order to correctly visualize it when in display mode
export function shiftPalette(stops: ColorStop[], max: number) {
// shift everything right and add an additional stop at the end
const result = stops.map((entry, i, array) => ({
...entry,
stop: i + 1 < array.length ? array[i + 1].stop : max,
}));
if (stops[stops.length - 1].stop === max) {
// extends the range by a fair amount to make it work the extra case for the last stop === max
const computedStep = getStepValue(stops, result, max) || 1;
// do not go beyond the unit step in this case
const step = Math.min(1, computedStep);
result[stops.length - 1].stop = max + step;
}
return result;
}
/** @internal **/
export function calculateStop(
stopValue: number,
newMin: number,
oldMin: number,
oldInterval: number,
newInterval: number
) {
if (oldInterval === 0) {
return newInterval + newMin;
}
return roundValue(newMin + ((stopValue - oldMin) * newInterval) / oldInterval);
}
// Utility to remap color stops within new domain
export function remapStopsByNewInterval(
controlStops: ColorStop[],
{
newInterval,
oldInterval,
newMin,
oldMin,
}: { newInterval: number; oldInterval: number; newMin: number; oldMin: number }
) {
return (controlStops || []).map(({ color, stop }) => {
return {
color,
stop: calculateStop(stop, newMin, oldMin, oldInterval, newInterval),
};
});
}
// Utility to remap color stops within new domain
export function getStopsFromColorRangesByNewInterval(
colorRanges: ColorRange[],
{
newInterval,
oldInterval,
newMin,
oldMin,
}: { newInterval: number; oldInterval: number; newMin: number; oldMin: number }
) {
return (colorRanges || []).map(({ color, start }) => {
let stop = calculateStop(start, newMin, oldMin, oldInterval, newInterval);
if (oldInterval === 0) {
stop = newInterval + newMin;
}
return {
color,
stop: roundValue(stop),
};
});
}
function getOverallMinMax(params: CustomPaletteParams | undefined, dataBounds: DataBounds) {
const { min: dataMin, max: dataMax } = getDataMinMax(params?.rangeType, dataBounds);
const minStopValue = params?.colorStops?.[0]?.stop ?? Number.POSITIVE_INFINITY;
const maxStopValue =
params?.colorStops?.[params.colorStops.length - 1]?.stop ?? Number.NEGATIVE_INFINITY;
const overallMin = Math.min(dataMin, minStopValue);
const overallMax = Math.max(dataMax, maxStopValue);
return { min: overallMin, max: overallMax };
}
export function getDataMinMax(
rangeType: CustomPaletteParams['rangeType'] | undefined,
dataBounds: DataBounds
) {
const dataMin = rangeType === 'number' ? dataBounds.min : DEFAULT_MIN_STOP;
const dataMax = rangeType === 'number' ? dataBounds.max : DEFAULT_MAX_STOP;
return { min: dataMin, max: dataMax };
}
/**
* This is a generic function to compute stops from the current parameters.
*/
export function getPaletteStops(
palettes: PaletteRegistry,
activePaletteParams: CustomPaletteParams,
// used to customize color resolution
{
prevPalette,
dataBounds,
mapFromMinValue,
defaultPaletteName,
}: {
prevPalette?: string;
dataBounds: DataBounds;
mapFromMinValue?: boolean;
defaultPaletteName?: string;
}
) {
const { min: minValue, max: maxValue } = getOverallMinMax(activePaletteParams, dataBounds);
const interval = maxValue - minValue;
const { stops: currentStops, ...otherParams } = activePaletteParams || {};
if (activePaletteParams.name === 'custom' && activePaletteParams?.colorStops) {
// need to generate the palette from the existing controlStops
return shiftPalette(activePaletteParams.colorStops, maxValue);
}
const steps = activePaletteParams?.steps || defaultPaletteParams.steps;
// generate a palette from predefined ones and customize the domain
const colorStopsFromPredefined = palettes
.get(
prevPalette || activePaletteParams?.name || defaultPaletteName || defaultPaletteParams.name
)
.getCategoricalColors(steps, otherParams);
const newStopsMin = mapFromMinValue || interval === 0 ? minValue : interval / steps;
return remapStopsByNewInterval(
colorStopsFromPredefined.map((color, index) => ({ color, stop: index })),
{
newInterval: interval,
oldInterval: colorStopsFromPredefined.length,
newMin: newStopsMin,
oldMin: 0,
}
);
}
export function reversePalette(paletteColorRepresentation: ColorStop[] = []) {
const stops = paletteColorRepresentation.map(({ stop }) => stop);
return paletteColorRepresentation
.map(({ color }, i) => ({
color,
stop: stops[paletteColorRepresentation.length - i - 1],
}))
.reverse();
}
export function mergePaletteParams(
activePalette: PaletteOutput<CustomPaletteParams>,
newParams: CustomPaletteParams
): PaletteOutput<CustomPaletteParams> {
return {
...activePalette,
params: {
...activePalette.params,
...newParams,
},
};
}
function isValidPonyfill(colorString: string) {
// we're using an old version of chroma without the valid function
try {
chroma(colorString);
return true;
} catch (e) {
return false;
}
}
export function isValidColor(colorString: string) {
// chroma can handle also hex values with alpha channel/transparency
// chroma accepts also hex without #, so test for it
return colorString !== '' && /^#/.test(colorString) && isValidPonyfill(colorString);
}
export function roundValue(value: number, fractionDigits: number = 2) {
return Number((Math.floor(value * 100) / 100).toFixed(fractionDigits));
}
// very simple heuristic: pick last two stops and compute a new stop based on the same distance
// if the new stop is above max, then reduce the step to reach max, or if zero then just 1.
//
// it accepts two series of stops as the function is used also when computing stops from colorStops
export function getStepValue(colorStops: ColorStop[], newColorStops: ColorStop[], max: number) {
const length = newColorStops.length;
// workout the steps from the last 2 items
const dataStep =
length > 1 ? newColorStops[length - 1].stop - newColorStops[length - 2].stop || 1 : 1;
let step = Number(dataStep.toFixed(2));
if (max < colorStops[length - 1].stop + step) {
const diffToMax = max - colorStops[length - 1].stop;
// if the computed step goes way out of bound, fallback to 1, otherwise reach max
step = diffToMax > 0 ? diffToMax : 1;
}
return step;
}
export function getSwitchToCustomParams(
palettes: PaletteRegistry,
activePalette: PaletteOutput<CustomPaletteParams>,
newParams: CustomPaletteParams,
dataBounds: DataBounds
) {
// if it's already a custom palette just return the params
if (activePalette?.params?.name === CUSTOM_PALETTE) {
const stops = getPaletteStops(
palettes,
{
steps: DEFAULT_COLOR_STEPS,
...activePalette.params,
...newParams,
},
{
dataBounds,
}
);
return mergePaletteParams(activePalette, {
...newParams,
stops,
});
}
// prepare everything to switch to custom palette
const newPaletteParams = {
steps: DEFAULT_COLOR_STEPS,
...activePalette.params,
...newParams,
name: CUSTOM_PALETTE,
};
const stops = getPaletteStops(palettes, newPaletteParams, {
prevPalette: newPaletteParams.colorStops ? undefined : activePalette.name,
dataBounds,
});
return mergePaletteParams(
{ name: CUSTOM_PALETTE, type: 'palette' },
{
...newPaletteParams,
stops,
}
);
}
export function getColorStops(
palettes: PaletteRegistry,
colorStops: Required<CustomPaletteParams>['stops'],
activePalette: PaletteOutput<CustomPaletteParams>,
dataBounds: DataBounds
) {
// just forward the current stops if custom
if (activePalette?.name === CUSTOM_PALETTE && colorStops?.length) {
return colorStops;
}
// for predefined palettes create some stops, then drop the last one.
// we're using these as starting point for the user
let freshColorStops = getPaletteStops(
palettes,
{ ...activePalette?.params },
// mapFromMinValue is a special flag to offset the stops values
// used here to avoid a new remap/left shift
{ dataBounds, mapFromMinValue: true, defaultPaletteName: activePalette.name }
);
if (activePalette?.params?.reverse) {
freshColorStops = reversePalette(freshColorStops);
}
return freshColorStops;
}
/**
* Both table coloring logic and EuiPaletteDisplay format implementation works differently than our current `colorStops`,
* by having the stop values at the end of each color segment rather than at the beginning: `stops` values are computed by a rightShift of `colorStops`.
* EuiPaletteDisplay has an additional requirement as it is always mapped against a domain [0, N]: from `stops` the `displayStops` are computed with
* some continuity enrichment and a remap against a [0, 100] domain to make the palette component work ok.
*
* These naming conventions would be useful to track the code flow in this feature as multiple transformations are happening
* for a single change.
*/
export function toColorRanges(
palettes: PaletteRegistry,
colorStops: CustomPaletteParams['colorStops'],
activePalette: PaletteOutput<CustomPaletteParams>,
dataBounds: DataBounds
) {
const {
continuity = defaultPaletteParams.continuity,
rangeType = defaultPaletteParams.rangeType,
} = activePalette.params ?? {};
const { min: dataMin, max: dataMax } = getDataMinMax(rangeType, dataBounds);
return getColorStops(palettes, colorStops || [], activePalette, dataBounds).map(
(colorStop, index, array) => {
const isFirst = index === 0;
const isLast = index === array.length - 1;
return {
color: colorStop.color,
start:
isFirst && checkIsMinContinuity(continuity)
? Number.NEGATIVE_INFINITY
: colorStop.stop ?? activePalette.params?.rangeMin ?? dataMin,
end:
isLast && checkIsMaxContinuity(continuity)
? Number.POSITIVE_INFINITY
: array[index + 1]?.stop ?? activePalette.params?.rangeMax ?? dataMax,
};
}
);
}
} from '@kbn/coloring';
import { Datatable } from '../../../../../../src/plugins/expressions';
export function getContrastColor(
color: string,
@ -555,10 +40,6 @@ export function getContrastColor(
return isColorDark(...finalColor.rgb()) ? lightColor : darkColor;
}
function getId(id: string) {
return id;
}
export function getNumericValue(rowValue: number | number[] | undefined) {
if (rowValue == null || Array.isArray(rowValue)) {
return;
@ -566,25 +47,27 @@ export function getNumericValue(rowValue: number | number[] | undefined) {
return rowValue;
}
export const getFallbackDataBounds = (
rangeType: CustomPaletteParams['rangeType'] = 'percent'
): DataBounds =>
rangeType === 'percent'
? {
min: 0,
max: 100,
fallback: true,
}
: {
min: 1,
max: 1,
fallback: true,
};
export function applyPaletteParams<T extends PaletteOutput<CustomPaletteParams>>(
palettes: PaletteRegistry,
activePalette: T,
dataBounds: DataBounds
) {
// make a copy of it as they have to be manipulated later on
const displayStops = getPaletteStops(palettes, activePalette?.params || {}, {
dataBounds,
defaultPaletteName: activePalette?.name,
});
if (activePalette?.params?.reverse && activePalette?.params?.name !== CUSTOM_PALETTE) {
return reversePalette(displayStops);
}
return displayStops;
}
export const findMinMaxByColumnId = (
columnIds: string[],
table: Datatable | undefined,
getOriginalId: (id: string) => string = getId
getOriginalId: (id: string) => string = (id: string) => id
) => {
const minMax: Record<string, DataBounds> = {};

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
import type { PaletteOutput, PaletteRegistry } from '@kbn/coloring';
import { EuiColorPalettePicker, EuiColorPalettePickerPaletteProps } from '@elastic/eui';
import { EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';

View file

@ -7,7 +7,7 @@
import { Ast } from '@kbn/interpreter';
import type { IconType } from '@elastic/eui/src/components/icon/icon';
import type { CoreSetup, SavedObjectReference, SavedObjectsResolveResponse } from 'kibana/public';
import type { PaletteOutput } from 'src/plugins/charts/public';
import type { PaletteOutput } from '@kbn/coloring';
import type { TopNavMenuData } from 'src/plugins/navigation/public';
import type { MutableRefObject } from 'react';
import { Filter } from '@kbn/es-query';

View file

@ -17,7 +17,12 @@ import {
} from '@elastic/eui';
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import type { PaletteRegistry } from 'src/plugins/charts/public';
import {
PaletteRegistry,
CustomizablePalette,
CUSTOM_PALETTE,
FIXED_PROGRESSION,
} from '@kbn/coloring';
import {
GaugeTicksPositions,
GaugeColorModes,
@ -29,9 +34,6 @@ import {
import { isNumericFieldForDatatable } from '../../../common/expressions';
import {
applyPaletteParams,
CustomizablePalette,
CUSTOM_PALETTE,
FIXED_PROGRESSION,
PalettePanelContainer,
TooltipWrapper,
} from '../../shared_components/';

View file

@ -10,9 +10,10 @@ import {
PaletteDefinition,
PaletteRegistry,
SeriesLayer,
} from 'src/plugins/charts/public';
RequiredPaletteParamTypes,
} from '@kbn/coloring';
import Color from 'color';
import { RequiredPaletteParamTypes } from '../../../common';
import { defaultPaletteParams as sharedDefaultParams } from '../../shared_components/';
export const DEFAULT_PALETTE_NAME = 'gray';

View file

@ -5,14 +5,14 @@
* 2.0.
*/
import type { PaletteOutput, CustomPaletteParams } from '@kbn/coloring';
import { getGaugeVisualization, isNumericDynamicMetric, isNumericMetric } from './visualization';
import { createMockDatasource, createMockFramePublicAPI } from '../../mocks';
import { GROUP_ID } from './constants';
import type { DatasourcePublicAPI, OperationDescriptor } from '../../types';
import { chartPluginMock } from 'src/plugins/charts/public/mocks';
import { CustomPaletteParams, layerTypes } from '../../../common';
import { layerTypes } from '../../../common';
import type { GaugeVisualizationState } from './constants';
import { PaletteOutput } from 'src/plugins/charts/common';
function exampleState(): GaugeVisualizationState {
return {

View file

@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage, I18nProvider } from '@kbn/i18n-react';
import { Ast } from '@kbn/interpreter';
import { DatatableRow } from 'src/plugins/expressions';
import { PaletteRegistry, CustomPaletteParams, CUSTOM_PALETTE } from '@kbn/coloring';
import type { GaugeArguments } from '../../../../../../src/plugins/chart_expressions/expression_gauge/common';
import {
GaugeShapes,
@ -24,7 +25,6 @@ import {
VerticalBulletIcon,
HorizontalBulletIcon,
} from '../../../../../../src/plugins/chart_expressions/expression_gauge/public';
import { PaletteRegistry } from '../../../../../../src/plugins/charts/public';
import type { DatasourcePublicAPI, OperationMetadata, Visualization } from '../../types';
import { getSuggestions } from './suggestions';
import {
@ -34,9 +34,9 @@ import {
GaugeExpressionState,
} from './constants';
import { GaugeToolbar } from './toolbar_component';
import { applyPaletteParams, CUSTOM_PALETTE } from '../../shared_components';
import { applyPaletteParams } from '../../shared_components';
import { GaugeDimensionEditor } from './dimension_editor';
import { CustomPaletteParams, layerTypes } from '../../../common';
import { layerTypes } from '../../../common';
import { generateId } from '../../id_generator';
import { getAccessorsFromState } from './utils';

View file

@ -6,7 +6,7 @@
*/
import { uniq, mapValues } from 'lodash';
import type { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
import type { PaletteOutput, PaletteRegistry } from '@kbn/coloring';
import type { Datatable } from 'src/plugins/expressions';
import { euiLightVars } from '@kbn/ui-theme';
import type { AccessorConfig, FramePublicAPI } from '../types';

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