Legend.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. import _ from 'lodash';
  2. import React, { PureComponent } from 'react';
  3. import { TimeSeries } from 'app/core/core';
  4. import { CustomScrollbar } from '@grafana/ui';
  5. import { LegendItem, LEGEND_STATS } from './LegendSeriesItem';
  6. type Sort = 'min' | 'max' | 'avg' | 'current' | 'total';
  7. interface LegendProps {
  8. seriesList: TimeSeries[];
  9. optionalClass?: string;
  10. }
  11. interface LegendEventHandlers {
  12. onToggleSeries?: (hiddenSeries: any) => void;
  13. onToggleSort?: (sortBy: any, sortDesc: any) => void;
  14. onToggleAxis?: (series: TimeSeries) => void;
  15. onColorChange?: (series: TimeSeries, color: string) => void;
  16. }
  17. interface LegendComponentEventHandlers {
  18. onToggleSeries?: (series: TimeSeries, event: any) => void;
  19. onToggleSort?: (sortBy: Sort, sortDesc: any) => void;
  20. onToggleAxis?: (series: TimeSeries) => void;
  21. onColorChange?: (series: TimeSeries, color: string) => void;
  22. }
  23. interface LegendDisplayProps {
  24. hiddenSeries: any;
  25. hideEmpty?: boolean;
  26. hideZero?: boolean;
  27. alignAsTable?: boolean;
  28. rightSide?: boolean;
  29. sideWidth?: number;
  30. }
  31. interface LegendValuesProps {
  32. values?: boolean;
  33. min?: boolean;
  34. max?: boolean;
  35. avg?: boolean;
  36. current?: boolean;
  37. total?: boolean;
  38. }
  39. interface LegendSortProps {
  40. sort?: Sort;
  41. sortDesc?: boolean;
  42. }
  43. export type GraphLegendProps = LegendProps &
  44. LegendDisplayProps &
  45. LegendValuesProps &
  46. LegendSortProps &
  47. LegendEventHandlers;
  48. export type LegendComponentProps = LegendProps &
  49. LegendDisplayProps &
  50. LegendValuesProps &
  51. LegendSortProps &
  52. LegendComponentEventHandlers;
  53. interface LegendState {
  54. hiddenSeries: { [seriesAlias: string]: boolean };
  55. }
  56. export class GraphLegend extends PureComponent<GraphLegendProps, LegendState> {
  57. static defaultProps: Partial<GraphLegendProps> = {
  58. values: false,
  59. min: false,
  60. max: false,
  61. avg: false,
  62. current: false,
  63. total: false,
  64. alignAsTable: false,
  65. rightSide: false,
  66. sort: undefined,
  67. sortDesc: false,
  68. optionalClass: '',
  69. onToggleSeries: () => {},
  70. onToggleSort: () => {},
  71. onToggleAxis: () => {},
  72. onColorChange: () => {},
  73. };
  74. constructor(props: GraphLegendProps) {
  75. super(props);
  76. this.state = {
  77. hiddenSeries: this.props.hiddenSeries,
  78. };
  79. }
  80. sortLegend() {
  81. let seriesList: TimeSeries[] = [...this.props.seriesList] || [];
  82. if (this.props.sort && this.props[this.props.sort] && this.props.alignAsTable) {
  83. seriesList = _.sortBy(seriesList, series => {
  84. let sort = series.stats[this.props.sort];
  85. if (sort === null) {
  86. sort = -Infinity;
  87. }
  88. return sort;
  89. }) as TimeSeries[];
  90. if (this.props.sortDesc) {
  91. seriesList = seriesList.reverse();
  92. }
  93. }
  94. return seriesList;
  95. }
  96. onToggleSeries = (series: TimeSeries, event: any) => {
  97. let hiddenSeries = { ...this.state.hiddenSeries };
  98. if (event.ctrlKey || event.metaKey || event.shiftKey) {
  99. if (hiddenSeries[series.alias]) {
  100. delete hiddenSeries[series.alias];
  101. } else {
  102. hiddenSeries[series.alias] = true;
  103. }
  104. } else {
  105. hiddenSeries = this.toggleSeriesExclusiveMode(series);
  106. }
  107. this.setState({ hiddenSeries: hiddenSeries });
  108. this.props.onToggleSeries(hiddenSeries);
  109. };
  110. toggleSeriesExclusiveMode(series: TimeSeries) {
  111. const hiddenSeries = { ...this.state.hiddenSeries };
  112. if (hiddenSeries[series.alias]) {
  113. delete hiddenSeries[series.alias];
  114. }
  115. // check if every other series is hidden
  116. const alreadyExclusive = this.props.seriesList.every(value => {
  117. if (value.alias === series.alias) {
  118. return true;
  119. }
  120. return hiddenSeries[value.alias];
  121. });
  122. if (alreadyExclusive) {
  123. // remove all hidden series
  124. this.props.seriesList.forEach(value => {
  125. delete hiddenSeries[value.alias];
  126. });
  127. } else {
  128. // hide all but this serie
  129. this.props.seriesList.forEach(value => {
  130. if (value.alias === series.alias) {
  131. return;
  132. }
  133. hiddenSeries[value.alias] = true;
  134. });
  135. }
  136. return hiddenSeries;
  137. }
  138. render() {
  139. const {
  140. optionalClass,
  141. rightSide,
  142. sideWidth,
  143. sort,
  144. sortDesc,
  145. hideEmpty,
  146. hideZero,
  147. values,
  148. min,
  149. max,
  150. avg,
  151. current,
  152. total,
  153. } = this.props;
  154. const seriesValuesProps = { values, min, max, avg, current, total };
  155. const hiddenSeries = this.state.hiddenSeries;
  156. const seriesHideProps = { hideEmpty, hideZero };
  157. const sortProps = { sort, sortDesc };
  158. const seriesList = this.sortLegend().filter(series => !series.hideFromLegend(seriesHideProps));
  159. const legendClass = `${this.props.alignAsTable ? 'graph-legend-table' : ''} ${optionalClass}`;
  160. // Set min-width if side style and there is a value, otherwise remove the CSS property
  161. // Set width so it works with IE11
  162. const width: any = rightSide && sideWidth ? sideWidth : undefined;
  163. const ieWidth: any = rightSide && sideWidth ? sideWidth - 1 : undefined;
  164. const legendStyle: React.CSSProperties = {
  165. minWidth: width,
  166. width: ieWidth,
  167. };
  168. const legendProps: LegendComponentProps = {
  169. seriesList: seriesList,
  170. hiddenSeries: hiddenSeries,
  171. onToggleSeries: this.onToggleSeries,
  172. onToggleAxis: this.props.onToggleAxis,
  173. onToggleSort: this.props.onToggleSort,
  174. onColorChange: this.props.onColorChange,
  175. ...seriesValuesProps,
  176. ...sortProps,
  177. };
  178. return (
  179. <div className={`graph-legend-content ${legendClass}`} style={legendStyle}>
  180. {this.props.alignAsTable ? <LegendTable {...legendProps} /> : <LegendSeriesList {...legendProps} />}
  181. </div>
  182. );
  183. }
  184. }
  185. class LegendSeriesList extends PureComponent<LegendComponentProps> {
  186. render() {
  187. const { seriesList, hiddenSeries, values, min, max, avg, current, total } = this.props;
  188. const seriesValuesProps = { values, min, max, avg, current, total };
  189. return seriesList.map((series, i) => (
  190. <LegendItem
  191. // This trick required because TimeSeries.id is not unique (it's just TimeSeries.alias).
  192. // In future would be good to make id unique across the series list.
  193. key={`${series.id}-${i}`}
  194. series={series}
  195. hidden={hiddenSeries[series.alias]}
  196. {...seriesValuesProps}
  197. onLabelClick={this.props.onToggleSeries}
  198. onColorChange={this.props.onColorChange}
  199. onToggleAxis={this.props.onToggleAxis}
  200. />
  201. ));
  202. }
  203. }
  204. class LegendTable extends PureComponent<Partial<LegendComponentProps>> {
  205. onToggleSort = (stat: Sort) => {
  206. let sortDesc = this.props.sortDesc;
  207. let sortBy = this.props.sort;
  208. if (stat !== sortBy) {
  209. sortDesc = null;
  210. }
  211. // if already sort ascending, disable sorting
  212. if (sortDesc === false) {
  213. sortBy = null;
  214. sortDesc = null;
  215. } else {
  216. sortDesc = !sortDesc;
  217. sortBy = stat;
  218. }
  219. this.props.onToggleSort(sortBy, sortDesc);
  220. };
  221. render() {
  222. const seriesList = this.props.seriesList;
  223. const { values, min, max, avg, current, total, sort, sortDesc, hiddenSeries } = this.props;
  224. const seriesValuesProps: any = { values, min, max, avg, current, total };
  225. return (
  226. <table>
  227. <colgroup>
  228. <col style={{ width: '100%' }} />
  229. </colgroup>
  230. <thead>
  231. <tr>
  232. <th style={{ textAlign: 'left' }} />
  233. {LEGEND_STATS.map(
  234. statName =>
  235. seriesValuesProps[statName] && (
  236. <LegendTableHeaderItem
  237. key={statName}
  238. statName={statName}
  239. sort={sort}
  240. sortDesc={sortDesc}
  241. onClick={this.onToggleSort}
  242. />
  243. )
  244. )}
  245. </tr>
  246. </thead>
  247. <tbody>
  248. {seriesList.map((series, i) => (
  249. <LegendItem
  250. key={`${series.id}-${i}`}
  251. asTable={true}
  252. series={series}
  253. hidden={hiddenSeries[series.alias]}
  254. onLabelClick={this.props.onToggleSeries}
  255. onColorChange={this.props.onColorChange}
  256. onToggleAxis={this.props.onToggleAxis}
  257. {...seriesValuesProps}
  258. />
  259. ))}
  260. </tbody>
  261. </table>
  262. );
  263. }
  264. }
  265. interface LegendTableHeaderProps {
  266. statName: string;
  267. onClick?: (statName: string) => void;
  268. }
  269. class LegendTableHeaderItem extends PureComponent<LegendTableHeaderProps & LegendSortProps> {
  270. onClick = () => this.props.onClick(this.props.statName);
  271. render() {
  272. const { statName, sort, sortDesc } = this.props;
  273. return (
  274. <th className="pointer" onClick={this.onClick}>
  275. {statName}
  276. {sort === statName && <span className={sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up'} />}
  277. </th>
  278. );
  279. }
  280. }
  281. export class Legend extends PureComponent<GraphLegendProps> {
  282. render() {
  283. return (
  284. <CustomScrollbar hideHorizontalTrack>
  285. <GraphLegend {...this.props} />
  286. </CustomScrollbar>
  287. );
  288. }
  289. }
  290. export default Legend;