UNPKG

@superset-ui/legacy-plugin-chart-time-table

Version:
275 lines (246 loc) 8.62 kB
import _pt from "prop-types"; /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import React from 'react'; import Mustache from 'mustache'; import { scaleLinear } from 'd3-scale'; import { Table, Thead, Th, Tr, Td } from 'reactable-arc'; import { formatNumber, formatTime, styled } from '@superset-ui/core'; import { InfoTooltipWithTrigger, MetricOption } from '@superset-ui/chart-controls'; import moment from 'moment'; import FormattedNumber from './FormattedNumber'; import SparklineCell from './SparklineCell'; import { jsx as ___EmotionJSX } from "@emotion/react"; const ACCESSIBLE_COLOR_BOUNDS = ['#ca0020', '#0571b0']; function colorFromBounds(value, bounds, colorBounds = ACCESSIBLE_COLOR_BOUNDS) { if (bounds) { const [min, max] = bounds; const [minColor, maxColor] = colorBounds; if (min !== null && max !== null) { const colorScale = scaleLinear().domain([min, (max + min) / 2, max]) // @ts-ignore .range([minColor, 'grey', maxColor]); // @ts-ignore return colorScale(value); } if (min !== null) { // @ts-ignore return value >= min ? maxColor : minColor; } if (max !== null) { // @ts-ignore return value < max ? maxColor : minColor; } } return null; } class TimeTable extends React.PureComponent { renderLeftCell(row) { const { rowType, url } = this.props; const context = { metric: row }; const fullUrl = url ? Mustache.render(url, context) : null; if (rowType === 'column') { const column = row; if (fullUrl) { return ___EmotionJSX("a", { href: fullUrl, rel: "noopener noreferrer", target: "_blank" }, column.label); } return column.label; } return ___EmotionJSX(MetricOption, { openInNewWindow: true, metric: row, url: fullUrl, showFormula: false }); } // eslint-disable-next-line class-methods-use-this renderSparklineCell(valueField, column, entries) { let sparkData; if (column.timeRatio) { // Period ratio sparkline sparkData = []; for (let i = column.timeRatio; i < entries.length; i += 1) { const prevData = entries[i - column.timeRatio][valueField]; if (prevData && prevData !== 0) { sparkData.push(entries[i][valueField] / prevData); } else { // @ts-ignore sparkData.push(null); } } } else { sparkData = entries.map(d => d[valueField]); } return ___EmotionJSX(Td, { key: column.key, column: column.key, value: sparkData[sparkData.length - 1] }, ___EmotionJSX(SparklineCell, { width: parseInt(column.width, 10) || 300, height: parseInt(column.height, 10) || 50, data: sparkData, ariaLabel: `spark-${valueField}`, numberFormat: column.d3format, yAxisBounds: column.yAxisBounds, showYAxis: column.showYAxis, renderTooltip: ({ index }) => ___EmotionJSX("div", null, ___EmotionJSX("strong", null, formatNumber(column.d3format, sparkData[index])), ___EmotionJSX("div", null, formatTime(column.dateFormat, moment.utc(entries[index].time).toDate()))) })); } // eslint-disable-next-line class-methods-use-this renderValueCell(valueField, column, reversedEntries) { const recent = reversedEntries[0][valueField]; let v = 0; let errorMsg; if (column.colType === 'time') { // Time lag ratio const timeLag = column.timeLag || 0; const totalLag = Object.keys(reversedEntries).length; if (timeLag >= totalLag) { errorMsg = `The time lag set at ${timeLag} is too large for the length of data at ${reversedEntries.length}. No data available.`; } else { v = reversedEntries[timeLag][valueField]; } if (column.comparisonType === 'diff') { v = recent - v; } else if (column.comparisonType === 'perc') { v = recent / v; } else if (column.comparisonType === 'perc_change') { v = recent / v - 1; } v = v || 0; } else if (column.colType === 'contrib') { // contribution to column total v = recent / Object.keys(reversedEntries[0]).map(k => k === 'time' ? 0 : reversedEntries[0][k]).reduce((a, b) => a + b); } else if (column.colType === 'avg') { // Average over the last {timeLag} v = reversedEntries.map((k, i) => i < column.timeLag ? k[valueField] : 0).reduce((a, b) => a + b) / column.timeLag; } const color = colorFromBounds(v, column.bounds); return ___EmotionJSX(Td, { key: column.key, column: column.key, value: v, style: color && { boxShadow: `inset 0px -2.5px 0px 0px ${color}`, borderRight: '2px solid #fff' } }, errorMsg ? ___EmotionJSX("div", null, errorMsg) : // @ts-ignore ___EmotionJSX("div", { style: { color } }, ___EmotionJSX(FormattedNumber, { num: v, format: column.d3format }))); } renderRow(row, entries, reversedEntries) { const { columnConfigs } = this.props; const valueField = row.label || row.metric_name; const leftCell = this.renderLeftCell(row); return ___EmotionJSX(Tr, { key: leftCell }, ___EmotionJSX(Td, { column: "metric", data: leftCell }, leftCell), columnConfigs.map(c => c.colType === 'spark' ? this.renderSparklineCell(valueField, c, entries) : this.renderValueCell(valueField, c, reversedEntries))); } render() { const { className, height, data, columnConfigs, rowType, rows } = this.props; const entries = Object.keys(data).sort() // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unsafe-return .map(time => ({ ...data[time], time })); const reversedEntries = entries.concat().reverse(); const defaultSort = rowType === 'column' && columnConfigs.length > 0 ? { column: columnConfigs[0].key, direction: 'desc' } : false; return ___EmotionJSX("div", { className: `time-table ${className}`, style: { height } }, ___EmotionJSX(Table, { className: "table table-no-hover", defaultSort: defaultSort, sortBy: defaultSort, sortable: columnConfigs.map(c => c.key) }, ___EmotionJSX(Thead, null, ___EmotionJSX(Th, { column: "metric" }, "Metric"), columnConfigs.map((c, i) => ___EmotionJSX(Th, { key: c.key, column: c.key, width: c.colType === 'spark' ? '1%' : null }, c == null ? void 0 : c.label, ' ', (c == null ? void 0 : c.tooltip) && ___EmotionJSX(InfoTooltipWithTrigger, { tooltip: c == null ? void 0 : c.tooltip, label: `tt-col-${i}`, placement: "top" })))), rows.map(row => this.renderRow(row, entries, reversedEntries)))); } } TimeTable.propTypes = { columnConfigs: _pt.arrayOf(_pt.shape({ colType: _pt.string.isRequired, comparisonType: _pt.string.isRequired, d3format: _pt.string.isRequired, key: _pt.string.isRequired, label: _pt.string.isRequired, timeLag: _pt.number.isRequired, tooltip: _pt.any.isRequired, bounds: _pt.arrayOf(_pt.number).isRequired, dateFormat: _pt.string.isRequired, width: _pt.string.isRequired, height: _pt.string.isRequired, yAxisBounds: _pt.arrayOf(_pt.number).isRequired, showYAxis: _pt.bool.isRequired, timeRatio: _pt.number.isRequired })).isRequired, data: _pt.object.isRequired, height: _pt.number.isRequired, rows: _pt.arrayOf(_pt.shape({ label: _pt.string.isRequired, metric_name: _pt.string.isRequired })).isRequired, rowType: _pt.string.isRequired, url: _pt.string.isRequired, row: _pt.arrayOf(_pt.any).isRequired }; export default styled(TimeTable)` .time-table { overflow: auto; } `;