in src/plugins/vis_type_timeline/public/components/timeline_vis_component.tsx [95:455]
function TimelineVisComponent({
interval,
seriesList,
renderComplete,
fireEvent,
}: TimelineVisComponentProps) {
const opensearchDashboards = useOpenSearchDashboards<TimelineVisDependencies>();
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,
opensearchDashboards.services.timefilter,
opensearchDashboards.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,
opensearchDashboards.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 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);
};
}, [plot, plotHover]);
const mouseLeaveHandler = useCallback(() => {
if (!plot) {
return;
}
(plot as CrosshairPlot).clearCrosshair();
clearLegendNumbers();
}, [plot, clearLegendNumbers]);
const plotSelectedHandler = useCallback(
(event: JQuery.TriggeredEvent, ranges: Ranges) => {
fireEvent({
name: 'applyFilter',
data: {
timeFieldName: '*',
filters: [
{
range: {
'*': {
gte: ranges.xaxis.from,
lte: ranges.xaxis.to,
},
},
},
],
},
});
},
[fireEvent]
);
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>
);
}