mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Replace flot with elastic-chart in Timelion (#81565)
* First draft migrate timelion to elastic-charts * Some refactoring. Added brush event. * Added title. Some refactoring * Fixed some type problems. Added logic for yaxes function * Fixed some types, added missing functionality for yaxes * Fixed some types, added missing functionality for stack property * Fixed unit test * Removed unneeded code * Some refactoring * Some refactoring * Fixed some remarks. * Fixed some styles * Added themes. Removed unneeded styles in BarSeries * removed unneeded code. * Fixed some comments * Fixed vertical cursor across Timelion visualizations of a dashboad * Fix some problems with styles * Use RxJS instead of jQuery * Remove unneeded code * Fixed some problems * Fixed unit test * Fix CI * Fix eslint * Fix some gaps * Fix legend columns * Some fixes * add 2 versions of Timeline app * fix CI * cleanup code * fix CI * fix legend position * fix some cases * fix some cases * remove extra casting * cleanup code * fix issue with static * fix header formatter * fix points * fix ts error * Fix yaxis behavior * Fix some case with yaxis * Add deprecation message and update asciidoc * Fix title * some text improvements Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Alexey Antonov <alexwizp@gmail.com>
This commit is contained in:
parent
33bc6f1d37
commit
8088565ee9
35 changed files with 1719 additions and 772 deletions
|
@ -514,6 +514,10 @@ Shows the Timelion tutorial to users when they first open the Timelion app.
|
|||
Used for calculating automatic intervals in visualizations, this is the number
|
||||
of buckets to try to represent.
|
||||
|
||||
[[timelion-legacyChartsLibrary]]`timelion:legacyChartsLibrary`::
|
||||
Enables the legacy charts library for timelion charts in Visualize.
|
||||
|
||||
|
||||
[float]
|
||||
[[kibana-visualization-settings]]
|
||||
==== Visualization
|
||||
|
|
|
@ -228,6 +228,7 @@ export class DocLinksService {
|
|||
indexManagement: `${ELASTICSEARCH_DOCS}index-mgmt.html`,
|
||||
kibanaSearchSettings: `${KIBANA_DOCS}advanced-options.html#kibana-search-settings`,
|
||||
visualizationSettings: `${KIBANA_DOCS}advanced-options.html#kibana-visualization-settings`,
|
||||
timelionSettings: `${KIBANA_DOCS}advanced-options.html#kibana-timelion-settings`,
|
||||
},
|
||||
ml: {
|
||||
guide: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/index.html`,
|
||||
|
|
|
@ -80,6 +80,10 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
|
|||
type: 'text',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
'timelion:legacyChartsLibrary': {
|
||||
type: 'boolean',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
'timelion:target_buckets': {
|
||||
type: 'long',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
|
|
|
@ -45,6 +45,7 @@ export interface UsageStats {
|
|||
'visualization:tileMap:maxPrecision': number;
|
||||
'csv:separator': string;
|
||||
'visualization:tileMap:WMSdefaults': string;
|
||||
'timelion:legacyChartsLibrary': boolean;
|
||||
'timelion:target_buckets': number;
|
||||
'timelion:max_buckets': number;
|
||||
'timelion:es.timefield': string;
|
||||
|
|
|
@ -7258,6 +7258,12 @@
|
|||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"timelion:legacyChartsLibrary": {
|
||||
"type": "boolean",
|
||||
"_meta": {
|
||||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"timelion:target_buckets": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
registerListenEventListener,
|
||||
watchMultiDecorator,
|
||||
} from '../../kibana_legacy/public';
|
||||
import { getTimezone } from '../../vis_type_timelion/public';
|
||||
import { _LEGACY_ as visTypeTimelion } from '../../vis_type_timelion/public';
|
||||
import { initCellsDirective } from './directives/cells/cells';
|
||||
import { initFullscreenDirective } from './directives/fullscreen/fullscreen';
|
||||
import { initFixedElementDirective } from './directives/fixed_element';
|
||||
|
@ -144,7 +144,7 @@ export function initTimelionApp(app, deps) {
|
|||
$scope.updatedSheets = [];
|
||||
|
||||
const savedVisualizations = deps.plugins.visualizations.savedVisualizationsLoader;
|
||||
const timezone = getTimezone(deps.core.uiSettings);
|
||||
const timezone = visTypeTimelion.getTimezone(deps.core.uiSettings);
|
||||
|
||||
const defaultExpression = '.es(*)';
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { parseTimelionExpressionAsync } from '../../../vis_type_timelion/public';
|
||||
import { _LEGACY_ as visTypeTimelion } from '../../../vis_type_timelion/public';
|
||||
|
||||
export const SUGGESTION_TYPE = {
|
||||
ARGUMENTS: 'arguments',
|
||||
|
@ -180,7 +180,7 @@ async function extractSuggestionsFromParsedResult(
|
|||
|
||||
export async function suggest(expression, functionList, cursorPosition, argValueSuggestions) {
|
||||
try {
|
||||
const result = await parseTimelionExpressionAsync(expression);
|
||||
const result = await visTypeTimelion.parseTimelionExpressionAsync(expression);
|
||||
return await extractSuggestionsFromParsedResult(
|
||||
result,
|
||||
cursorPosition,
|
||||
|
|
|
@ -15,4 +15,4 @@
|
|||
// styles for timelion visualization are lazy loaded only while a vis is opened
|
||||
// this will duplicate styles only if both Timelion app and timelion visualization are loaded
|
||||
// could be left here as it is since the Timelion app is deprecated
|
||||
@import '../../vis_type_timelion/public/components/timelion_vis.scss';
|
||||
@import '../../vis_type_timelion/public/legacy/timelion_vis.scss';
|
||||
|
|
|
@ -11,13 +11,7 @@ import $ from 'jquery';
|
|||
import moment from 'moment-timezone';
|
||||
// @ts-ignore
|
||||
import observeResize from '../../lib/observe_resize';
|
||||
import {
|
||||
calculateInterval,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
tickFormatters,
|
||||
xaxisFormatterProvider,
|
||||
generateTicksProvider,
|
||||
} from '../../../../vis_type_timelion/public';
|
||||
import { _LEGACY_ as visTypeTimelion } from '../../../../vis_type_timelion/public';
|
||||
import { TimelionVisualizationDependencies } from '../../application';
|
||||
|
||||
const DEBOUNCE_DELAY = 50;
|
||||
|
@ -37,9 +31,9 @@ export function timechartFn(dependencies: TimelionVisualizationDependencies) {
|
|||
help: 'Draw a timeseries chart',
|
||||
render($scope: any, $elem: any) {
|
||||
const template = '<div class="chart-top-title"></div><div class="chart-canvas"></div>';
|
||||
const formatters = tickFormatters() as any;
|
||||
const getxAxisFormatter = xaxisFormatterProvider(uiSettings);
|
||||
const generateTicks = generateTicksProvider();
|
||||
const formatters = visTypeTimelion.tickFormatters() as any;
|
||||
const getxAxisFormatter = visTypeTimelion.xaxisFormatterProvider(uiSettings);
|
||||
const generateTicks = visTypeTimelion.generateTicksProvider();
|
||||
|
||||
// TODO: I wonder if we should supply our own moment that sets this every time?
|
||||
// could just use angular's injection to provide a moment service?
|
||||
|
@ -226,7 +220,7 @@ export function timechartFn(dependencies: TimelionVisualizationDependencies) {
|
|||
if (legendCaption) {
|
||||
legendCaption.text(
|
||||
moment(pos.x).format(
|
||||
_.get(dataset, '[0]._global.legend.timeFormat', DEFAULT_TIME_FORMAT)
|
||||
_.get(dataset, '[0]._global.legend.timeFormat', visTypeTimelion.DEFAULT_TIME_FORMAT)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -289,7 +283,7 @@ export function timechartFn(dependencies: TimelionVisualizationDependencies) {
|
|||
|
||||
// Get the X-axis tick format
|
||||
const time = timefilter.timefilter.getBounds() as any;
|
||||
const interval = calculateInterval(
|
||||
const interval = visTypeTimelion.calculateInterval(
|
||||
time.min.valueOf(),
|
||||
time.max.valueOf(),
|
||||
uiSettings.get('timelion:target_buckets') || 200,
|
||||
|
|
18
src/plugins/vis_type_timelion/common/constants.ts
Normal file
18
src/plugins/vis_type_timelion/common/constants.ts
Normal file
|
@ -0,0 +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 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 const UI_SETTINGS = {
|
||||
LEGACY_CHARTS_LIBRARY: 'timelion:legacyChartsLibrary',
|
||||
ES_TIMEFIELD: 'timelion:es.timefield',
|
||||
DEFAULT_INDEX: 'timelion:es.default_index',
|
||||
TARGET_BUCKETS: 'timelion:target_buckets',
|
||||
MAX_BUCKETS: 'timelion:max_buckets',
|
||||
MIN_INTERVAL: 'timelion:min_interval',
|
||||
GRAPHITE_URL: 'timelion:graphite.url',
|
||||
QUANDL_KEY: 'timelion:quandl.key',
|
||||
};
|
33
src/plugins/vis_type_timelion/common/vis_data.ts
Normal file
33
src/plugins/vis_type_timelion/common/vis_data.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 interface VisSeries {
|
||||
yaxis?: number;
|
||||
label: string;
|
||||
lines?: {
|
||||
show?: boolean;
|
||||
lineWidth?: number;
|
||||
fill?: number;
|
||||
steps?: number;
|
||||
};
|
||||
points?: {
|
||||
show?: boolean;
|
||||
symbol?: 'cross' | 'x' | 'circle' | 'square' | 'diamond' | 'plus' | 'triangle';
|
||||
fillColor?: string;
|
||||
fill?: number;
|
||||
radius?: number;
|
||||
lineWidth?: number;
|
||||
};
|
||||
bars: {
|
||||
lineWidth?: number;
|
||||
fill?: number;
|
||||
};
|
||||
color?: string;
|
||||
data: Array<Array<number | null>>;
|
||||
stack: boolean;
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
"kibanaVersion": "kibana",
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["visualizations", "data", "expressions"],
|
||||
"requiredPlugins": ["visualizations", "data", "expressions", "charts"],
|
||||
"requiredBundles": ["kibanaUtils", "kibanaReact", "visDefaultEditor"],
|
||||
"owner": {
|
||||
"name": "Kibana App",
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { AreaSeries, ScaleType, CurveType, AreaSeriesStyle, PointShape } from '@elastic/charts';
|
||||
import type { VisSeries } from '../../../common/vis_data';
|
||||
|
||||
interface AreaSeriesComponentProps {
|
||||
index: number;
|
||||
visData: VisSeries;
|
||||
groupId: string;
|
||||
}
|
||||
|
||||
const isShowLines = (lines: VisSeries['lines'], points: VisSeries['points']) =>
|
||||
lines?.show ? true : points?.show ? false : true;
|
||||
|
||||
const getAreaSeriesStyle = ({ color, lines, points }: AreaSeriesComponentProps['visData']) =>
|
||||
({
|
||||
line: {
|
||||
opacity: isShowLines(lines, points) ? 1 : 0,
|
||||
stroke: color,
|
||||
strokeWidth: lines?.lineWidth !== undefined ? Number(lines.lineWidth) : 3,
|
||||
visible: isShowLines(lines, points),
|
||||
},
|
||||
area: {
|
||||
fill: color,
|
||||
opacity: lines?.fill ?? 0,
|
||||
visible: lines?.show ?? points?.show ?? true,
|
||||
},
|
||||
point: {
|
||||
fill: points?.fillColor ?? color,
|
||||
opacity: points?.lineWidth !== undefined ? (points.fill || 1) * 10 : 10,
|
||||
radius: points?.radius ?? 3,
|
||||
stroke: color,
|
||||
strokeWidth: points?.lineWidth ?? 2,
|
||||
visible: points?.show ?? false,
|
||||
shape: points?.symbol === 'cross' ? PointShape.X : points?.symbol,
|
||||
},
|
||||
curve: lines?.steps ? CurveType.CURVE_STEP : CurveType.LINEAR,
|
||||
} as AreaSeriesStyle);
|
||||
|
||||
export const AreaSeriesComponent = ({ index, groupId, visData }: AreaSeriesComponentProps) => (
|
||||
<AreaSeries
|
||||
id={index + visData.label}
|
||||
groupId={groupId}
|
||||
name={visData.label}
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor={0}
|
||||
yAccessors={[1]}
|
||||
data={visData.data}
|
||||
sortIndex={index}
|
||||
color={visData.color}
|
||||
stackAccessors={visData.stack ? [0] : undefined}
|
||||
areaSeriesStyle={getAreaSeriesStyle(visData)}
|
||||
/>
|
||||
);
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 { BarSeries, ScaleType, BarSeriesStyle } from '@elastic/charts';
|
||||
import type { VisSeries } from '../../../common/vis_data';
|
||||
|
||||
interface BarSeriesComponentProps {
|
||||
index: number;
|
||||
visData: VisSeries;
|
||||
groupId: string;
|
||||
}
|
||||
|
||||
const getBarSeriesStyle = ({ color, bars }: BarSeriesComponentProps['visData']) => {
|
||||
let opacity = bars.fill ?? 1;
|
||||
|
||||
if (opacity < 0) {
|
||||
opacity = 0;
|
||||
} else if (opacity > 1) {
|
||||
opacity = 1;
|
||||
}
|
||||
|
||||
return {
|
||||
rectBorder: {
|
||||
stroke: color,
|
||||
strokeWidth: Math.max(1, bars.lineWidth ? Math.ceil(bars.lineWidth / 2) : 1),
|
||||
visible: true,
|
||||
},
|
||||
rect: {
|
||||
fill: color,
|
||||
opacity,
|
||||
widthPixel: 1,
|
||||
},
|
||||
} as BarSeriesStyle;
|
||||
};
|
||||
|
||||
export const BarSeriesComponent = ({ index, groupId, visData }: BarSeriesComponentProps) => (
|
||||
<BarSeries
|
||||
id={index + visData.label}
|
||||
groupId={groupId}
|
||||
name={visData.label}
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor={0}
|
||||
yAccessors={[1]}
|
||||
data={visData.data}
|
||||
sortIndex={index}
|
||||
enableHistogramMode={false}
|
||||
color={visData.color}
|
||||
stackAccessors={visData.stack ? [0] : undefined}
|
||||
barSeriesStyle={getBarSeriesStyle(visData)}
|
||||
/>
|
||||
);
|
|
@ -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 { BarSeriesComponent } from './bar';
|
||||
export { AreaSeriesComponent } from './area';
|
|
@ -1,60 +1,10 @@
|
|||
.timChart {
|
||||
.timelionChart {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// Custom Jquery FLOT / schema selectors
|
||||
// Cannot change at the moment
|
||||
|
||||
.chart-top-title {
|
||||
@include euiFontSizeXS;
|
||||
flex: 0;
|
||||
text-align: center;
|
||||
font-weight: $euiFontWeightBold;
|
||||
}
|
||||
|
||||
.chart-canvas {
|
||||
min-width: 100%;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.legendLabel {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.legendColorBox {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ngLegendValue {
|
||||
color: $euiTextColor;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.ngLegendValueNumber {
|
||||
margin-left: $euiSizeXS;
|
||||
margin-right: $euiSizeXS;
|
||||
font-weight: $euiFontWeightBold;
|
||||
}
|
||||
|
||||
.flot-tick-label {
|
||||
font-size: $euiFontSizeXS;
|
||||
color: $euiColorDarkShade;
|
||||
}
|
||||
}
|
||||
|
||||
.timChart__legendCaption {
|
||||
color: $euiTextColor;
|
||||
white-space: nowrap;
|
||||
font-weight: $euiFontWeightBold;
|
||||
.timelionChart__topTitle {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -6,422 +6,228 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import $ from 'jquery';
|
||||
import moment from 'moment-timezone';
|
||||
import { debounce, compact, get, each, cloneDeep, last, map } from 'lodash';
|
||||
import { useResizeObserver } from '@elastic/eui';
|
||||
import React, { useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { compact, last, map } from 'lodash';
|
||||
import {
|
||||
Chart,
|
||||
Settings,
|
||||
Position,
|
||||
Axis,
|
||||
TooltipType,
|
||||
PointerEvent,
|
||||
LegendPositionConfig,
|
||||
LayoutDirection,
|
||||
} from '@elastic/charts';
|
||||
import { EuiTitle } from '@elastic/eui';
|
||||
|
||||
import { IInterpreterRenderHandlers } from 'src/plugins/expressions';
|
||||
import { useKibana } from '../../../kibana_react/public';
|
||||
import { DEFAULT_TIME_FORMAT } from '../../common/lib';
|
||||
|
||||
import { AreaSeriesComponent, BarSeriesComponent } from './series';
|
||||
|
||||
import {
|
||||
buildSeriesData,
|
||||
buildOptions,
|
||||
SERIES_ID_ATTR,
|
||||
colors,
|
||||
Axis,
|
||||
ACTIVE_CURSOR,
|
||||
eventBus,
|
||||
extractAllYAxis,
|
||||
withStaticPadding,
|
||||
createTickFormat,
|
||||
validateLegendPositionValue,
|
||||
MAIN_GROUP_ID,
|
||||
} from '../helpers/panel_utils';
|
||||
|
||||
import { Series, Sheet } from '../helpers/timelion_request_handler';
|
||||
import { tickFormatters } from '../helpers/tick_formatters';
|
||||
import { generateTicksProvider } from '../helpers/tick_generator';
|
||||
import { TimelionVisDependencies } from '../plugin';
|
||||
import { colors } from '../helpers/chart_constants';
|
||||
import { activeCursor$ } from '../helpers/active_cursor';
|
||||
|
||||
import type { Sheet } from '../helpers/timelion_request_handler';
|
||||
import type { IInterpreterRenderHandlers } from '../../../expressions';
|
||||
import type { TimelionVisDependencies } from '../plugin';
|
||||
import type { RangeFilterParams } from '../../../data/public';
|
||||
import type { Series } from '../helpers/timelion_request_handler';
|
||||
|
||||
import './timelion_vis.scss';
|
||||
|
||||
interface CrosshairPlot extends jquery.flot.plot {
|
||||
setCrosshair: (pos: Position) => void;
|
||||
clearCrosshair: () => void;
|
||||
}
|
||||
|
||||
interface TimelionVisComponentProps {
|
||||
fireEvent: IInterpreterRenderHandlers['event'];
|
||||
interval: string;
|
||||
seriesList: Sheet;
|
||||
onBrushEvent: (rangeFilterParams: RangeFilterParams) => void;
|
||||
renderComplete: IInterpreterRenderHandlers['done'];
|
||||
}
|
||||
|
||||
interface Position {
|
||||
x: number;
|
||||
x1: number;
|
||||
y: number;
|
||||
y1: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
}
|
||||
const DefaultYAxis = () => (
|
||||
<Axis
|
||||
id="left"
|
||||
domain={withStaticPadding({
|
||||
fit: false,
|
||||
})}
|
||||
position={Position.Left}
|
||||
groupId={`${MAIN_GROUP_ID}`}
|
||||
/>
|
||||
);
|
||||
|
||||
interface Range {
|
||||
to: number;
|
||||
from: number;
|
||||
}
|
||||
const renderYAxis = (series: Series[]) => {
|
||||
const yAxisOptions = extractAllYAxis(series);
|
||||
|
||||
interface Ranges {
|
||||
xaxis: Range;
|
||||
yaxis: Range;
|
||||
}
|
||||
const yAxis = yAxisOptions.map((option, index) => (
|
||||
<Axis
|
||||
groupId={option.groupId}
|
||||
key={index}
|
||||
id={option.id!}
|
||||
title={option.title}
|
||||
position={option.position}
|
||||
tickFormat={option.tickFormat}
|
||||
gridLine={{
|
||||
visible: !index,
|
||||
}}
|
||||
domain={option.domain}
|
||||
/>
|
||||
));
|
||||
|
||||
const DEBOUNCE_DELAY = 50;
|
||||
// ensure legend is the same height with or without a caption so legend items do not move around
|
||||
const emptyCaption = '<br>';
|
||||
return yAxis.length ? yAxis : <DefaultYAxis />;
|
||||
};
|
||||
|
||||
function TimelionVisComponent({
|
||||
const TimelionVisComponent = ({
|
||||
interval,
|
||||
seriesList,
|
||||
renderComplete,
|
||||
fireEvent,
|
||||
}: TimelionVisComponentProps) {
|
||||
onBrushEvent,
|
||||
}: TimelionVisComponentProps) => {
|
||||
const kibana = useKibana<TimelionVisDependencies>();
|
||||
const [chart, setChart] = useState(() => cloneDeep(seriesList.list));
|
||||
const [canvasElem, setCanvasElem] = useState<HTMLDivElement>();
|
||||
const [chartElem, setChartElem] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const [originalColorMap, setOriginalColorMap] = useState(() => new Map<Series, string>());
|
||||
|
||||
const [highlightedSeries, setHighlightedSeries] = useState<number | null>(null);
|
||||
const [focusedSeries, setFocusedSeries] = useState<number | null>();
|
||||
const [plot, setPlot] = useState<jquery.flot.plot>();
|
||||
|
||||
// Used to toggle the series, and for displaying values on hover
|
||||
const [legendValueNumbers, setLegendValueNumbers] = useState<JQuery<HTMLElement>>();
|
||||
const [legendCaption, setLegendCaption] = useState<JQuery<HTMLElement>>();
|
||||
|
||||
const canvasRef = useCallback((node: HTMLDivElement | null) => {
|
||||
if (node !== null) {
|
||||
setCanvasElem(node);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const elementRef = useCallback((node: HTMLDivElement | null) => {
|
||||
if (node !== null) {
|
||||
setChartElem(node);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (chartElem) {
|
||||
$(chartElem).off('plotselected').off('plothover').off('mouseleave');
|
||||
}
|
||||
},
|
||||
[chartElem]
|
||||
);
|
||||
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
const highlightSeries = useCallback(
|
||||
debounce(({ currentTarget }: JQuery.TriggeredEvent) => {
|
||||
const id = Number(currentTarget.getAttribute(SERIES_ID_ATTR));
|
||||
if (highlightedSeries === id) {
|
||||
return;
|
||||
}
|
||||
|
||||
setHighlightedSeries(id);
|
||||
setChart((chartState) =>
|
||||
chartState.map((series: Series, seriesIndex: number) => {
|
||||
series.color =
|
||||
seriesIndex === id
|
||||
? originalColorMap.get(series) // color it like it was
|
||||
: 'rgba(128,128,128,0.1)'; // mark as grey
|
||||
|
||||
return series;
|
||||
})
|
||||
);
|
||||
}, DEBOUNCE_DELAY),
|
||||
[originalColorMap, highlightedSeries]
|
||||
);
|
||||
|
||||
const focusSeries = useCallback(
|
||||
(event: JQuery.TriggeredEvent) => {
|
||||
const id = Number(event.currentTarget.getAttribute(SERIES_ID_ATTR));
|
||||
setFocusedSeries(id);
|
||||
highlightSeries(event);
|
||||
},
|
||||
[highlightSeries]
|
||||
);
|
||||
|
||||
const toggleSeries = useCallback(({ currentTarget }: JQuery.TriggeredEvent) => {
|
||||
const id = Number(currentTarget.getAttribute(SERIES_ID_ATTR));
|
||||
|
||||
setChart((chartState) =>
|
||||
chartState.map((series: Series, seriesIndex: number) => {
|
||||
if (seriesIndex === id) {
|
||||
series._hide = !series._hide;
|
||||
}
|
||||
return series;
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
const updateCaption = useCallback(
|
||||
(plotData: any) => {
|
||||
if (canvasElem && get(plotData, '[0]._global.legend.showTime', true)) {
|
||||
const caption = $('<caption class="timChart__legendCaption"></caption>');
|
||||
caption.html(emptyCaption);
|
||||
setLegendCaption(caption);
|
||||
|
||||
const canvasNode = $(canvasElem);
|
||||
canvasNode.find('div.legend table').append(caption);
|
||||
setLegendValueNumbers(canvasNode.find('.ngLegendValueNumber'));
|
||||
|
||||
const legend = $(canvasElem).find('.ngLegendValue');
|
||||
if (legend) {
|
||||
legend.click(toggleSeries);
|
||||
legend.focus(focusSeries);
|
||||
legend.mouseover(highlightSeries);
|
||||
}
|
||||
|
||||
// legend has been re-created. Apply focus on legend element when previously set
|
||||
if (focusedSeries || focusedSeries === 0) {
|
||||
canvasNode.find('div.legend table .legendLabel>span').get(focusedSeries).focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
[focusedSeries, canvasElem, toggleSeries, focusSeries, highlightSeries]
|
||||
);
|
||||
|
||||
const updatePlot = useCallback(
|
||||
(chartValue: Series[], grid?: boolean) => {
|
||||
if (canvasElem && canvasElem.clientWidth > 0 && canvasElem.clientHeight > 0) {
|
||||
const options = buildOptions(
|
||||
interval,
|
||||
kibana.services.timefilter,
|
||||
kibana.services.uiSettings,
|
||||
chartElem?.clientWidth,
|
||||
grid
|
||||
);
|
||||
const updatedSeries = buildSeriesData(chartValue, options);
|
||||
|
||||
if (options.yaxes) {
|
||||
options.yaxes.forEach((yaxis: Axis) => {
|
||||
if (yaxis && yaxis.units) {
|
||||
const formatters = tickFormatters();
|
||||
yaxis.tickFormatter = formatters[yaxis.units.type as keyof typeof formatters];
|
||||
const byteModes = ['bytes', 'bytes/s'];
|
||||
if (byteModes.includes(yaxis.units.type)) {
|
||||
yaxis.tickGenerator = generateTicksProvider();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const newPlot = $.plot($(canvasElem), updatedSeries, options);
|
||||
setPlot(newPlot);
|
||||
renderComplete();
|
||||
|
||||
updateCaption(newPlot.getData());
|
||||
}
|
||||
},
|
||||
[canvasElem, chartElem?.clientWidth, renderComplete, kibana.services, interval, updateCaption]
|
||||
);
|
||||
|
||||
const dimensions = useResizeObserver(chartElem);
|
||||
const chartRef = useRef<Chart>(null);
|
||||
const chart = seriesList.list;
|
||||
|
||||
useEffect(() => {
|
||||
updatePlot(chart, seriesList.render && seriesList.render.grid);
|
||||
}, [chart, updatePlot, seriesList.render, dimensions]);
|
||||
|
||||
useEffect(() => {
|
||||
const colorsSet: Array<[Series, string]> = [];
|
||||
const newChart = seriesList.list.map((series: Series, seriesIndex: number) => {
|
||||
const newSeries = { ...series };
|
||||
if (!newSeries.color) {
|
||||
const colorIndex = seriesIndex % colors.length;
|
||||
newSeries.color = colors[colorIndex];
|
||||
}
|
||||
colorsSet.push([newSeries, newSeries.color]);
|
||||
return newSeries;
|
||||
const subscription = activeCursor$.subscribe((cursor: PointerEvent) => {
|
||||
chartRef.current?.dispatchExternalPointerEvent(cursor);
|
||||
});
|
||||
setChart(newChart);
|
||||
setOriginalColorMap(new Map(colorsSet));
|
||||
}, [seriesList.list]);
|
||||
|
||||
const unhighlightSeries = useCallback(() => {
|
||||
if (highlightedSeries === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setHighlightedSeries(null);
|
||||
setFocusedSeries(null);
|
||||
|
||||
setChart((chartState) =>
|
||||
chartState.map((series: Series) => {
|
||||
series.color = originalColorMap.get(series); // reset the colors
|
||||
return series;
|
||||
})
|
||||
);
|
||||
}, [originalColorMap, highlightedSeries]);
|
||||
|
||||
// Shamelessly borrowed from the flotCrosshairs example
|
||||
const setLegendNumbers = useCallback(
|
||||
(pos: Position) => {
|
||||
unhighlightSeries();
|
||||
|
||||
const axes = plot!.getAxes();
|
||||
if (pos.x < axes.xaxis.min! || pos.x > axes.xaxis.max!) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataset = plot!.getData();
|
||||
if (legendCaption) {
|
||||
legendCaption.text(
|
||||
moment(pos.x).format(get(dataset, '[0]._global.legend.timeFormat', DEFAULT_TIME_FORMAT))
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < dataset.length; ++i) {
|
||||
const series = dataset[i];
|
||||
const useNearestPoint = series.lines!.show && !series.lines!.steps;
|
||||
const precision = get(series, '_meta.precision', 2);
|
||||
|
||||
// We're setting this flag on top on the series object belonging to the flot library, so we're simply casting here.
|
||||
if ((series as { _hide?: boolean })._hide) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentPoint = series.data.find((point: [number, number], index: number) => {
|
||||
if (index + 1 === series.data.length) {
|
||||
return true;
|
||||
}
|
||||
if (useNearestPoint) {
|
||||
return pos.x - point[0] < series.data[index + 1][0] - pos.x;
|
||||
} else {
|
||||
return pos.x < series.data[index + 1][0];
|
||||
}
|
||||
});
|
||||
|
||||
const y = currentPoint[1];
|
||||
|
||||
if (legendValueNumbers) {
|
||||
if (y == null) {
|
||||
legendValueNumbers.eq(i).empty();
|
||||
} else {
|
||||
let label = y.toFixed(precision);
|
||||
const formatter = ((series.yaxis as unknown) as Axis).tickFormatter;
|
||||
if (formatter) {
|
||||
label = formatter(Number(label), (series.yaxis as unknown) as Axis);
|
||||
}
|
||||
legendValueNumbers.eq(i).text(`(${label})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[plot, legendValueNumbers, unhighlightSeries, legendCaption]
|
||||
);
|
||||
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
const debouncedSetLegendNumbers = useCallback(
|
||||
debounce(setLegendNumbers, DEBOUNCE_DELAY, {
|
||||
maxWait: DEBOUNCE_DELAY,
|
||||
leading: true,
|
||||
trailing: false,
|
||||
}),
|
||||
[setLegendNumbers]
|
||||
);
|
||||
|
||||
const clearLegendNumbers = useCallback(() => {
|
||||
if (legendCaption) {
|
||||
legendCaption.html(emptyCaption);
|
||||
}
|
||||
each(legendValueNumbers!, (num: Node) => {
|
||||
$(num).empty();
|
||||
});
|
||||
}, [legendCaption, legendValueNumbers]);
|
||||
|
||||
const plotHover = useCallback(
|
||||
(pos: Position) => {
|
||||
(plot as CrosshairPlot).setCrosshair(pos);
|
||||
debouncedSetLegendNumbers(pos);
|
||||
},
|
||||
[plot, debouncedSetLegendNumbers]
|
||||
);
|
||||
|
||||
const plotHoverHandler = useCallback(
|
||||
(event: JQuery.TriggeredEvent, pos: Position) => {
|
||||
if (!plot) {
|
||||
return;
|
||||
}
|
||||
plotHover(pos);
|
||||
eventBus.trigger(ACTIVE_CURSOR, [event, pos]);
|
||||
},
|
||||
[plot, plotHover]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const updateCursor = (_: any, event: JQuery.TriggeredEvent, pos: Position) => {
|
||||
if (!plot) {
|
||||
return;
|
||||
}
|
||||
plotHover(pos);
|
||||
};
|
||||
|
||||
eventBus.on(ACTIVE_CURSOR, updateCursor);
|
||||
|
||||
return () => {
|
||||
eventBus.off(ACTIVE_CURSOR, updateCursor);
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [plot, plotHover]);
|
||||
}, []);
|
||||
|
||||
const mouseLeaveHandler = useCallback(() => {
|
||||
if (!plot) {
|
||||
return;
|
||||
}
|
||||
(plot as CrosshairPlot).clearCrosshair();
|
||||
clearLegendNumbers();
|
||||
}, [plot, clearLegendNumbers]);
|
||||
const handleCursorUpdate = useCallback((cursor: PointerEvent) => {
|
||||
activeCursor$.next(cursor);
|
||||
}, []);
|
||||
|
||||
const plotSelectedHandler = useCallback(
|
||||
(event: JQuery.TriggeredEvent, ranges: Ranges) => {
|
||||
fireEvent({
|
||||
name: 'applyFilter',
|
||||
data: {
|
||||
timeFieldName: '*',
|
||||
filters: [
|
||||
{
|
||||
range: {
|
||||
'*': {
|
||||
gte: ranges.xaxis.from,
|
||||
lte: ranges.xaxis.to,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
const brushEndListener = useCallback(
|
||||
({ x }) => {
|
||||
if (!x) {
|
||||
return;
|
||||
}
|
||||
|
||||
onBrushEvent({
|
||||
gte: x[0],
|
||||
lte: x[1],
|
||||
});
|
||||
},
|
||||
[fireEvent]
|
||||
[onBrushEvent]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (chartElem) {
|
||||
$(chartElem).off('plotselected').on('plotselected', plotSelectedHandler);
|
||||
}
|
||||
}, [chartElem, plotSelectedHandler]);
|
||||
|
||||
useEffect(() => {
|
||||
if (chartElem) {
|
||||
$(chartElem).off('mouseleave').on('mouseleave', mouseLeaveHandler);
|
||||
}
|
||||
}, [chartElem, mouseLeaveHandler]);
|
||||
|
||||
useEffect(() => {
|
||||
if (chartElem) {
|
||||
$(chartElem).off('plothover').on('plothover', plotHoverHandler);
|
||||
}
|
||||
}, [chartElem, plotHoverHandler]);
|
||||
const onRenderChange = useCallback(
|
||||
(isRendered: boolean) => {
|
||||
if (isRendered) {
|
||||
renderComplete();
|
||||
}
|
||||
},
|
||||
[renderComplete]
|
||||
);
|
||||
|
||||
const title: string = useMemo(() => last(compact(map(seriesList.list, '_title'))) || '', [
|
||||
seriesList.list,
|
||||
]);
|
||||
|
||||
const tickFormat = useMemo(
|
||||
() => createTickFormat(interval, kibana.services.timefilter, kibana.services.uiSettings),
|
||||
[interval, kibana.services.timefilter, kibana.services.uiSettings]
|
||||
);
|
||||
|
||||
const legend = useMemo(() => {
|
||||
const legendPosition: LegendPositionConfig = {
|
||||
floating: true,
|
||||
floatingColumns: 1,
|
||||
vAlign: Position.Top,
|
||||
hAlign: Position.Left,
|
||||
direction: LayoutDirection.Vertical,
|
||||
};
|
||||
let showLegend = true;
|
||||
|
||||
chart.forEach((series) => {
|
||||
if (series._global?.legend) {
|
||||
const { show = true, position, noColumns = legendPosition.floatingColumns } =
|
||||
series._global?.legend ?? {};
|
||||
|
||||
if (validateLegendPositionValue(position)) {
|
||||
const [vAlign, hAlign] = position.split('');
|
||||
|
||||
legendPosition.vAlign = vAlign === 'n' ? Position.Top : Position.Bottom;
|
||||
legendPosition.hAlign = hAlign === 'e' ? Position.Right : Position.Left;
|
||||
}
|
||||
|
||||
if (!show) {
|
||||
showLegend = false;
|
||||
}
|
||||
|
||||
if (noColumns !== undefined) {
|
||||
legendPosition.floatingColumns = noColumns;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { legendPosition, showLegend };
|
||||
}, [chart]);
|
||||
|
||||
return (
|
||||
<div ref={elementRef} className="timChart">
|
||||
<div className="chart-top-title">{title}</div>
|
||||
<div ref={canvasRef} className="chart-canvas" />
|
||||
<div className="timelionChart">
|
||||
{title && (
|
||||
<EuiTitle className="timelionChart__topTitle" size="xxxs">
|
||||
<h4>{title}</h4>
|
||||
</EuiTitle>
|
||||
)}
|
||||
<Chart ref={chartRef} renderer="canvas" size={{ width: '100%' }}>
|
||||
<Settings
|
||||
onBrushEnd={brushEndListener}
|
||||
showLegend={legend.showLegend}
|
||||
showLegendExtra={true}
|
||||
legendPosition={legend.legendPosition}
|
||||
onRenderChange={onRenderChange}
|
||||
onPointerUpdate={handleCursorUpdate}
|
||||
theme={kibana.services.chartTheme.useChartsTheme()}
|
||||
baseTheme={kibana.services.chartTheme.useChartsBaseTheme()}
|
||||
tooltip={{
|
||||
snap: true,
|
||||
headerFormatter: ({ value }) => tickFormat(value),
|
||||
type: TooltipType.VerticalCursor,
|
||||
}}
|
||||
externalPointerEvents={{ tooltip: { visible: false } }}
|
||||
/>
|
||||
|
||||
<Axis
|
||||
id="bottom"
|
||||
position={Position.Bottom}
|
||||
showOverlappingTicks
|
||||
tickFormat={tickFormat}
|
||||
gridLine={{ visible: false }}
|
||||
/>
|
||||
|
||||
{renderYAxis(chart)}
|
||||
|
||||
{chart.map((data, index) => {
|
||||
const visData = { ...data };
|
||||
const SeriesComponent = data.bars ? BarSeriesComponent : AreaSeriesComponent;
|
||||
|
||||
if (!visData.color) {
|
||||
visData.color = colors[index % colors.length];
|
||||
}
|
||||
return (
|
||||
<SeriesComponent
|
||||
key={`${index}-${visData.label}`}
|
||||
visData={visData}
|
||||
index={chart.length - index}
|
||||
groupId={`${visData.yaxis ? visData.yaxis : MAIN_GROUP_ID}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Chart>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// default export required for React.Lazy
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 { Subject } from 'rxjs';
|
||||
import { PointerEvent } from '@elastic/charts';
|
||||
|
||||
export const activeCursor$ = new Subject<PointerEvent>();
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 const colors = [
|
||||
'#01A4A4',
|
||||
'#C66',
|
||||
'#D0D102',
|
||||
'#616161',
|
||||
'#00A1CB',
|
||||
'#32742C',
|
||||
'#F18D05',
|
||||
'#113F8C',
|
||||
'#61AE24',
|
||||
'#D70060',
|
||||
];
|
|
@ -6,18 +6,18 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { cloneDeep, defaults, mergeWith, compact } from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import moment, { Moment } from 'moment-timezone';
|
||||
|
||||
import { TimefilterContract } from 'src/plugins/data/public';
|
||||
import { IUiSettingsClient } from 'kibana/public';
|
||||
import moment from 'moment-timezone';
|
||||
import { Position, AxisSpec } from '@elastic/charts';
|
||||
import type { TimefilterContract } from 'src/plugins/data/public';
|
||||
import type { IUiSettingsClient } from 'kibana/public';
|
||||
|
||||
import { calculateInterval } from '../../common/lib';
|
||||
import { xaxisFormatterProvider } from './xaxis_formatter';
|
||||
import { Series } from './timelion_request_handler';
|
||||
import { tickFormatters } from './tick_formatters';
|
||||
|
||||
export interface Axis {
|
||||
import type { Series } from './timelion_request_handler';
|
||||
|
||||
export interface IAxis {
|
||||
delta?: number;
|
||||
max?: number;
|
||||
min?: number;
|
||||
|
@ -30,87 +30,26 @@ export interface Axis {
|
|||
tickLength: number;
|
||||
timezone: string;
|
||||
tickDecimals?: number;
|
||||
tickFormatter: ((val: number) => string) | ((val: number, axis: Axis) => string);
|
||||
tickGenerator?(axis: Axis): number[];
|
||||
units?: { type: string };
|
||||
tickFormatter: (val: number) => string;
|
||||
tickGenerator?(axis: IAxis): number[];
|
||||
units?: { type: string; prefix: string; suffix: string };
|
||||
domain?: {
|
||||
min?: number;
|
||||
max?: number;
|
||||
};
|
||||
position?: Position;
|
||||
axisLabel?: string;
|
||||
}
|
||||
|
||||
interface TimeRangeBounds {
|
||||
min: Moment | undefined;
|
||||
max: Moment | undefined;
|
||||
}
|
||||
export const validateLegendPositionValue = (position: string) => /^(n|s)(e|w)$/s.test(position);
|
||||
|
||||
export const ACTIVE_CURSOR = 'ACTIVE_CURSOR_TIMELION';
|
||||
export const eventBus = $({});
|
||||
|
||||
const colors = [
|
||||
'#01A4A4',
|
||||
'#C66',
|
||||
'#D0D102',
|
||||
'#616161',
|
||||
'#00A1CB',
|
||||
'#32742C',
|
||||
'#F18D05',
|
||||
'#113F8C',
|
||||
'#61AE24',
|
||||
'#D70060',
|
||||
];
|
||||
|
||||
const SERIES_ID_ATTR = 'data-series-id';
|
||||
|
||||
function buildSeriesData(chart: Series[], options: jquery.flot.plotOptions) {
|
||||
const seriesData = chart.map((series: Series, seriesIndex: number) => {
|
||||
const newSeries: Series = cloneDeep(
|
||||
defaults(series, {
|
||||
shadowSize: 0,
|
||||
lines: {
|
||||
lineWidth: 3,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
newSeries._id = seriesIndex;
|
||||
|
||||
if (series.color) {
|
||||
const span = document.createElement('span');
|
||||
span.style.color = series.color;
|
||||
newSeries.color = span.style.color;
|
||||
}
|
||||
|
||||
if (series._hide) {
|
||||
newSeries.data = [];
|
||||
newSeries.stack = false;
|
||||
newSeries.label = `(hidden) ${series.label}`;
|
||||
}
|
||||
|
||||
if (series._global) {
|
||||
mergeWith(options, series._global, (objVal, srcVal) => {
|
||||
// This is kind of gross, it means that you can't replace a global value with a null
|
||||
// best you can do is an empty string. Deal with it.
|
||||
if (objVal == null) {
|
||||
return srcVal;
|
||||
}
|
||||
if (srcVal == null) {
|
||||
return objVal;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return newSeries;
|
||||
});
|
||||
|
||||
return compact(seriesData);
|
||||
}
|
||||
|
||||
function buildOptions(
|
||||
export const createTickFormat = (
|
||||
intervalValue: string,
|
||||
timefilter: TimefilterContract,
|
||||
uiSettings: IUiSettingsClient,
|
||||
clientWidth = 0,
|
||||
showGrid?: boolean
|
||||
) {
|
||||
uiSettings: IUiSettingsClient
|
||||
) => {
|
||||
// Get the X-axis tick format
|
||||
const time: TimeRangeBounds = timefilter.getBounds();
|
||||
const time = timefilter.getBounds();
|
||||
const interval = calculateInterval(
|
||||
(time.min && time.min.valueOf()) || 0,
|
||||
(time.max && time.max.valueOf()) || 0,
|
||||
|
@ -120,61 +59,75 @@ function buildOptions(
|
|||
);
|
||||
const format = xaxisFormatterProvider(uiSettings)(interval);
|
||||
|
||||
const tickLetterWidth = 7;
|
||||
const tickPadding = 45;
|
||||
return (val: number) => moment(val).format(format);
|
||||
};
|
||||
|
||||
const options = {
|
||||
xaxis: {
|
||||
mode: 'time',
|
||||
tickLength: 5,
|
||||
timezone: 'browser',
|
||||
// Calculate how many ticks can fit on the axis
|
||||
ticks: Math.floor(clientWidth / (format.length * tickLetterWidth + tickPadding)),
|
||||
// Use moment to format ticks so we get timezone correction
|
||||
tickFormatter: (val: number) => moment(val).format(format),
|
||||
},
|
||||
selection: {
|
||||
mode: 'x',
|
||||
color: '#ccc',
|
||||
},
|
||||
crosshair: {
|
||||
mode: 'x',
|
||||
color: '#C66',
|
||||
lineWidth: 2,
|
||||
},
|
||||
colors,
|
||||
grid: {
|
||||
show: showGrid,
|
||||
borderWidth: 0,
|
||||
borderColor: null,
|
||||
margin: 10,
|
||||
hoverable: true,
|
||||
autoHighlight: false,
|
||||
},
|
||||
legend: {
|
||||
backgroundColor: 'rgb(255,255,255,0)',
|
||||
position: 'nw',
|
||||
labelBoxBorderColor: 'rgb(255,255,255,0)',
|
||||
labelFormatter(label: string, series: { _id: number }) {
|
||||
const wrapperSpan = document.createElement('span');
|
||||
const labelSpan = document.createElement('span');
|
||||
const numberSpan = document.createElement('span');
|
||||
/** While we support 2 versions of the timeline, we need this adapter. **/
|
||||
export const MAIN_GROUP_ID = 1;
|
||||
|
||||
wrapperSpan.setAttribute('class', 'ngLegendValue');
|
||||
wrapperSpan.setAttribute(SERIES_ID_ATTR, `${series._id}`);
|
||||
export const withStaticPadding = (domain: AxisSpec['domain']): AxisSpec['domain'] =>
|
||||
(({
|
||||
...domain,
|
||||
padding: 50,
|
||||
paddingUnit: 'pixel',
|
||||
} as unknown) as AxisSpec['domain']);
|
||||
|
||||
labelSpan.appendChild(document.createTextNode(label));
|
||||
numberSpan.setAttribute('class', 'ngLegendValueNumber');
|
||||
const adaptYaxisParams = (yaxis: IAxis) => {
|
||||
const y = { ...yaxis };
|
||||
|
||||
wrapperSpan.appendChild(labelSpan);
|
||||
wrapperSpan.appendChild(numberSpan);
|
||||
if (y.units) {
|
||||
const formatters = tickFormatters(y);
|
||||
y.tickFormatter = formatters[y.units.type as keyof typeof formatters];
|
||||
} else if (yaxis.tickDecimals) {
|
||||
y.tickFormatter = (val: number) => val.toFixed(yaxis.tickDecimals);
|
||||
}
|
||||
|
||||
return wrapperSpan.outerHTML;
|
||||
},
|
||||
},
|
||||
} as jquery.flot.plotOptions & { yaxes?: Axis[] };
|
||||
return {
|
||||
title: y.axisLabel,
|
||||
position: y.position,
|
||||
tickFormat: y.tickFormatter,
|
||||
domain: withStaticPadding({
|
||||
fit: y.min === undefined && y.max === undefined,
|
||||
min: y.min,
|
||||
max: y.max,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
return options;
|
||||
}
|
||||
const extractYAxisForSeries = (series: Series) => {
|
||||
const yaxis = (series._global?.yaxes ?? []).reduce(
|
||||
(acc: IAxis, item: IAxis) => ({
|
||||
...acc,
|
||||
...item,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
export { buildSeriesData, buildOptions, SERIES_ID_ATTR, colors };
|
||||
if (Object.keys(yaxis).length) {
|
||||
return adaptYaxisParams(yaxis);
|
||||
}
|
||||
};
|
||||
|
||||
export const extractAllYAxis = (series: Series[]) => {
|
||||
return series.reduce((acc, data, index) => {
|
||||
const yaxis = extractYAxisForSeries(data);
|
||||
const groupId = `${data.yaxis ? data.yaxis : MAIN_GROUP_ID}`;
|
||||
|
||||
if (acc.every((axis) => axis.groupId !== groupId)) {
|
||||
acc.push({
|
||||
groupId,
|
||||
domain: withStaticPadding({
|
||||
fit: false,
|
||||
}),
|
||||
id: (yaxis?.position || Position.Left) + index,
|
||||
position: Position.Left,
|
||||
...yaxis,
|
||||
});
|
||||
} else if (yaxis) {
|
||||
const axisOptionIndex = acc.findIndex((axis) => axis.groupId === groupId);
|
||||
acc[axisOptionIndex] = { ...acc[axisOptionIndex], ...yaxis };
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as Array<Partial<AxisSpec>>);
|
||||
};
|
||||
|
|
|
@ -7,25 +7,26 @@
|
|||
*/
|
||||
|
||||
import { tickFormatters } from './tick_formatters';
|
||||
import type { IAxis } from './panel_utils';
|
||||
|
||||
describe('Tick Formatters', function () {
|
||||
describe('Tick Formatters', () => {
|
||||
let formatters: any;
|
||||
|
||||
beforeEach(function () {
|
||||
formatters = tickFormatters();
|
||||
formatters = tickFormatters({} as IAxis);
|
||||
});
|
||||
|
||||
describe('Bits mode', function () {
|
||||
describe('Bits mode', () => {
|
||||
let bitFormatter: any;
|
||||
beforeEach(function () {
|
||||
bitFormatter = formatters.bits;
|
||||
});
|
||||
|
||||
it('is a function', function () {
|
||||
it('is a function', () => {
|
||||
expect(bitFormatter).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it('formats with b/kb/mb/gb', function () {
|
||||
it('formats with b/kb/mb/gb', () => {
|
||||
expect(bitFormatter(7)).toEqual('7b');
|
||||
expect(bitFormatter(4 * 1000)).toEqual('4kb');
|
||||
expect(bitFormatter(4.1 * 1000 * 1000)).toEqual('4.1mb');
|
||||
|
@ -40,24 +41,24 @@ describe('Tick Formatters', function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Bits/s mode', function () {
|
||||
describe('Bits/s mode', () => {
|
||||
let bitsFormatter: any;
|
||||
beforeEach(function () {
|
||||
bitsFormatter = formatters['bits/s'];
|
||||
});
|
||||
|
||||
it('is a function', function () {
|
||||
it('is a function', () => {
|
||||
expect(bitsFormatter).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it('formats with b/kb/mb/gb', function () {
|
||||
it('formats with b/kb/mb/gb', () => {
|
||||
expect(bitsFormatter(7)).toEqual('7b/s');
|
||||
expect(bitsFormatter(4 * 1000)).toEqual('4kb/s');
|
||||
expect(bitsFormatter(4.1 * 1000 * 1000)).toEqual('4.1mb/s');
|
||||
expect(bitsFormatter(3 * 1000 * 1000 * 1000)).toEqual('3gb/s');
|
||||
});
|
||||
|
||||
it('formats negative values with b/kb/mb/gb', function () {
|
||||
it('formats negative values with b/kb/mb/gb', () => {
|
||||
expect(bitsFormatter(-7)).toEqual('-7b/s');
|
||||
expect(bitsFormatter(-4 * 1000)).toEqual('-4kb/s');
|
||||
expect(bitsFormatter(-4.1 * 1000 * 1000)).toEqual('-4.1mb/s');
|
||||
|
@ -65,24 +66,24 @@ describe('Tick Formatters', function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Bytes mode', function () {
|
||||
describe('Bytes mode', () => {
|
||||
let byteFormatter: any;
|
||||
beforeEach(function () {
|
||||
byteFormatter = formatters.bytes;
|
||||
});
|
||||
|
||||
it('is a function', function () {
|
||||
it('is a function', () => {
|
||||
expect(byteFormatter).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it('formats with B/KB/MB/GB', function () {
|
||||
it('formats with B/KB/MB/GB', () => {
|
||||
expect(byteFormatter(10)).toEqual('10B');
|
||||
expect(byteFormatter(10 * 1024)).toEqual('10KB');
|
||||
expect(byteFormatter(10.2 * 1024 * 1024)).toEqual('10.2MB');
|
||||
expect(byteFormatter(3 * 1024 * 1024 * 1024)).toEqual('3GB');
|
||||
});
|
||||
|
||||
it('formats negative values with B/KB/MB/GB', function () {
|
||||
it('formats negative values with B/KB/MB/GB', () => {
|
||||
expect(byteFormatter(-10)).toEqual('-10B');
|
||||
expect(byteFormatter(-10 * 1024)).toEqual('-10KB');
|
||||
expect(byteFormatter(-10.2 * 1024 * 1024)).toEqual('-10.2MB');
|
||||
|
@ -90,24 +91,24 @@ describe('Tick Formatters', function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Bytes/s mode', function () {
|
||||
describe('Bytes/s mode', () => {
|
||||
let bytesFormatter: any;
|
||||
beforeEach(function () {
|
||||
bytesFormatter = formatters['bytes/s'];
|
||||
});
|
||||
|
||||
it('is a function', function () {
|
||||
it('is a function', () => {
|
||||
expect(bytesFormatter).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it('formats with B/KB/MB/GB', function () {
|
||||
it('formats with B/KB/MB/GB', () => {
|
||||
expect(bytesFormatter(10)).toEqual('10B/s');
|
||||
expect(bytesFormatter(10 * 1024)).toEqual('10KB/s');
|
||||
expect(bytesFormatter(10.2 * 1024 * 1024)).toEqual('10.2MB/s');
|
||||
expect(bytesFormatter(3 * 1024 * 1024 * 1024)).toEqual('3GB/s');
|
||||
});
|
||||
|
||||
it('formats negative values with B/KB/MB/GB', function () {
|
||||
it('formats negative values with B/KB/MB/GB', () => {
|
||||
expect(bytesFormatter(-10)).toEqual('-10B/s');
|
||||
expect(bytesFormatter(-10 * 1024)).toEqual('-10KB/s');
|
||||
expect(bytesFormatter(-10.2 * 1024 * 1024)).toEqual('-10.2MB/s');
|
||||
|
@ -115,108 +116,105 @@ describe('Tick Formatters', function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Currency mode', function () {
|
||||
describe('Currency mode', () => {
|
||||
let currencyFormatter: any;
|
||||
beforeEach(function () {
|
||||
currencyFormatter = formatters.currency;
|
||||
});
|
||||
|
||||
it('is a function', function () {
|
||||
it('is a function', () => {
|
||||
expect(currencyFormatter).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it('formats with $ by default', function () {
|
||||
it('formats with $ by default', () => {
|
||||
const axis = {
|
||||
options: {
|
||||
units: {},
|
||||
},
|
||||
units: {},
|
||||
};
|
||||
expect(currencyFormatter(10.2, axis)).toEqual('$10.20');
|
||||
formatters = tickFormatters(axis as IAxis);
|
||||
currencyFormatter = formatters.currency;
|
||||
expect(currencyFormatter(10.2)).toEqual('$10.20');
|
||||
});
|
||||
|
||||
it('accepts currency in ISO 4217', function () {
|
||||
it('accepts currency in ISO 4217', () => {
|
||||
const axis = {
|
||||
options: {
|
||||
units: {
|
||||
prefix: 'CNY',
|
||||
},
|
||||
units: {
|
||||
prefix: 'CNY',
|
||||
},
|
||||
};
|
||||
|
||||
expect(currencyFormatter(10.2, axis)).toEqual('CN¥10.20');
|
||||
formatters = tickFormatters(axis as IAxis);
|
||||
currencyFormatter = formatters.currency;
|
||||
expect(currencyFormatter(10.2)).toEqual('CN¥10.20');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Percent mode', function () {
|
||||
describe('Percent mode', () => {
|
||||
let percentFormatter: any;
|
||||
beforeEach(function () {
|
||||
percentFormatter = formatters.percent;
|
||||
});
|
||||
|
||||
it('is a function', function () {
|
||||
it('is a function', () => {
|
||||
expect(percentFormatter).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it('formats with %', function () {
|
||||
it('formats with %', () => {
|
||||
const axis = {
|
||||
options: {
|
||||
units: {},
|
||||
},
|
||||
units: {},
|
||||
};
|
||||
expect(percentFormatter(0.1234, axis)).toEqual('12%');
|
||||
formatters = tickFormatters(axis as IAxis);
|
||||
percentFormatter = formatters.percent;
|
||||
expect(percentFormatter(0.1234)).toEqual('12%');
|
||||
});
|
||||
|
||||
it('formats with % with decimal precision', function () {
|
||||
it('formats with % with decimal precision', () => {
|
||||
const tickDecimals = 3;
|
||||
const tickDecimalShift = 2;
|
||||
const axis = {
|
||||
tickDecimals: tickDecimals + tickDecimalShift,
|
||||
options: {
|
||||
units: {
|
||||
tickDecimalsShift: tickDecimalShift,
|
||||
},
|
||||
units: {
|
||||
tickDecimalsShift: tickDecimalShift,
|
||||
},
|
||||
};
|
||||
expect(percentFormatter(0.12345, axis)).toEqual('12.345%');
|
||||
} as unknown;
|
||||
formatters = tickFormatters(axis as IAxis);
|
||||
percentFormatter = formatters.percent;
|
||||
expect(percentFormatter(0.12345)).toEqual('12.345%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom mode', function () {
|
||||
describe('Custom mode', () => {
|
||||
let customFormatter: any;
|
||||
beforeEach(function () {
|
||||
customFormatter = formatters.custom;
|
||||
});
|
||||
|
||||
it('is a function', function () {
|
||||
it('is a function', () => {
|
||||
expect(customFormatter).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it('accepts prefix and suffix', function () {
|
||||
it('accepts prefix and suffix', () => {
|
||||
const axis = {
|
||||
options: {
|
||||
units: {
|
||||
prefix: 'prefix',
|
||||
suffix: 'suffix',
|
||||
},
|
||||
units: {
|
||||
prefix: 'prefix',
|
||||
suffix: 'suffix',
|
||||
},
|
||||
tickDecimals: 1,
|
||||
};
|
||||
|
||||
expect(customFormatter(10.2, axis)).toEqual('prefix10.2suffix');
|
||||
formatters = tickFormatters(axis as IAxis);
|
||||
customFormatter = formatters.custom;
|
||||
expect(customFormatter(10.2)).toEqual('prefix10.2suffix');
|
||||
});
|
||||
|
||||
it('correctly renders small values', function () {
|
||||
it('correctly renders small values', () => {
|
||||
const axis = {
|
||||
options: {
|
||||
units: {
|
||||
prefix: 'prefix',
|
||||
suffix: 'suffix',
|
||||
},
|
||||
units: {
|
||||
prefix: 'prefix',
|
||||
suffix: 'suffix',
|
||||
},
|
||||
tickDecimals: 3,
|
||||
};
|
||||
|
||||
expect(customFormatter(0.00499999999999999, axis)).toEqual('prefix0.005suffix');
|
||||
formatters = tickFormatters(axis as IAxis);
|
||||
customFormatter = formatters.custom;
|
||||
expect(customFormatter(0.00499999999999999)).toEqual('prefix0.005suffix');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
|
||||
import { get } from 'lodash';
|
||||
|
||||
import { Axis } from './panel_utils';
|
||||
import { IAxis } from './panel_utils';
|
||||
|
||||
function baseTickFormatter(value: number, axis: Axis) {
|
||||
function baseTickFormatter(value: number, axis: IAxis) {
|
||||
const factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1;
|
||||
const formatted = '' + Math.round(value * factor) / factor;
|
||||
|
||||
|
@ -45,21 +45,20 @@ function unitFormatter(divisor: number, units: string[]) {
|
|||
};
|
||||
}
|
||||
|
||||
export function tickFormatters() {
|
||||
export function tickFormatters(axis: IAxis) {
|
||||
return {
|
||||
bits: unitFormatter(1000, ['b', 'kb', 'mb', 'gb', 'tb', 'pb']),
|
||||
'bits/s': unitFormatter(1000, ['b/s', 'kb/s', 'mb/s', 'gb/s', 'tb/s', 'pb/s']),
|
||||
bytes: unitFormatter(1024, ['B', 'KB', 'MB', 'GB', 'TB', 'PB']),
|
||||
'bytes/s': unitFormatter(1024, ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s', 'PB/s']),
|
||||
currency(val: number, axis: Axis) {
|
||||
currency(val: number) {
|
||||
return val.toLocaleString('en', {
|
||||
style: 'currency',
|
||||
currency: (axis && axis.options && axis.options.units.prefix) || 'USD',
|
||||
currency: (axis && axis.units && axis.units.prefix) || 'USD',
|
||||
});
|
||||
},
|
||||
percent(val: number, axis: Axis) {
|
||||
let precision =
|
||||
get(axis, 'tickDecimals', 0) - get(axis, 'options.units.tickDecimalsShift', 0);
|
||||
percent(val: number) {
|
||||
let precision = get(axis, 'tickDecimals', 0) - get(axis, 'units.tickDecimalsShift', 0);
|
||||
// toFixed only accepts values between 0 and 20
|
||||
if (precision < 0) {
|
||||
precision = 0;
|
||||
|
@ -69,10 +68,10 @@ export function tickFormatters() {
|
|||
|
||||
return (val * 100).toFixed(precision) + '%';
|
||||
},
|
||||
custom(val: number, axis: Axis) {
|
||||
custom(val: number) {
|
||||
const formattedVal = baseTickFormatter(val, axis);
|
||||
const prefix = axis && axis.options && axis.options.units.prefix;
|
||||
const suffix = axis && axis.options && axis.options.units.suffix;
|
||||
const prefix = axis && axis.units && axis.units.prefix;
|
||||
const suffix = axis && axis.units && axis.units.suffix;
|
||||
return prefix + formattedVal + suffix;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Axis } from './panel_utils';
|
||||
import { IAxis } from './panel_utils';
|
||||
|
||||
export function generateTicksProvider() {
|
||||
function floorInBase(n: number, base: number) {
|
||||
return base * Math.floor(n / base);
|
||||
}
|
||||
|
||||
function generateTicks(axis: Axis) {
|
||||
function generateTicks(axis: IAxis) {
|
||||
const returnTicks = [];
|
||||
let tickSize = 2;
|
||||
let delta = axis.delta || 0;
|
||||
|
@ -46,5 +46,5 @@ export function generateTicksProvider() {
|
|||
return returnTicks;
|
||||
}
|
||||
|
||||
return (axis: Axis) => generateTicks(axis);
|
||||
return (axis: IAxis) => generateTicks(axis);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { TimelionVisDependencies } from '../plugin';
|
|||
import { getTimezone } from './get_timezone';
|
||||
import { TimelionVisParams } from '../timelion_vis_fn';
|
||||
import { getDataSearch } from '../helpers/plugin_services';
|
||||
import { VisSeries } from '../../common/vis_data';
|
||||
|
||||
interface Stats {
|
||||
cacheCount: number;
|
||||
|
@ -21,17 +22,13 @@ interface Stats {
|
|||
sheetTime: number;
|
||||
}
|
||||
|
||||
export interface Series {
|
||||
_global?: boolean;
|
||||
export interface Series extends VisSeries {
|
||||
_global?: Record<any, any>;
|
||||
_hide?: boolean;
|
||||
_id?: number;
|
||||
_title?: string;
|
||||
color?: string;
|
||||
data: Array<Record<number, number>>;
|
||||
fit: string;
|
||||
label: string;
|
||||
split: string;
|
||||
stack?: boolean;
|
||||
type: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,16 +9,26 @@
|
|||
import { PluginInitializerContext } from 'kibana/public';
|
||||
import { TimelionVisPlugin as Plugin } from './plugin';
|
||||
|
||||
import { tickFormatters } from './legacy/tick_formatters';
|
||||
import { getTimezone } from './helpers/get_timezone';
|
||||
import { xaxisFormatterProvider } from './helpers/xaxis_formatter';
|
||||
import { generateTicksProvider } from './helpers/tick_generator';
|
||||
import { DEFAULT_TIME_FORMAT, calculateInterval } from '../common/lib';
|
||||
import { parseTimelionExpressionAsync } from '../common/parser_async';
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new Plugin(initializerContext);
|
||||
}
|
||||
|
||||
export { getTimezone } from './helpers/get_timezone';
|
||||
export { tickFormatters } from './helpers/tick_formatters';
|
||||
export { xaxisFormatterProvider } from './helpers/xaxis_formatter';
|
||||
export { generateTicksProvider } from './helpers/tick_generator';
|
||||
|
||||
export { DEFAULT_TIME_FORMAT, calculateInterval } from '../common/lib';
|
||||
export { parseTimelionExpressionAsync } from '../common/parser_async';
|
||||
// This export should be removed on removing Timeline APP
|
||||
export const _LEGACY_ = {
|
||||
DEFAULT_TIME_FORMAT,
|
||||
calculateInterval,
|
||||
parseTimelionExpressionAsync,
|
||||
tickFormatters,
|
||||
getTimezone,
|
||||
xaxisFormatterProvider,
|
||||
generateTicksProvider,
|
||||
};
|
||||
|
||||
export { VisTypeTimelionPluginStart, VisTypeTimelionPluginSetup } from './plugin';
|
||||
|
|
168
src/plugins/vis_type_timelion/public/legacy/panel_utils.ts
Normal file
168
src/plugins/vis_type_timelion/public/legacy/panel_utils.ts
Normal file
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* 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 { cloneDeep, defaults, mergeWith, compact } from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import moment, { Moment } from 'moment-timezone';
|
||||
|
||||
import { TimefilterContract } from 'src/plugins/data/public';
|
||||
import { IUiSettingsClient } from 'kibana/public';
|
||||
|
||||
import { calculateInterval } from '../../common/lib';
|
||||
import { xaxisFormatterProvider } from '../helpers/xaxis_formatter';
|
||||
import { Series } from '../helpers/timelion_request_handler';
|
||||
import { colors } from '../helpers/chart_constants';
|
||||
|
||||
export interface LegacyAxis {
|
||||
delta?: number;
|
||||
max?: number;
|
||||
min?: number;
|
||||
mode: string;
|
||||
options?: {
|
||||
units: { prefix: string; suffix: string };
|
||||
};
|
||||
tickSize?: number;
|
||||
ticks: number;
|
||||
tickLength: number;
|
||||
timezone: string;
|
||||
tickDecimals?: number;
|
||||
tickFormatter: ((val: number) => string) | ((val: number, axis: LegacyAxis) => string);
|
||||
tickGenerator?(axis: LegacyAxis): number[];
|
||||
units?: { type: string };
|
||||
}
|
||||
|
||||
interface TimeRangeBounds {
|
||||
min: Moment | undefined;
|
||||
max: Moment | undefined;
|
||||
}
|
||||
|
||||
export const ACTIVE_CURSOR = 'ACTIVE_CURSOR_TIMELION';
|
||||
export const eventBus = $({});
|
||||
|
||||
const SERIES_ID_ATTR = 'data-series-id';
|
||||
|
||||
function buildSeriesData(chart: Series[], options: jquery.flot.plotOptions) {
|
||||
const seriesData = chart.map((series: Series, seriesIndex: number) => {
|
||||
const newSeries: Series = cloneDeep(
|
||||
defaults(series, {
|
||||
shadowSize: 0,
|
||||
lines: {
|
||||
lineWidth: 3,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
newSeries._id = seriesIndex;
|
||||
|
||||
if (series.color) {
|
||||
const span = document.createElement('span');
|
||||
span.style.color = series.color;
|
||||
newSeries.color = span.style.color;
|
||||
}
|
||||
|
||||
if (series._hide) {
|
||||
newSeries.data = [];
|
||||
newSeries.stack = false;
|
||||
newSeries.label = `(hidden) ${series.label}`;
|
||||
}
|
||||
|
||||
if (series._global) {
|
||||
mergeWith(options, series._global, (objVal, srcVal) => {
|
||||
// This is kind of gross, it means that you can't replace a global value with a null
|
||||
// best you can do is an empty string. Deal with it.
|
||||
if (objVal == null) {
|
||||
return srcVal;
|
||||
}
|
||||
if (srcVal == null) {
|
||||
return objVal;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return newSeries;
|
||||
});
|
||||
|
||||
return compact(seriesData);
|
||||
}
|
||||
|
||||
function buildOptions(
|
||||
intervalValue: string,
|
||||
timefilter: TimefilterContract,
|
||||
uiSettings: IUiSettingsClient,
|
||||
clientWidth = 0,
|
||||
showGrid?: boolean
|
||||
) {
|
||||
// Get the X-axis tick format
|
||||
const time: TimeRangeBounds = timefilter.getBounds();
|
||||
const interval = calculateInterval(
|
||||
(time.min && time.min.valueOf()) || 0,
|
||||
(time.max && time.max.valueOf()) || 0,
|
||||
uiSettings.get('timelion:target_buckets') || 200,
|
||||
intervalValue,
|
||||
uiSettings.get('timelion:min_interval') || '1ms'
|
||||
);
|
||||
const format = xaxisFormatterProvider(uiSettings)(interval);
|
||||
|
||||
const tickLetterWidth = 7;
|
||||
const tickPadding = 45;
|
||||
|
||||
const options = {
|
||||
xaxis: {
|
||||
mode: 'time',
|
||||
tickLength: 5,
|
||||
timezone: 'browser',
|
||||
// Calculate how many ticks can fit on the axis
|
||||
ticks: Math.floor(clientWidth / (format.length * tickLetterWidth + tickPadding)),
|
||||
// Use moment to format ticks so we get timezone correction
|
||||
tickFormatter: (val: number) => moment(val).format(format),
|
||||
},
|
||||
selection: {
|
||||
mode: 'x',
|
||||
color: '#ccc',
|
||||
},
|
||||
crosshair: {
|
||||
mode: 'x',
|
||||
color: '#C66',
|
||||
lineWidth: 2,
|
||||
},
|
||||
colors,
|
||||
grid: {
|
||||
show: showGrid,
|
||||
borderWidth: 0,
|
||||
borderColor: null,
|
||||
margin: 10,
|
||||
hoverable: true,
|
||||
autoHighlight: false,
|
||||
},
|
||||
legend: {
|
||||
backgroundColor: 'rgb(255,255,255,0)',
|
||||
position: 'nw',
|
||||
labelBoxBorderColor: 'rgb(255,255,255,0)',
|
||||
labelFormatter(label: string, series: { _id: number }) {
|
||||
const wrapperSpan = document.createElement('span');
|
||||
const labelSpan = document.createElement('span');
|
||||
const numberSpan = document.createElement('span');
|
||||
|
||||
wrapperSpan.setAttribute('class', 'ngLegendValue');
|
||||
wrapperSpan.setAttribute(SERIES_ID_ATTR, `${series._id}`);
|
||||
|
||||
labelSpan.appendChild(document.createTextNode(label));
|
||||
numberSpan.setAttribute('class', 'ngLegendValueNumber');
|
||||
|
||||
wrapperSpan.appendChild(labelSpan);
|
||||
wrapperSpan.appendChild(numberSpan);
|
||||
|
||||
return wrapperSpan.outerHTML;
|
||||
},
|
||||
},
|
||||
} as jquery.flot.plotOptions & { yaxes?: LegacyAxis[] };
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export { buildSeriesData, buildOptions, SERIES_ID_ATTR };
|
|
@ -0,0 +1,222 @@
|
|||
/*
|
||||
* 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 { tickFormatters } from './tick_formatters';
|
||||
|
||||
describe('Tick Formatters', function () {
|
||||
let formatters: any;
|
||||
|
||||
beforeEach(function () {
|
||||
formatters = tickFormatters();
|
||||
});
|
||||
|
||||
describe('Bits mode', function () {
|
||||
let bitFormatter: any;
|
||||
beforeEach(function () {
|
||||
bitFormatter = formatters.bits;
|
||||
});
|
||||
|
||||
it('is a function', function () {
|
||||
expect(bitFormatter).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it('formats with b/kb/mb/gb', function () {
|
||||
expect(bitFormatter(7)).toEqual('7b');
|
||||
expect(bitFormatter(4 * 1000)).toEqual('4kb');
|
||||
expect(bitFormatter(4.1 * 1000 * 1000)).toEqual('4.1mb');
|
||||
expect(bitFormatter(3 * 1000 * 1000 * 1000)).toEqual('3gb');
|
||||
});
|
||||
|
||||
it('formats negative values with b/kb/mb/gb', () => {
|
||||
expect(bitFormatter(-7)).toEqual('-7b');
|
||||
expect(bitFormatter(-4 * 1000)).toEqual('-4kb');
|
||||
expect(bitFormatter(-4.1 * 1000 * 1000)).toEqual('-4.1mb');
|
||||
expect(bitFormatter(-3 * 1000 * 1000 * 1000)).toEqual('-3gb');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bits/s mode', function () {
|
||||
let bitsFormatter: any;
|
||||
beforeEach(function () {
|
||||
bitsFormatter = formatters['bits/s'];
|
||||
});
|
||||
|
||||
it('is a function', function () {
|
||||
expect(bitsFormatter).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it('formats with b/kb/mb/gb', function () {
|
||||
expect(bitsFormatter(7)).toEqual('7b/s');
|
||||
expect(bitsFormatter(4 * 1000)).toEqual('4kb/s');
|
||||
expect(bitsFormatter(4.1 * 1000 * 1000)).toEqual('4.1mb/s');
|
||||
expect(bitsFormatter(3 * 1000 * 1000 * 1000)).toEqual('3gb/s');
|
||||
});
|
||||
|
||||
it('formats negative values with b/kb/mb/gb', function () {
|
||||
expect(bitsFormatter(-7)).toEqual('-7b/s');
|
||||
expect(bitsFormatter(-4 * 1000)).toEqual('-4kb/s');
|
||||
expect(bitsFormatter(-4.1 * 1000 * 1000)).toEqual('-4.1mb/s');
|
||||
expect(bitsFormatter(-3 * 1000 * 1000 * 1000)).toEqual('-3gb/s');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bytes mode', function () {
|
||||
let byteFormatter: any;
|
||||
beforeEach(function () {
|
||||
byteFormatter = formatters.bytes;
|
||||
});
|
||||
|
||||
it('is a function', function () {
|
||||
expect(byteFormatter).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it('formats with B/KB/MB/GB', function () {
|
||||
expect(byteFormatter(10)).toEqual('10B');
|
||||
expect(byteFormatter(10 * 1024)).toEqual('10KB');
|
||||
expect(byteFormatter(10.2 * 1024 * 1024)).toEqual('10.2MB');
|
||||
expect(byteFormatter(3 * 1024 * 1024 * 1024)).toEqual('3GB');
|
||||
});
|
||||
|
||||
it('formats negative values with B/KB/MB/GB', function () {
|
||||
expect(byteFormatter(-10)).toEqual('-10B');
|
||||
expect(byteFormatter(-10 * 1024)).toEqual('-10KB');
|
||||
expect(byteFormatter(-10.2 * 1024 * 1024)).toEqual('-10.2MB');
|
||||
expect(byteFormatter(-3 * 1024 * 1024 * 1024)).toEqual('-3GB');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bytes/s mode', function () {
|
||||
let bytesFormatter: any;
|
||||
beforeEach(function () {
|
||||
bytesFormatter = formatters['bytes/s'];
|
||||
});
|
||||
|
||||
it('is a function', function () {
|
||||
expect(bytesFormatter).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it('formats with B/KB/MB/GB', function () {
|
||||
expect(bytesFormatter(10)).toEqual('10B/s');
|
||||
expect(bytesFormatter(10 * 1024)).toEqual('10KB/s');
|
||||
expect(bytesFormatter(10.2 * 1024 * 1024)).toEqual('10.2MB/s');
|
||||
expect(bytesFormatter(3 * 1024 * 1024 * 1024)).toEqual('3GB/s');
|
||||
});
|
||||
|
||||
it('formats negative values with B/KB/MB/GB', function () {
|
||||
expect(bytesFormatter(-10)).toEqual('-10B/s');
|
||||
expect(bytesFormatter(-10 * 1024)).toEqual('-10KB/s');
|
||||
expect(bytesFormatter(-10.2 * 1024 * 1024)).toEqual('-10.2MB/s');
|
||||
expect(bytesFormatter(-3 * 1024 * 1024 * 1024)).toEqual('-3GB/s');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Currency mode', function () {
|
||||
let currencyFormatter: any;
|
||||
beforeEach(function () {
|
||||
currencyFormatter = formatters.currency;
|
||||
});
|
||||
|
||||
it('is a function', function () {
|
||||
expect(currencyFormatter).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it('formats with $ by default', function () {
|
||||
const axis = {
|
||||
options: {
|
||||
units: {},
|
||||
},
|
||||
};
|
||||
expect(currencyFormatter(10.2, axis)).toEqual('$10.20');
|
||||
});
|
||||
|
||||
it('accepts currency in ISO 4217', function () {
|
||||
const axis = {
|
||||
options: {
|
||||
units: {
|
||||
prefix: 'CNY',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(currencyFormatter(10.2, axis)).toEqual('CN¥10.20');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Percent mode', function () {
|
||||
let percentFormatter: any;
|
||||
beforeEach(function () {
|
||||
percentFormatter = formatters.percent;
|
||||
});
|
||||
|
||||
it('is a function', function () {
|
||||
expect(percentFormatter).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it('formats with %', function () {
|
||||
const axis = {
|
||||
options: {
|
||||
units: {},
|
||||
},
|
||||
};
|
||||
expect(percentFormatter(0.1234, axis)).toEqual('12%');
|
||||
});
|
||||
|
||||
it('formats with % with decimal precision', function () {
|
||||
const tickDecimals = 3;
|
||||
const tickDecimalShift = 2;
|
||||
const axis = {
|
||||
tickDecimals: tickDecimals + tickDecimalShift,
|
||||
options: {
|
||||
units: {
|
||||
tickDecimalsShift: tickDecimalShift,
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(percentFormatter(0.12345, axis)).toEqual('12.345%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom mode', function () {
|
||||
let customFormatter: any;
|
||||
beforeEach(function () {
|
||||
customFormatter = formatters.custom;
|
||||
});
|
||||
|
||||
it('is a function', function () {
|
||||
expect(customFormatter).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it('accepts prefix and suffix', function () {
|
||||
const axis = {
|
||||
options: {
|
||||
units: {
|
||||
prefix: 'prefix',
|
||||
suffix: 'suffix',
|
||||
},
|
||||
},
|
||||
tickDecimals: 1,
|
||||
};
|
||||
|
||||
expect(customFormatter(10.2, axis)).toEqual('prefix10.2suffix');
|
||||
});
|
||||
|
||||
it('correctly renders small values', function () {
|
||||
const axis = {
|
||||
options: {
|
||||
units: {
|
||||
prefix: 'prefix',
|
||||
suffix: 'suffix',
|
||||
},
|
||||
},
|
||||
tickDecimals: 3,
|
||||
};
|
||||
|
||||
expect(customFormatter(0.00499999999999999, axis)).toEqual('prefix0.005suffix');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 { get } from 'lodash';
|
||||
|
||||
import type { LegacyAxis } from './panel_utils';
|
||||
|
||||
function baseTickFormatter(value: number, axis: LegacyAxis) {
|
||||
const factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1;
|
||||
const formatted = '' + Math.round(value * factor) / factor;
|
||||
|
||||
// If tickDecimals was specified, ensure that we have exactly that
|
||||
// much precision; otherwise default to the value's own precision.
|
||||
|
||||
if (axis.tickDecimals != null) {
|
||||
const decimal = formatted.indexOf('.');
|
||||
const precision = decimal === -1 ? 0 : formatted.length - decimal - 1;
|
||||
if (precision < axis.tickDecimals) {
|
||||
return (
|
||||
(precision ? formatted : formatted + '.') +
|
||||
('' + factor).substr(1, axis.tickDecimals - precision)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
function unitFormatter(divisor: number, units: string[]) {
|
||||
return (val: number) => {
|
||||
let index = 0;
|
||||
const isNegative = val < 0;
|
||||
val = Math.abs(val);
|
||||
while (val >= divisor && index < units.length) {
|
||||
val /= divisor;
|
||||
index++;
|
||||
}
|
||||
const value = (Math.round(val * 100) / 100) * (isNegative ? -1 : 1);
|
||||
return `${value}${units[index]}`;
|
||||
};
|
||||
}
|
||||
|
||||
export function tickFormatters() {
|
||||
return {
|
||||
bits: unitFormatter(1000, ['b', 'kb', 'mb', 'gb', 'tb', 'pb']),
|
||||
'bits/s': unitFormatter(1000, ['b/s', 'kb/s', 'mb/s', 'gb/s', 'tb/s', 'pb/s']),
|
||||
bytes: unitFormatter(1024, ['B', 'KB', 'MB', 'GB', 'TB', 'PB']),
|
||||
'bytes/s': unitFormatter(1024, ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s', 'PB/s']),
|
||||
currency(val: number, axis: LegacyAxis) {
|
||||
return val.toLocaleString('en', {
|
||||
style: 'currency',
|
||||
currency: (axis && axis.options && axis.options.units.prefix) || 'USD',
|
||||
});
|
||||
},
|
||||
percent(val: number, axis: LegacyAxis) {
|
||||
let precision =
|
||||
get(axis, 'tickDecimals', 0) - get(axis, 'options.units.tickDecimalsShift', 0);
|
||||
// toFixed only accepts values between 0 and 20
|
||||
if (precision < 0) {
|
||||
precision = 0;
|
||||
} else if (precision > 20) {
|
||||
precision = 20;
|
||||
}
|
||||
|
||||
return (val * 100).toFixed(precision) + '%';
|
||||
},
|
||||
custom(val: number, axis: LegacyAxis) {
|
||||
const formattedVal = baseTickFormatter(val, axis);
|
||||
const prefix = axis && axis.options && axis.options.units.prefix;
|
||||
const suffix = axis && axis.options && axis.options.units.suffix;
|
||||
return prefix + formattedVal + suffix;
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
.timChart {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// Custom Jquery FLOT / schema selectors
|
||||
// Cannot change at the moment
|
||||
|
||||
.chart-top-title {
|
||||
@include euiFontSizeXS;
|
||||
flex: 0;
|
||||
text-align: center;
|
||||
font-weight: $euiFontWeightBold;
|
||||
}
|
||||
|
||||
.chart-canvas {
|
||||
min-width: 100%;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.legendLabel {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.legendColorBox {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ngLegendValue {
|
||||
color: $euiTextColor;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.ngLegendValueNumber {
|
||||
margin-left: $euiSizeXS;
|
||||
margin-right: $euiSizeXS;
|
||||
font-weight: $euiFontWeightBold;
|
||||
}
|
||||
|
||||
.flot-tick-label {
|
||||
font-size: $euiFontSizeXS;
|
||||
color: $euiColorDarkShade;
|
||||
}
|
||||
}
|
||||
|
||||
.timChart__legendCaption {
|
||||
color: $euiTextColor;
|
||||
white-space: nowrap;
|
||||
font-weight: $euiFontWeightBold;
|
||||
}
|
|
@ -0,0 +1,418 @@
|
|||
/*
|
||||
* 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, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import $ from 'jquery';
|
||||
import moment from 'moment-timezone';
|
||||
import { debounce, compact, get, each, cloneDeep, last, map } from 'lodash';
|
||||
import { useResizeObserver } from '@elastic/eui';
|
||||
|
||||
import { IInterpreterRenderHandlers } from 'src/plugins/expressions';
|
||||
import { useKibana } from '../../../kibana_react/public';
|
||||
import { DEFAULT_TIME_FORMAT } from '../../common/lib';
|
||||
|
||||
import {
|
||||
buildSeriesData,
|
||||
buildOptions,
|
||||
SERIES_ID_ATTR,
|
||||
LegacyAxis,
|
||||
ACTIVE_CURSOR,
|
||||
eventBus,
|
||||
} from './panel_utils';
|
||||
|
||||
import { Series, Sheet } from '../helpers/timelion_request_handler';
|
||||
import { colors } from '../helpers/chart_constants';
|
||||
import { tickFormatters } from './tick_formatters';
|
||||
import { generateTicksProvider } from '../helpers/tick_generator';
|
||||
|
||||
import type { TimelionVisDependencies } from '../plugin';
|
||||
import type { RangeFilterParams } from '../../../data/common';
|
||||
|
||||
import './timelion_vis.scss';
|
||||
|
||||
interface CrosshairPlot extends jquery.flot.plot {
|
||||
setCrosshair: (pos: Position) => void;
|
||||
clearCrosshair: () => void;
|
||||
}
|
||||
|
||||
interface TimelionVisComponentProps {
|
||||
onBrushEvent: (rangeFilterParams: RangeFilterParams) => void;
|
||||
interval: string;
|
||||
seriesList: Sheet;
|
||||
renderComplete: IInterpreterRenderHandlers['done'];
|
||||
}
|
||||
|
||||
interface Position {
|
||||
x: number;
|
||||
x1: number;
|
||||
y: number;
|
||||
y1: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
}
|
||||
|
||||
interface Range {
|
||||
to: number;
|
||||
from: number;
|
||||
}
|
||||
|
||||
interface Ranges {
|
||||
xaxis: Range;
|
||||
yaxis: Range;
|
||||
}
|
||||
|
||||
const DEBOUNCE_DELAY = 50;
|
||||
// ensure legend is the same height with or without a caption so legend items do not move around
|
||||
const emptyCaption = '<br>';
|
||||
|
||||
function TimelionVisComponent({
|
||||
interval,
|
||||
seriesList,
|
||||
renderComplete,
|
||||
onBrushEvent,
|
||||
}: TimelionVisComponentProps) {
|
||||
const kibana = useKibana<TimelionVisDependencies>();
|
||||
const [chart, setChart] = useState(() => cloneDeep(seriesList.list));
|
||||
const [canvasElem, setCanvasElem] = useState<HTMLDivElement>();
|
||||
const [chartElem, setChartElem] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const [originalColorMap, setOriginalColorMap] = useState(() => new Map<Series, string>());
|
||||
|
||||
const [highlightedSeries, setHighlightedSeries] = useState<number | null>(null);
|
||||
const [focusedSeries, setFocusedSeries] = useState<number | null>();
|
||||
const [plot, setPlot] = useState<jquery.flot.plot>();
|
||||
|
||||
// Used to toggle the series, and for displaying values on hover
|
||||
const [legendValueNumbers, setLegendValueNumbers] = useState<JQuery<HTMLElement>>();
|
||||
const [legendCaption, setLegendCaption] = useState<JQuery<HTMLElement>>();
|
||||
|
||||
const canvasRef = useCallback((node: HTMLDivElement | null) => {
|
||||
if (node !== null) {
|
||||
setCanvasElem(node);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const elementRef = useCallback((node: HTMLDivElement | null) => {
|
||||
if (node !== null) {
|
||||
setChartElem(node);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (chartElem) {
|
||||
$(chartElem).off('plotselected').off('plothover').off('mouseleave');
|
||||
}
|
||||
},
|
||||
[chartElem]
|
||||
);
|
||||
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
const highlightSeries = useCallback(
|
||||
debounce(({ currentTarget }: JQuery.TriggeredEvent) => {
|
||||
const id = Number(currentTarget.getAttribute(SERIES_ID_ATTR));
|
||||
if (highlightedSeries === id) {
|
||||
return;
|
||||
}
|
||||
|
||||
setHighlightedSeries(id);
|
||||
setChart((chartState) =>
|
||||
chartState.map((series: Series, seriesIndex: number) => {
|
||||
series.color =
|
||||
seriesIndex === id
|
||||
? originalColorMap.get(series) // color it like it was
|
||||
: 'rgba(128,128,128,0.1)'; // mark as grey
|
||||
|
||||
return series;
|
||||
})
|
||||
);
|
||||
}, DEBOUNCE_DELAY),
|
||||
[originalColorMap, highlightedSeries]
|
||||
);
|
||||
|
||||
const focusSeries = useCallback(
|
||||
(event: JQuery.TriggeredEvent) => {
|
||||
const id = Number(event.currentTarget.getAttribute(SERIES_ID_ATTR));
|
||||
setFocusedSeries(id);
|
||||
highlightSeries(event);
|
||||
},
|
||||
[highlightSeries]
|
||||
);
|
||||
|
||||
const toggleSeries = useCallback(({ currentTarget }: JQuery.TriggeredEvent) => {
|
||||
const id = Number(currentTarget.getAttribute(SERIES_ID_ATTR));
|
||||
|
||||
setChart((chartState) =>
|
||||
chartState.map((series: Series, seriesIndex: number) => {
|
||||
if (seriesIndex === id) {
|
||||
series._hide = !series._hide;
|
||||
}
|
||||
return series;
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
const updateCaption = useCallback(
|
||||
(plotData: any) => {
|
||||
if (canvasElem && get(plotData, '[0]._global.legend.showTime', true)) {
|
||||
const caption = $('<caption class="timChart__legendCaption"></caption>');
|
||||
caption.html(emptyCaption);
|
||||
setLegendCaption(caption);
|
||||
|
||||
const canvasNode = $(canvasElem);
|
||||
canvasNode.find('div.legend table').append(caption);
|
||||
setLegendValueNumbers(canvasNode.find('.ngLegendValueNumber'));
|
||||
|
||||
const legend = $(canvasElem).find('.ngLegendValue');
|
||||
if (legend) {
|
||||
legend.click(toggleSeries);
|
||||
legend.focus(focusSeries);
|
||||
legend.mouseover(highlightSeries);
|
||||
}
|
||||
|
||||
// legend has been re-created. Apply focus on legend element when previously set
|
||||
if (focusedSeries || focusedSeries === 0) {
|
||||
canvasNode.find('div.legend table .legendLabel>span').get(focusedSeries).focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
[focusedSeries, canvasElem, toggleSeries, focusSeries, highlightSeries]
|
||||
);
|
||||
|
||||
const updatePlot = useCallback(
|
||||
(chartValue: Series[], grid?: boolean) => {
|
||||
if (canvasElem && canvasElem.clientWidth > 0 && canvasElem.clientHeight > 0) {
|
||||
const options = buildOptions(
|
||||
interval,
|
||||
kibana.services.timefilter,
|
||||
kibana.services.uiSettings,
|
||||
chartElem?.clientWidth,
|
||||
grid
|
||||
);
|
||||
const updatedSeries = buildSeriesData(chartValue, options);
|
||||
|
||||
if (options.yaxes) {
|
||||
options.yaxes.forEach((yaxis: LegacyAxis) => {
|
||||
if (yaxis && yaxis.units) {
|
||||
const formatters = tickFormatters();
|
||||
yaxis.tickFormatter = formatters[yaxis.units.type as keyof typeof formatters];
|
||||
const byteModes = ['bytes', 'bytes/s'];
|
||||
if (byteModes.includes(yaxis.units.type)) {
|
||||
yaxis.tickGenerator = generateTicksProvider();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const newPlot = $.plot($(canvasElem), updatedSeries, options);
|
||||
setPlot(newPlot);
|
||||
renderComplete();
|
||||
|
||||
updateCaption(newPlot.getData());
|
||||
}
|
||||
},
|
||||
[canvasElem, chartElem?.clientWidth, renderComplete, kibana.services, interval, updateCaption]
|
||||
);
|
||||
|
||||
const dimensions = useResizeObserver(chartElem);
|
||||
|
||||
useEffect(() => {
|
||||
updatePlot(chart, seriesList.render && seriesList.render.grid);
|
||||
}, [chart, updatePlot, seriesList.render, dimensions]);
|
||||
|
||||
useEffect(() => {
|
||||
const colorsSet: Array<[Series, string]> = [];
|
||||
const newChart = seriesList.list.map((series: Series, seriesIndex: number) => {
|
||||
const newSeries = { ...series };
|
||||
if (!newSeries.color) {
|
||||
const colorIndex = seriesIndex % colors.length;
|
||||
newSeries.color = colors[colorIndex];
|
||||
}
|
||||
colorsSet.push([newSeries, newSeries.color]);
|
||||
return newSeries;
|
||||
});
|
||||
setChart(newChart);
|
||||
setOriginalColorMap(new Map(colorsSet));
|
||||
}, [seriesList.list]);
|
||||
|
||||
const unhighlightSeries = useCallback(() => {
|
||||
if (highlightedSeries === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setHighlightedSeries(null);
|
||||
setFocusedSeries(null);
|
||||
|
||||
setChart((chartState) =>
|
||||
chartState.map((series: Series) => {
|
||||
series.color = originalColorMap.get(series); // reset the colors
|
||||
return series;
|
||||
})
|
||||
);
|
||||
}, [originalColorMap, highlightedSeries]);
|
||||
|
||||
// Shamelessly borrowed from the flotCrosshairs example
|
||||
const setLegendNumbers = useCallback(
|
||||
(pos: Position) => {
|
||||
unhighlightSeries();
|
||||
|
||||
const axes = plot!.getAxes();
|
||||
if (pos.x < axes.xaxis.min! || pos.x > axes.xaxis.max!) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataset = plot!.getData();
|
||||
if (legendCaption) {
|
||||
legendCaption.text(
|
||||
moment(pos.x).format(get(dataset, '[0]._global.legend.timeFormat', DEFAULT_TIME_FORMAT))
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < dataset.length; ++i) {
|
||||
const series = dataset[i];
|
||||
const useNearestPoint = series.lines!.show && !series.lines!.steps;
|
||||
const precision = get(series, '_meta.precision', 2);
|
||||
|
||||
// We're setting this flag on top on the series object belonging to the flot library, so we're simply casting here.
|
||||
if ((series as { _hide?: boolean })._hide) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentPoint = series.data.find((point: [number, number], index: number) => {
|
||||
if (index + 1 === series.data.length) {
|
||||
return true;
|
||||
}
|
||||
if (useNearestPoint) {
|
||||
return pos.x - point[0] < series.data[index + 1][0] - pos.x;
|
||||
} else {
|
||||
return pos.x < series.data[index + 1][0];
|
||||
}
|
||||
});
|
||||
|
||||
const y = currentPoint[1];
|
||||
|
||||
if (legendValueNumbers) {
|
||||
if (y == null) {
|
||||
legendValueNumbers.eq(i).empty();
|
||||
} else {
|
||||
let label = y.toFixed(precision);
|
||||
const formatter = ((series.yaxis as unknown) as LegacyAxis).tickFormatter;
|
||||
if (formatter) {
|
||||
label = formatter(Number(label), (series.yaxis as unknown) as LegacyAxis);
|
||||
}
|
||||
legendValueNumbers.eq(i).text(`(${label})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[plot, legendValueNumbers, unhighlightSeries, legendCaption]
|
||||
);
|
||||
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
const debouncedSetLegendNumbers = useCallback(
|
||||
debounce(setLegendNumbers, DEBOUNCE_DELAY, {
|
||||
maxWait: DEBOUNCE_DELAY,
|
||||
leading: true,
|
||||
trailing: false,
|
||||
}),
|
||||
[setLegendNumbers]
|
||||
);
|
||||
|
||||
const clearLegendNumbers = useCallback(() => {
|
||||
if (legendCaption) {
|
||||
legendCaption.html(emptyCaption);
|
||||
}
|
||||
each(legendValueNumbers!, (num: Node) => {
|
||||
$(num).empty();
|
||||
});
|
||||
}, [legendCaption, legendValueNumbers]);
|
||||
|
||||
const plotHover = useCallback(
|
||||
(pos: Position) => {
|
||||
(plot as CrosshairPlot).setCrosshair(pos);
|
||||
debouncedSetLegendNumbers(pos);
|
||||
},
|
||||
[plot, debouncedSetLegendNumbers]
|
||||
);
|
||||
|
||||
const plotHoverHandler = useCallback(
|
||||
(event: JQuery.TriggeredEvent, pos: Position) => {
|
||||
if (!plot) {
|
||||
return;
|
||||
}
|
||||
plotHover(pos);
|
||||
eventBus.trigger(ACTIVE_CURSOR, [event, pos]);
|
||||
},
|
||||
[plot, plotHover]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const updateCursor = (_: any, event: JQuery.TriggeredEvent, pos: Position) => {
|
||||
if (!plot) {
|
||||
return;
|
||||
}
|
||||
plotHover(pos);
|
||||
};
|
||||
|
||||
eventBus.on(ACTIVE_CURSOR, updateCursor);
|
||||
|
||||
return () => {
|
||||
eventBus.off(ACTIVE_CURSOR, updateCursor);
|
||||
};
|
||||
}, [plot, plotHover]);
|
||||
|
||||
const mouseLeaveHandler = useCallback(() => {
|
||||
if (!plot) {
|
||||
return;
|
||||
}
|
||||
(plot as CrosshairPlot).clearCrosshair();
|
||||
clearLegendNumbers();
|
||||
}, [plot, clearLegendNumbers]);
|
||||
|
||||
const plotSelectedHandler = useCallback(
|
||||
(event: JQuery.TriggeredEvent, ranges: Ranges) => {
|
||||
onBrushEvent({
|
||||
gte: ranges.xaxis.from,
|
||||
lte: ranges.xaxis.to,
|
||||
});
|
||||
},
|
||||
[onBrushEvent]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (chartElem) {
|
||||
$(chartElem).off('plotselected').on('plotselected', plotSelectedHandler);
|
||||
}
|
||||
}, [chartElem, plotSelectedHandler]);
|
||||
|
||||
useEffect(() => {
|
||||
if (chartElem) {
|
||||
$(chartElem).off('mouseleave').on('mouseleave', mouseLeaveHandler);
|
||||
}
|
||||
}, [chartElem, mouseLeaveHandler]);
|
||||
|
||||
useEffect(() => {
|
||||
if (chartElem) {
|
||||
$(chartElem).off('plothover').on('plothover', plotHoverHandler);
|
||||
}
|
||||
}, [chartElem, plotHoverHandler]);
|
||||
|
||||
const title: string = useMemo(() => last(compact(map(seriesList.list, '_title'))) || '', [
|
||||
seriesList.list,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div ref={elementRef} className="timChart">
|
||||
<div className="chart-top-title">{title}</div>
|
||||
<div ref={canvasRef} className="chart-canvas" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// default export required for React.Lazy
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { TimelionVisComponent as default };
|
|
@ -22,6 +22,7 @@ import {
|
|||
} from 'src/plugins/data/public';
|
||||
|
||||
import { VisualizationsSetup } from '../../visualizations/public';
|
||||
import { ChartsPluginSetup } from '../../charts/public';
|
||||
|
||||
import { getTimelionVisualizationConfig } from './timelion_vis_fn';
|
||||
import { getTimelionVisDefinition } from './timelion_vis_type';
|
||||
|
@ -36,6 +37,7 @@ export interface TimelionVisDependencies extends Partial<CoreStart> {
|
|||
uiSettings: IUiSettingsClient;
|
||||
http: HttpSetup;
|
||||
timefilter: TimefilterContract;
|
||||
chartTheme: ChartsPluginSetup['theme'];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
@ -43,6 +45,7 @@ export interface TimelionVisSetupDependencies {
|
|||
expressions: ReturnType<ExpressionsPlugin['setup']>;
|
||||
visualizations: VisualizationsSetup;
|
||||
data: DataPublicPluginSetup;
|
||||
charts: ChartsPluginSetup;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
@ -72,13 +75,14 @@ export class TimelionVisPlugin
|
|||
constructor(public initializerContext: PluginInitializerContext<ConfigSchema>) {}
|
||||
|
||||
public setup(
|
||||
core: CoreSetup,
|
||||
{ expressions, visualizations, data }: TimelionVisSetupDependencies
|
||||
{ uiSettings, http }: CoreSetup,
|
||||
{ expressions, visualizations, data, charts }: TimelionVisSetupDependencies
|
||||
) {
|
||||
const dependencies: TimelionVisDependencies = {
|
||||
uiSettings: core.uiSettings,
|
||||
http: core.http,
|
||||
http,
|
||||
uiSettings,
|
||||
timefilter: data.query.timefilter.timefilter,
|
||||
chartTheme: charts.theme,
|
||||
};
|
||||
|
||||
expressions.registerFunction(() => getTimelionVisualizationConfig(dependencies));
|
||||
|
|
|
@ -14,8 +14,11 @@ import { KibanaContextProvider } from '../../kibana_react/public';
|
|||
import { VisualizationContainer } from '../../visualizations/public';
|
||||
import { TimelionVisDependencies } from './plugin';
|
||||
import { TimelionRenderValue } from './timelion_vis_fn';
|
||||
// @ts-ignore
|
||||
import { UI_SETTINGS } from '../common/constants';
|
||||
import { RangeFilterParams } from '../../data/public';
|
||||
|
||||
const TimelionVisComponent = lazy(() => import('./components/timelion_vis_component'));
|
||||
const TimelionVisLegacyComponent = lazy(() => import('./legacy/timelion_vis_component'));
|
||||
|
||||
export const getTimelionVisRenderer: (
|
||||
deps: TimelionVisDependencies
|
||||
|
@ -31,14 +34,34 @@ export const getTimelionVisRenderer: (
|
|||
const [seriesList] = visData.sheet;
|
||||
const showNoResult = !seriesList || !seriesList.list.length;
|
||||
|
||||
const VisComponent = deps.uiSettings.get(UI_SETTINGS.LEGACY_CHARTS_LIBRARY, false)
|
||||
? TimelionVisLegacyComponent
|
||||
: TimelionVisComponent;
|
||||
|
||||
const onBrushEvent = (rangeFilterParams: RangeFilterParams) => {
|
||||
handlers.event({
|
||||
name: 'applyFilter',
|
||||
data: {
|
||||
timeFieldName: '*',
|
||||
filters: [
|
||||
{
|
||||
range: {
|
||||
'*': rangeFilterParams,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
render(
|
||||
<VisualizationContainer handlers={handlers} showNoResult={showNoResult}>
|
||||
<KibanaContextProvider services={{ ...deps }}>
|
||||
<TimelionVisComponent
|
||||
<VisComponent
|
||||
interval={visParams.interval}
|
||||
seriesList={seriesList}
|
||||
renderComplete={handlers.done}
|
||||
fireEvent={handlers.event}
|
||||
onBrushEvent={onBrushEvent}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
</VisualizationContainer>,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { TypeOf, schema } from '@kbn/config-schema';
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { RecursiveReadonly } from '@kbn/utility-types';
|
||||
import { deepFreeze } from '@kbn/std';
|
||||
|
||||
|
@ -19,10 +19,7 @@ import { functionsRoute } from './routes/functions';
|
|||
import { validateEsRoute } from './routes/validate_es';
|
||||
import { runRoute } from './routes/run';
|
||||
import { ConfigManager } from './lib/config_manager';
|
||||
|
||||
const experimentalLabel = i18n.translate('timelion.uiSettings.experimentalLabel', {
|
||||
defaultMessage: 'experimental',
|
||||
});
|
||||
import { getUiSettings } from './ui_settings';
|
||||
|
||||
/**
|
||||
* Describes public Timelion plugin contract returned at the `setup` stage.
|
||||
|
@ -78,97 +75,7 @@ export class TimelionPlugin
|
|||
runRoute(router, deps);
|
||||
validateEsRoute(router);
|
||||
|
||||
core.uiSettings.register({
|
||||
'timelion:es.timefield': {
|
||||
name: i18n.translate('timelion.uiSettings.timeFieldLabel', {
|
||||
defaultMessage: 'Time field',
|
||||
}),
|
||||
value: '@timestamp',
|
||||
description: i18n.translate('timelion.uiSettings.timeFieldDescription', {
|
||||
defaultMessage: 'Default field containing a timestamp when using {esParam}',
|
||||
values: { esParam: '.es()' },
|
||||
}),
|
||||
category: ['timelion'],
|
||||
schema: schema.string(),
|
||||
},
|
||||
'timelion:es.default_index': {
|
||||
name: i18n.translate('timelion.uiSettings.defaultIndexLabel', {
|
||||
defaultMessage: 'Default index',
|
||||
}),
|
||||
value: '_all',
|
||||
description: i18n.translate('timelion.uiSettings.defaultIndexDescription', {
|
||||
defaultMessage: 'Default elasticsearch index to search with {esParam}',
|
||||
values: { esParam: '.es()' },
|
||||
}),
|
||||
category: ['timelion'],
|
||||
schema: schema.string(),
|
||||
},
|
||||
'timelion:target_buckets': {
|
||||
name: i18n.translate('timelion.uiSettings.targetBucketsLabel', {
|
||||
defaultMessage: 'Target buckets',
|
||||
}),
|
||||
value: 200,
|
||||
description: i18n.translate('timelion.uiSettings.targetBucketsDescription', {
|
||||
defaultMessage: 'The number of buckets to shoot for when using auto intervals',
|
||||
}),
|
||||
category: ['timelion'],
|
||||
schema: schema.number(),
|
||||
},
|
||||
'timelion:max_buckets': {
|
||||
name: i18n.translate('timelion.uiSettings.maximumBucketsLabel', {
|
||||
defaultMessage: 'Maximum buckets',
|
||||
}),
|
||||
value: 2000,
|
||||
description: i18n.translate('timelion.uiSettings.maximumBucketsDescription', {
|
||||
defaultMessage: 'The maximum number of buckets a single datasource can return',
|
||||
}),
|
||||
category: ['timelion'],
|
||||
schema: schema.number(),
|
||||
},
|
||||
'timelion:min_interval': {
|
||||
name: i18n.translate('timelion.uiSettings.minimumIntervalLabel', {
|
||||
defaultMessage: 'Minimum interval',
|
||||
}),
|
||||
value: '1ms',
|
||||
description: i18n.translate('timelion.uiSettings.minimumIntervalDescription', {
|
||||
defaultMessage: 'The smallest interval that will be calculated when using "auto"',
|
||||
description:
|
||||
'"auto" is a technical value in that context, that should not be translated.',
|
||||
}),
|
||||
category: ['timelion'],
|
||||
schema: schema.string(),
|
||||
},
|
||||
'timelion:graphite.url': {
|
||||
name: i18n.translate('timelion.uiSettings.graphiteURLLabel', {
|
||||
defaultMessage: 'Graphite URL',
|
||||
description:
|
||||
'The URL should be in the form of https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite',
|
||||
}),
|
||||
value: config.graphiteUrls && config.graphiteUrls.length ? config.graphiteUrls[0] : null,
|
||||
description: i18n.translate('timelion.uiSettings.graphiteURLDescription', {
|
||||
defaultMessage:
|
||||
'{experimentalLabel} The <a href="https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite" target="_blank" rel="noopener">URL</a> of your graphite host',
|
||||
values: { experimentalLabel: `<em>[${experimentalLabel}]</em>` },
|
||||
}),
|
||||
type: 'select',
|
||||
options: config.graphiteUrls || [],
|
||||
category: ['timelion'],
|
||||
schema: schema.nullable(schema.string()),
|
||||
},
|
||||
'timelion:quandl.key': {
|
||||
name: i18n.translate('timelion.uiSettings.quandlKeyLabel', {
|
||||
defaultMessage: 'Quandl key',
|
||||
}),
|
||||
value: 'someKeyHere',
|
||||
description: i18n.translate('timelion.uiSettings.quandlKeyDescription', {
|
||||
defaultMessage: '{experimentalLabel} Your API key from www.quandl.com',
|
||||
values: { experimentalLabel: `<em>[${experimentalLabel}]</em>` },
|
||||
}),
|
||||
sensitive: true,
|
||||
category: ['timelion'],
|
||||
schema: schema.string(),
|
||||
},
|
||||
});
|
||||
core.uiSettings.register(getUiSettings(config));
|
||||
|
||||
return deepFreeze({ uiEnabled: config.ui.enabled });
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ export default new Chainable('label', {
|
|||
// that it doesn't prevent Kibana from starting up and we only have an issue using Timelion labels
|
||||
const RE2 = require('re2');
|
||||
eachSeries.label = eachSeries.label.replace(new RE2(config.regex), config.label);
|
||||
} else {
|
||||
} else if (config.label) {
|
||||
eachSeries.label = config.label;
|
||||
}
|
||||
|
||||
|
|
130
src/plugins/vis_type_timelion/server/ui_settings.ts
Normal file
130
src/plugins/vis_type_timelion/server/ui_settings.ts
Normal file
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import type { UiSettingsParams } from 'kibana/server';
|
||||
|
||||
import { UI_SETTINGS } from '../common/constants';
|
||||
import { configSchema } from '../config';
|
||||
|
||||
const experimentalLabel = i18n.translate('timelion.uiSettings.experimentalLabel', {
|
||||
defaultMessage: 'experimental',
|
||||
});
|
||||
|
||||
export function getUiSettings(
|
||||
config: TypeOf<typeof configSchema>
|
||||
): Record<string, UiSettingsParams<unknown>> {
|
||||
return {
|
||||
[UI_SETTINGS.LEGACY_CHARTS_LIBRARY]: {
|
||||
name: i18n.translate('timelion.uiSettings.legacyChartsLibraryLabel', {
|
||||
defaultMessage: 'Timelion legacy charts library',
|
||||
}),
|
||||
description: i18n.translate('timelion.uiSettings.legacyChartsLibraryDescription', {
|
||||
defaultMessage: 'Enables the legacy charts library for timelion charts in Visualize',
|
||||
}),
|
||||
deprecation: {
|
||||
message: i18n.translate('timelion.uiSettings.legacyChartsLibraryDeprication', {
|
||||
defaultMessage: 'This setting is deprecated and will not be supported as of 8.0.',
|
||||
}),
|
||||
docLinksKey: 'timelionSettings',
|
||||
},
|
||||
value: false,
|
||||
category: ['timelion'],
|
||||
schema: schema.boolean(),
|
||||
},
|
||||
[UI_SETTINGS.ES_TIMEFIELD]: {
|
||||
name: i18n.translate('timelion.uiSettings.timeFieldLabel', {
|
||||
defaultMessage: 'Time field',
|
||||
}),
|
||||
value: '@timestamp',
|
||||
description: i18n.translate('timelion.uiSettings.timeFieldDescription', {
|
||||
defaultMessage: 'Default field containing a timestamp when using {esParam}',
|
||||
values: { esParam: '.es()' },
|
||||
}),
|
||||
category: ['timelion'],
|
||||
schema: schema.string(),
|
||||
},
|
||||
[UI_SETTINGS.DEFAULT_INDEX]: {
|
||||
name: i18n.translate('timelion.uiSettings.defaultIndexLabel', {
|
||||
defaultMessage: 'Default index',
|
||||
}),
|
||||
value: '_all',
|
||||
description: i18n.translate('timelion.uiSettings.defaultIndexDescription', {
|
||||
defaultMessage: 'Default elasticsearch index to search with {esParam}',
|
||||
values: { esParam: '.es()' },
|
||||
}),
|
||||
category: ['timelion'],
|
||||
schema: schema.string(),
|
||||
},
|
||||
[UI_SETTINGS.TARGET_BUCKETS]: {
|
||||
name: i18n.translate('timelion.uiSettings.targetBucketsLabel', {
|
||||
defaultMessage: 'Target buckets',
|
||||
}),
|
||||
value: 200,
|
||||
description: i18n.translate('timelion.uiSettings.targetBucketsDescription', {
|
||||
defaultMessage: 'The number of buckets to shoot for when using auto intervals',
|
||||
}),
|
||||
category: ['timelion'],
|
||||
schema: schema.number(),
|
||||
},
|
||||
[UI_SETTINGS.MAX_BUCKETS]: {
|
||||
name: i18n.translate('timelion.uiSettings.maximumBucketsLabel', {
|
||||
defaultMessage: 'Maximum buckets',
|
||||
}),
|
||||
value: 2000,
|
||||
description: i18n.translate('timelion.uiSettings.maximumBucketsDescription', {
|
||||
defaultMessage: 'The maximum number of buckets a single datasource can return',
|
||||
}),
|
||||
category: ['timelion'],
|
||||
schema: schema.number(),
|
||||
},
|
||||
[UI_SETTINGS.MIN_INTERVAL]: {
|
||||
name: i18n.translate('timelion.uiSettings.minimumIntervalLabel', {
|
||||
defaultMessage: 'Minimum interval',
|
||||
}),
|
||||
value: '1ms',
|
||||
description: i18n.translate('timelion.uiSettings.minimumIntervalDescription', {
|
||||
defaultMessage: 'The smallest interval that will be calculated when using "auto"',
|
||||
description: '"auto" is a technical value in that context, that should not be translated.',
|
||||
}),
|
||||
category: ['timelion'],
|
||||
schema: schema.string(),
|
||||
},
|
||||
[UI_SETTINGS.GRAPHITE_URL]: {
|
||||
name: i18n.translate('timelion.uiSettings.graphiteURLLabel', {
|
||||
defaultMessage: 'Graphite URL',
|
||||
description:
|
||||
'The URL should be in the form of https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite',
|
||||
}),
|
||||
value: config.graphiteUrls && config.graphiteUrls.length ? config.graphiteUrls[0] : null,
|
||||
description: i18n.translate('timelion.uiSettings.graphiteURLDescription', {
|
||||
defaultMessage:
|
||||
'{experimentalLabel} The <a href="https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite" target="_blank" rel="noopener">URL</a> of your graphite host',
|
||||
values: { experimentalLabel: `<em>[${experimentalLabel}]</em>` },
|
||||
}),
|
||||
type: 'select',
|
||||
options: config.graphiteUrls || [],
|
||||
category: ['timelion'],
|
||||
schema: schema.nullable(schema.string()),
|
||||
},
|
||||
[UI_SETTINGS.QUANDL_KEY]: {
|
||||
name: i18n.translate('timelion.uiSettings.quandlKeyLabel', {
|
||||
defaultMessage: 'Quandl key',
|
||||
}),
|
||||
value: 'someKeyHere',
|
||||
description: i18n.translate('timelion.uiSettings.quandlKeyDescription', {
|
||||
defaultMessage: '{experimentalLabel} Your API key from www.quandl.com',
|
||||
values: { experimentalLabel: `<em>[${experimentalLabel}]</em>` },
|
||||
}),
|
||||
sensitive: true,
|
||||
category: ['timelion'],
|
||||
schema: schema.string(),
|
||||
},
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue