Skip to content
This repository has been archived by the owner on Dec 10, 2021. It is now read-only.

[chart plugin]TableVis Plugin #66

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/superset-ui-plugin-table/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## @superset-ui/plugin-table

[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-table.svg?style=flat)](https://img.shields.io/npm/v/@superset-ui/plugin-table.svg?style=flat)
[![David (path)](https://img.shields.io/david/apache-superset/superset-ui.svg?path=packages%2Fsuperset-ui-plugin-table&style=flat-square)](https://david-dm.org/apache-superset/superset-ui?path=packages/superset-ui-plugin-table)

Description

#### Example usage

```js
import { xxx } from '@superset-ui/plugin-table';
```

#### API

`fn(args)`

- Do something

### Development

`@data-ui/build-config` is used to manage the build configuration for this package including babel
builds, jest testing, eslint, and prettier.
26 changes: 26 additions & 0 deletions packages/superset-ui-plugin-table/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@superset-ui/plugin-table",
"version": "0.0.0",
"description": "Superset UI plugin-table",
"sideEffects": false,
"main": "lib/index.js",
"module": "esm/index.js",
"files": [
"esm",
"lib"
],
"repository": {
"type": "git",
"url": "git+https://github.com/apache-superset/superset-ui.git"
},
"keywords": ["superset"],
"author": "Superset",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/apache-superset/superset-ui/issues"
},
"homepage": "https://github.com/apache-superset/superset-ui#readme",
"publishConfig": {
"access": "public"
}
}
4 changes: 4 additions & 0 deletions packages/superset-ui-plugin-table/src/ReactTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import reactify from '../../utils/reactify';
import Component from './Table';

export default reactify(Component);
13 changes: 13 additions & 0 deletions packages/superset-ui-plugin-table/src/Table.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.slice_container.table table.table {
margin: 0px !important;
background: transparent;
background-color: white;
}

table.table thead th.sorting:after, table.table thead th.sorting_asc:after, table.table thead th.sorting_desc:after {
top: 0px;
}

.like-pre {
white-space: pre-wrap;
}
236 changes: 236 additions & 0 deletions packages/superset-ui-plugin-table/src/Table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import d3 from 'd3';
import $ from 'jquery';
import PropTypes from 'prop-types';
import dt from 'datatables.net-bs';
import 'datatables.net-bs/css/dataTables.bootstrap.css';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we may not be able to import this type of file in babel.

if we refactor datatables and still need to import css, we could instead copy it in the lib output, and have incubator superset import (since it has proper css loader support)

import dompurify from 'dompurify';
import { getNumberFormatter, NumberFormats } from '@superset-ui/number-format';
import { getTimeFormatter } from '@superset-ui/time-format';
import { fixDataTableBodyHeight } from '../../modules/utils';
import './Table.css';

dt(window, $);

const propTypes = {
// Each object is { field1: value1, field2: value2 }
data: PropTypes.arrayOf(PropTypes.object),
height: PropTypes.number,
alignPositiveNegative: PropTypes.bool,
colorPositiveNegative: PropTypes.bool,
columns: PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.string,
label: PropTypes.string,
format: PropTypes.string,
}),
),
filters: PropTypes.object,
includeSearch: PropTypes.bool,
metrics: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object])),
onAddFilter: PropTypes.func,
onRemoveFilter: PropTypes.func,
orderDesc: PropTypes.bool,
pageLength: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
percentMetrics: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object])),
tableFilter: PropTypes.bool,
tableTimestampFormat: PropTypes.string,
timeseriesLimitMetric: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
};

const formatValue = getNumberFormatter(NumberFormats.INTEGER);
const formatPercent = getNumberFormatter(NumberFormats.PERCENT_3_POINT);
function NOOP() {}

