Legend.tsx 8.8 KB

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