ExploreGraph.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import { css, cx } from '@emotion/css';
  2. import { identity } from 'lodash';
  3. import React, { useEffect, useMemo, useRef, useState } from 'react';
  4. import { usePrevious } from 'react-use';
  5. import {
  6. AbsoluteTimeRange,
  7. applyFieldOverrides,
  8. compareArrayValues,
  9. compareDataFrameStructures,
  10. createFieldConfigRegistry,
  11. DataFrame,
  12. dateTime,
  13. FieldColorModeId,
  14. FieldConfigSource,
  15. getFrameDisplayName,
  16. GrafanaTheme2,
  17. LoadingState,
  18. SplitOpen,
  19. TimeZone,
  20. } from '@grafana/data';
  21. import { PanelRenderer } from '@grafana/runtime';
  22. import { GraphDrawStyle, LegendDisplayMode, TooltipDisplayMode, SortOrder } from '@grafana/schema';
  23. import {
  24. Icon,
  25. PanelContext,
  26. PanelContextProvider,
  27. SeriesVisibilityChangeMode,
  28. useStyles2,
  29. useTheme2,
  30. } from '@grafana/ui';
  31. import appEvents from 'app/core/app_events';
  32. import { defaultGraphConfig, getGraphFieldConfig } from 'app/plugins/panel/timeseries/config';
  33. import { TimeSeriesOptions } from 'app/plugins/panel/timeseries/types';
  34. import { ExploreGraphStyle } from '../../types';
  35. import { seriesVisibilityConfigFactory } from '../dashboard/dashgrid/SeriesVisibilityConfigFactory';
  36. import { applyGraphStyle } from './exploreGraphStyleUtils';
  37. const MAX_NUMBER_OF_TIME_SERIES = 20;
  38. interface Props {
  39. data: DataFrame[];
  40. height: number;
  41. width: number;
  42. absoluteRange: AbsoluteTimeRange;
  43. timeZone: TimeZone;
  44. loadingState: LoadingState;
  45. annotations?: DataFrame[];
  46. onHiddenSeriesChanged?: (hiddenSeries: string[]) => void;
  47. tooltipDisplayMode?: TooltipDisplayMode;
  48. splitOpenFn?: SplitOpen;
  49. onChangeTime: (timeRange: AbsoluteTimeRange) => void;
  50. graphStyle: ExploreGraphStyle;
  51. }
  52. export function ExploreGraph({
  53. data,
  54. height,
  55. width,
  56. timeZone,
  57. absoluteRange,
  58. onChangeTime,
  59. loadingState,
  60. annotations,
  61. onHiddenSeriesChanged,
  62. splitOpenFn,
  63. graphStyle,
  64. tooltipDisplayMode = TooltipDisplayMode.Single,
  65. }: Props) {
  66. const theme = useTheme2();
  67. const [showAllTimeSeries, setShowAllTimeSeries] = useState(false);
  68. const [baseStructureRev, setBaseStructureRev] = useState(1);
  69. const previousData = usePrevious(data);
  70. const structureChangesRef = useRef(0);
  71. if (data && previousData && !compareArrayValues(previousData, data, compareDataFrameStructures)) {
  72. structureChangesRef.current++;
  73. }
  74. const structureRev = baseStructureRev + structureChangesRef.current;
  75. const [fieldConfig, setFieldConfig] = useState<FieldConfigSource>({
  76. defaults: {
  77. color: {
  78. mode: FieldColorModeId.PaletteClassic,
  79. },
  80. custom: {
  81. drawStyle: GraphDrawStyle.Line,
  82. fillOpacity: 0,
  83. pointSize: 5,
  84. },
  85. },
  86. overrides: [],
  87. });
  88. const style = useStyles2(getStyles);
  89. const timeRange = {
  90. from: dateTime(absoluteRange.from),
  91. to: dateTime(absoluteRange.to),
  92. raw: {
  93. from: dateTime(absoluteRange.from),
  94. to: dateTime(absoluteRange.to),
  95. },
  96. };
  97. const dataWithConfig = useMemo(() => {
  98. const registry = createFieldConfigRegistry(getGraphFieldConfig(defaultGraphConfig), 'Explore');
  99. const styledFieldConfig = applyGraphStyle(fieldConfig, graphStyle);
  100. return applyFieldOverrides({
  101. fieldConfig: styledFieldConfig,
  102. data,
  103. timeZone,
  104. replaceVariables: (value) => value, // We don't need proper replace here as it is only used in getLinks and we use getFieldLinks
  105. theme,
  106. fieldConfigRegistry: registry,
  107. });
  108. }, [fieldConfig, graphStyle, data, timeZone, theme]);
  109. useEffect(() => {
  110. if (onHiddenSeriesChanged) {
  111. const hiddenFrames: string[] = [];
  112. dataWithConfig.forEach((frame) => {
  113. const allFieldsHidden = frame.fields.map((field) => field.config?.custom?.hideFrom?.viz).every(identity);
  114. if (allFieldsHidden) {
  115. hiddenFrames.push(getFrameDisplayName(frame));
  116. }
  117. });
  118. onHiddenSeriesChanged(hiddenFrames);
  119. }
  120. }, [dataWithConfig, onHiddenSeriesChanged]);
  121. const seriesToShow = showAllTimeSeries ? dataWithConfig : dataWithConfig.slice(0, MAX_NUMBER_OF_TIME_SERIES);
  122. const panelContext: PanelContext = {
  123. eventBus: appEvents,
  124. onSplitOpen: splitOpenFn,
  125. onToggleSeriesVisibility(label: string, mode: SeriesVisibilityChangeMode) {
  126. setBaseStructureRev((r) => r + 1);
  127. setFieldConfig(seriesVisibilityConfigFactory(label, mode, fieldConfig, data));
  128. },
  129. };
  130. return (
  131. <PanelContextProvider value={panelContext}>
  132. {dataWithConfig.length > MAX_NUMBER_OF_TIME_SERIES && !showAllTimeSeries && (
  133. <div className={cx([style.timeSeriesDisclaimer])}>
  134. <Icon className={style.disclaimerIcon} name="exclamation-triangle" />
  135. {`Showing only ${MAX_NUMBER_OF_TIME_SERIES} time series. `}
  136. <span
  137. className={cx([style.showAllTimeSeries])}
  138. onClick={() => {
  139. structureChangesRef.current++;
  140. setShowAllTimeSeries(true);
  141. }}
  142. >{`Show all ${dataWithConfig.length}`}</span>
  143. </div>
  144. )}
  145. <PanelRenderer
  146. data={{ series: seriesToShow, timeRange, structureRev, state: loadingState, annotations }}
  147. pluginId="timeseries"
  148. title=""
  149. width={width}
  150. height={height}
  151. onChangeTimeRange={onChangeTime}
  152. timeZone={timeZone}
  153. options={
  154. {
  155. tooltip: { mode: tooltipDisplayMode, sort: SortOrder.None },
  156. legend: { displayMode: LegendDisplayMode.List, placement: 'bottom', calcs: [] },
  157. } as TimeSeriesOptions
  158. }
  159. />
  160. </PanelContextProvider>
  161. );
  162. }
  163. const getStyles = (theme: GrafanaTheme2) => ({
  164. timeSeriesDisclaimer: css`
  165. label: time-series-disclaimer;
  166. width: 300px;
  167. margin: ${theme.spacing(1)} auto;
  168. padding: 10px 0;
  169. border-radius: ${theme.spacing(2)};
  170. text-align: center;
  171. background-color: ${theme.colors.background.primary};
  172. `,
  173. disclaimerIcon: css`
  174. label: disclaimer-icon;
  175. color: ${theme.colors.warning.main};
  176. margin-right: ${theme.spacing(0.5)};
  177. `,
  178. showAllTimeSeries: css`
  179. label: show-all-time-series;
  180. cursor: pointer;
  181. color: ${theme.colors.text.link};
  182. `,
  183. });