/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
import { EuiEmptyPrompt, EuiText } from '@elastic/eui';
import _ from 'lodash';
import moment from 'moment';
// @ts-ignore
import Plotly, { PlotData } from 'plotly.js-dist';
import React, { useEffect } from 'react';
import plotComponentFactory from 'react-plotly.js/factory';
import { UiSettingsClient } from 'src/core/public/ui_settings';
import { ExprVis } from 'src/plugins/visualizations/public';
import { v1 as uuid } from 'uuid';
import { GanttParams, GanttSuccessResponse } from '../gantt_vis_type';
export function GanttChart({
config,
vis,
visData,
visParams,
}: {
config: UiSettingsClient;
vis: ExprVis;
visData: GanttSuccessResponse;
visParams: GanttParams;
}) {
const PlotComponent = plotComponentFactory(Plotly);
const getGanttData = (): {
data: PlotData[];
tickvals: number[];
ticktext: string[];
yLabels: string[];
yTexts: string[];
} => {
const source: any[] = visData.source;
const data: PlotData[] = [];
if (source.length === 0) return { data, tickvals: [], ticktext: [], yLabels: [], yTexts: [] };
const getStartTime =
typeof _.get(source[0], visParams.startTimeField) === 'string'
? (document: any): number => Date.parse(_.get(document, visParams.startTimeField))
: (document: any): number => _.get(document, visParams.startTimeField);
// source is ordered by startTimeField desc, last trace in source is the earliest trace and should start at 0
const minStartTime: number = getStartTime(source[source.length - 1]);
let maxEndTime: number = 0;
source.forEach((document) => {
const rawStartTime: number = getStartTime(document);
// subtract with start time of earliest trace to get relative start time
const startTime: number = rawStartTime - minStartTime;
const duration: number = Number(_.get(document, visParams.durationField));
maxEndTime = Math.max(maxEndTime, rawStartTime + duration);
const label: string = _.get(document, visParams.labelField);
const uniqueLabel: string = label + uuid();
data.push(
{
x: [startTime],
y: [uniqueLabel],
type: 'bar',
orientation: 'h',
width: 0.4,
marker: { color: 'rgba(0, 0, 0, 0)' },
hoverinfo: 'none',
showlegend: false,
} as PlotData,
{
x: [duration],
y: [uniqueLabel],
type: 'bar',
orientation: 'h',
width: 0.4,
name: label || 'undefined',
text: [duration && duration.toString()],
hovertemplate: '%{text}',
marker: {
color: visParams.colors,
},
} as PlotData
);
});
// get unique labels from traces
const yLabels = data.map((d) => d.y[0]).filter((label, i, self) => self.indexOf(label) === i);
// remove uuid when displaying y-ticks
const yTexts = yLabels.map((label) => label.substring(0, label.length - 36));
return { data, ...getTicks(minStartTime, maxEndTime), yLabels, yTexts };
};
const getTicks = (minStartTime: number, maxEndTime: number) => {
const ticks = 5;
const interval = Math.round((maxEndTime - minStartTime) / ticks);
const tickvals = Array.from({ length: ticks }, (v, i: number) => interval * i);
const ticktext = tickvals.map((val: number) => toTimeString(val + minStartTime));
return { tickvals, ticktext };
};
const toTimeString = (val: number) => {
let divisor = 1;
const valStr = Math.floor(val).toString();
if (valStr.length <= 10)
// unit is seconds
divisor = 0.001;
else if (valStr.length <= 13)
// unit is milliseconds
divisor = 1;
else if (valStr.length <= 16)
// unit is microseconds
divisor = 1000;
else if (valStr.length <= 19)
// unit is nanoseconds
divisor = 1000 * 1000;
return moment(val / divisor).format(visParams.timeFormat);
};
const ganttData = getGanttData();
// workaround to disable 'data-loading' filter effects
useEffect(() => {
let node: Element | null = document.querySelector('#plotly-gantt-chart');
while (node && node?.tagName !== 'HTML') {
if (node?.className.includes('visEditor')) {
break;
}
if (node?.getAttribute('data-loading')) {
node.setAttribute('data-type', 'plotlyGanttChart');
break;
}
node = node?.parentElement;
}
}, []);
return (
<>
{visParams.labelField &&
visParams.startTimeField &&
visParams.durationField &&
ganttData.data.length > 0 ? (
) : visParams.labelField && visParams.startTimeField && visParams.durationField ? (
No data}
body={No data matching the selected filter.}
/>
) : (
No data}
body={
Specify data to plot the chart using the Data & Options panel
on the right.
}
/>
)}
>
);
}