PieChartPanel.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import React, { useEffect, useState } from 'react';
  2. import { Subscription } from 'rxjs';
  3. import {
  4. DataHoverClearEvent,
  5. DataHoverEvent,
  6. FALLBACK_COLOR,
  7. FieldDisplay,
  8. formattedValueToString,
  9. getFieldDisplayValues,
  10. PanelProps,
  11. } from '@grafana/data';
  12. import { PanelDataErrorView } from '@grafana/runtime';
  13. import { LegendDisplayMode } from '@grafana/schema';
  14. import {
  15. SeriesVisibilityChangeBehavior,
  16. usePanelContext,
  17. useTheme2,
  18. VizLayout,
  19. VizLegend,
  20. VizLegendItem,
  21. } from '@grafana/ui';
  22. import { PieChart } from './PieChart';
  23. import { PieChartLegendOptions, PieChartLegendValues, PieChartOptions } from './types';
  24. import { filterDisplayItems, sumDisplayItemsReducer } from './utils';
  25. const defaultLegendOptions: PieChartLegendOptions = {
  26. displayMode: LegendDisplayMode.List,
  27. placement: 'right',
  28. calcs: [],
  29. values: [PieChartLegendValues.Percent],
  30. };
  31. interface Props extends PanelProps<PieChartOptions> {}
  32. /**
  33. * @beta
  34. */
  35. export function PieChartPanel(props: Props) {
  36. const { data, timeZone, fieldConfig, replaceVariables, width, height, options, id } = props;
  37. const theme = useTheme2();
  38. const highlightedTitle = useSliceHighlightState();
  39. const fieldDisplayValues = getFieldDisplayValues({
  40. fieldConfig,
  41. reduceOptions: options.reduceOptions,
  42. data: data.series,
  43. theme: theme,
  44. replaceVariables,
  45. timeZone,
  46. });
  47. if (!hasFrames(fieldDisplayValues)) {
  48. return <PanelDataErrorView panelId={id} fieldConfig={fieldConfig} data={data} />;
  49. }
  50. return (
  51. <VizLayout width={width} height={height} legend={getLegend(props, fieldDisplayValues)}>
  52. {(vizWidth: number, vizHeight: number) => {
  53. return (
  54. <PieChart
  55. width={vizWidth}
  56. height={vizHeight}
  57. highlightedTitle={highlightedTitle}
  58. fieldDisplayValues={fieldDisplayValues}
  59. tooltipOptions={options.tooltip}
  60. pieType={options.pieType}
  61. displayLabels={options.displayLabels}
  62. />
  63. );
  64. }}
  65. </VizLayout>
  66. );
  67. }
  68. function getLegend(props: Props, displayValues: FieldDisplay[]) {
  69. const legendOptions = props.options.legend ?? defaultLegendOptions;
  70. if (legendOptions.displayMode === LegendDisplayMode.Hidden) {
  71. return undefined;
  72. }
  73. const total = displayValues.filter(filterDisplayItems).reduce(sumDisplayItemsReducer, 0);
  74. const legendItems = displayValues
  75. // Since the pie chart is always sorted, let's sort the legend as well.
  76. .sort((a, b) => {
  77. if (isNaN(a.display.numeric)) {
  78. return 1;
  79. } else if (isNaN(b.display.numeric)) {
  80. return -1;
  81. } else {
  82. return b.display.numeric - a.display.numeric;
  83. }
  84. })
  85. .map<VizLegendItem>((value, idx) => {
  86. const hidden = value.field.custom.hideFrom.viz;
  87. const display = value.display;
  88. return {
  89. label: display.title ?? '',
  90. color: display.color ?? FALLBACK_COLOR,
  91. yAxis: 1,
  92. disabled: hidden,
  93. getItemKey: () => (display.title ?? '') + idx,
  94. getDisplayValues: () => {
  95. const valuesToShow = legendOptions.values ?? [];
  96. let displayValues = [];
  97. if (valuesToShow.includes(PieChartLegendValues.Value)) {
  98. displayValues.push({ numeric: display.numeric, text: formattedValueToString(display), title: 'Value' });
  99. }
  100. if (valuesToShow.includes(PieChartLegendValues.Percent)) {
  101. const fractionOfTotal = hidden ? 0 : display.numeric / total;
  102. const percentOfTotal = fractionOfTotal * 100;
  103. displayValues.push({
  104. numeric: fractionOfTotal,
  105. percent: percentOfTotal,
  106. text:
  107. hidden || isNaN(fractionOfTotal)
  108. ? props.fieldConfig.defaults.noValue ?? '-'
  109. : percentOfTotal.toFixed(value.field.decimals ?? 0) + '%',
  110. title: valuesToShow.length > 1 ? 'Percent' : '',
  111. });
  112. }
  113. return displayValues;
  114. },
  115. };
  116. });
  117. return (
  118. <VizLegend
  119. items={legendItems}
  120. seriesVisibilityChangeBehavior={SeriesVisibilityChangeBehavior.Hide}
  121. placement={legendOptions.placement}
  122. displayMode={legendOptions.displayMode}
  123. />
  124. );
  125. }
  126. function hasFrames(fieldDisplayValues: FieldDisplay[]) {
  127. return fieldDisplayValues.some((fd) => fd.view?.dataFrame.length);
  128. }
  129. function useSliceHighlightState() {
  130. const [highlightedTitle, setHighlightedTitle] = useState<string>();
  131. const { eventBus } = usePanelContext();
  132. useEffect(() => {
  133. const setHighlightedSlice = (event: DataHoverEvent) => {
  134. setHighlightedTitle(event.payload.dataId);
  135. };
  136. const resetHighlightedSlice = (event: DataHoverClearEvent) => {
  137. setHighlightedTitle(undefined);
  138. };
  139. const subs = new Subscription();
  140. subs.add(eventBus.getStream(DataHoverEvent).subscribe({ next: setHighlightedSlice }));
  141. subs.add(eventBus.getStream(DataHoverClearEvent).subscribe({ next: resetHighlightedSlice }));
  142. return () => {
  143. subs.unsubscribe();
  144. };
  145. }, [setHighlightedTitle, eventBus]);
  146. return highlightedTitle;
  147. }