function TableVis(element, props) {
const {
data,
height,
alignPositiveNegative = false,
colorPositiveNegative = false,
columns,
filters = {},
includeSearch = false,
metrics: rawMetrics,
onAddFilter = NOOP,
onRemoveFilter = NOOP,
orderDesc,
pageLength,
percentMetrics,
tableFilter,
tableTimestampFormat,
timeseriesLimitMetric,
} = props;

const $container = $(element);

const metrics = (rawMetrics || [])
.map(m => m.label || m)
// Add percent metrics
.concat((percentMetrics || []).map(m => '%' + m))
// Removing metrics (aggregates) that are strings
.filter(m => typeof data[0][m] === 'number');

function col(c) {
const arr = [];
for (let i = 0; i < data.length; i += 1) {
arr.push(data[i][c]);
}
return arr;
}
const maxes = {};
const mins = {};
for (let i = 0; i < metrics.length; i += 1) {
if (alignPositiveNegative) {
maxes[metrics[i]] = d3.max(col(metrics[i]).map(Math.abs));
} else {
maxes[metrics[i]] = d3.max(col(metrics[i]));
mins[metrics[i]] = d3.min(col(metrics[i]));
}
}

const tsFormatter = getTimeFormatter(tableTimestampFormat);

const div = d3.select(element);
div.html('');
const table = div
.append('table')
.classed(
'dataframe dataframe table table-striped ' +
'table-condensed table-hover dataTable no-footer',
true,
)
.attr('width', '100%');

table
.append('thead')
.append('tr')
.selectAll('th')
.data(columns.map(c => c.label))
.enter()
.append('th')
.text(d => d);

table
.append('tbody')
.selectAll('tr')
.data(data)
.enter()
.append('tr')
.selectAll('td')
.data(row =>
columns.map(({ key, format }) => {
const val = row[key];
let html;
const isMetric = metrics.indexOf(key) >= 0;
if (key === '__timestamp') {
html = tsFormatter(val);
}
if (typeof val === 'string') {
html = `<span class="like-pre">${dompurify.sanitize(val)}</span>`;
}
if (isMetric) {
html = getNumberFormatter(format)(val);
}
if (key[0] === '%') {
html = formatPercent(val);
}

return {
col: key,
val,
html,
isMetric,
};
}),
)
.enter()
.append('td')
.style('background-image', function(d) {
if (d.isMetric) {
const r = colorPositiveNegative && d.val < 0 ? 150 : 0;
if (alignPositiveNegative) {
const perc = Math.abs(Math.round((d.val / maxes[d.col]) * 100));
// The 0.01 to 0.001 is a workaround for what appears to be a
// CSS rendering bug on flat, transparent colors
return (
`linear-gradient(to right, rgba(${r},0,0,0.2), rgba(${r},0,0,0.2) ${perc}%, ` +
`rgba(0,0,0,0.01) ${perc}%, rgba(0,0,0,0.001) 100%)`
);
}
const posExtent = Math.abs(Math.max(maxes[d.col], 0));
const negExtent = Math.abs(Math.min(mins[d.col], 0));
const tot = posExtent + negExtent;
const perc1 = Math.round((Math.min(negExtent + d.val, negExtent) / tot) * 100);
const perc2 = Math.round((Math.abs(d.val) / tot) * 100);
// The 0.01 to 0.001 is a workaround for what appears to be a
// CSS rendering bug on flat, transparent colors
return (
`linear-gradient(to right, rgba(0,0,0,0.01), rgba(0,0,0,0.001) ${perc1}%, ` +
`rgba(${r},0,0,0.2) ${perc1}%, rgba(${r},0,0,0.2) ${perc1 + perc2}%, ` +
`rgba(0,0,0,0.01) ${perc1 + perc2}%, rgba(0,0,0,0.001) 100%)`
);
}
return null;
})
.classed('text-right', d => d.isMetric)
.attr('title', d => (!Number.isNaN(d.val) ? formatValue(d.val) : null))
.attr('data-sort', d => (d.isMetric ? d.val : null))
// Check if the dashboard currently has a filter for each row
.classed('filtered', d => filters && filters[d.col] && filters[d.col].indexOf(d.val) >= 0)
.on('click', function(d) {
if (!d.isMetric && tableFilter) {
const td = d3.select(this);
if (td.classed('filtered')) {
onRemoveFilter(d.col, [d.val]);
d3.select(this).classed('filtered', false);
} else {
d3.select(this).classed('filtered', true);
onAddFilter(d.col, [d.val]);
}
}
})
.style('cursor', d => (!d.isMetric ? 'pointer' : ''))
.html(d => (d.html ? d.html : d.val));

const paging = pageLength && pageLength > 0;

const datatable = $container.find('.dataTable').DataTable({
paging,
pageLength,
aaSorting: [],
searching: includeSearch,
bInfo: false,
scrollY: `${height}px`,
scrollCollapse: true,
scrollX: true,
});

fixDataTableBodyHeight($container.find('.dataTables_wrapper'), height);
// Sorting table by main column
let sortBy;
const limitMetric = Array.isArray(timeseriesLimitMetric)
? timeseriesLimitMetric[0]
: timeseriesLimitMetric;
if (limitMetric) {
// Sort by as specified
sortBy = limitMetric.label || limitMetric;
} else if (metrics.length > 0) {
// If not specified, use the first metric from the list
sortBy = metrics[0];
}
if (sortBy) {
const keys = columns.map(c => c.key);
const index = keys.indexOf(sortBy);
datatable.column(index).order(orderDesc ? 'desc' : 'asc');
if (metrics.indexOf(sortBy) < 0) {
// Hiding the sortBy column if not in the metrics list
datatable.column(index).visible(false);
}
}
datatable.draw();
}

TableVis.displayName = 'TableVis';
TableVis.propTypes = propTypes;

export default TableVis;
22 changes: 22 additions & 0 deletions packages/superset-ui-plugin-table/src/TableChartPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
import { ANNOTATION_TYPES } from '../../modules/AnnotationTypes';

const metadata = new ChartMetadata({
name: t('Table'),
description: '',
canBeAnnotationTypes: [ANNOTATION_TYPES.EVENT, ANNOTATION_TYPES.INTERVAL],
thumbnail,
});

export default class TableChartPlugin extends ChartPlugin {
constructor() {
super({
metadata,
transformProps,
loadChart: () => import('./ReactTable.js'),
});
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions packages/superset-ui-plugin-table/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const x = 1;
export default x;
53 changes: 53 additions & 0 deletions packages/superset-ui-plugin-table/src/transformProps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
export default function transformProps(chartProps) {
const { height, datasource, filters, formData, onAddFilter, payload } = chartProps;
const {
alignPn,
colorPn,
includeSearch,
metrics,
orderDesc,
pageLength,
percentMetrics,
tableFilter,
tableTimestampFormat,
timeseriesLimitMetric,
} = formData;
const { columnFormats, verboseMap } = datasource;
const { records, columns } = payload.data;

const processedColumns = columns.map(key => {
let label = verboseMap[key];
// Handle verbose names for percents
if (!label) {
if (key[0] === '%') {
const cleanedKey = key.substring(1);
label = '% ' + (verboseMap[cleanedKey] || cleanedKey);
} else {
label = key;
}
}
return {
key,
label,
format: columnFormats && columnFormats[key],
};
});

return {
height,
data: records,
alignPositiveNegative: alignPn,
colorPositiveNegative: colorPn,
columns: processedColumns,
filters,
includeSearch,
metrics,
onAddFilter,
orderDesc,
pageLength: pageLength && parseInt(pageLength, 10),
percentMetrics,
tableFilter,
tableTimestampFormat,
timeseriesLimitMetric,
};
}
5 changes: 5 additions & 0 deletions packages/superset-ui-plugin-table/test/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
describe('My Test', () => {
it('tests something', () => {
expect(1).toEqual(1);
});
});