Skip to content

Commit

Permalink
feat: add chart companion components (apache#139)
Browse files Browse the repository at this point in the history
* feat: add chart companion components

* test: add more tests

* test: improve coverage

* test: fix failed test

* test: mock ResizeObserver

* fix: test coverage

* test: fix set dimension

* feat: make keyColumn optional

* fix: address comments

* fix: test delay
  • Loading branch information
kristw authored and zhaoyongjie committed Nov 26, 2021
1 parent d6f3933 commit e5e09f9
Show file tree
Hide file tree
Showing 13 changed files with 719 additions and 0 deletions.
@@ -0,0 +1,28 @@
## @superset-ui/chart-composition

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

Description

#### Example usage

```js
import {
ChartFrame,
TooltipFrame,
TooltipTable,
WithLegend,
} from '@superset-ui/chart-composition';
```

#### 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.
@@ -0,0 +1,22 @@
const allCallbacks = [];

export default function ResizeObserver(callback) {
if (callback) {
allCallbacks.push(callback);
}

return {
disconnect: () => {
allCallbacks.splice(allCallbacks.findIndex(callback), 1);
},
observe: () => {},
};
}

const DEFAULT_OUTPUT = [{ contentRect: { height: 300, width: 300 } }];

export function triggerResizeObserver(output = DEFAULT_OUTPUT) {
allCallbacks.forEach(fn => {
fn(output);
});
}
@@ -0,0 +1,36 @@
{
"name": "@superset-ui/chart-composition",
"version": "0.0.0",
"description": "Superset UI chart-composition",
"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"
},
"private": true,
"dependencies": {
"@vx/responsive": "^0.0.184",
"@types/react": "^16.7.17",
"csstype": "^2.6.4"
},
"peerDependencies": {
"@superset-ui/core": "^0.11.0",
"react": "^15 || ^16"
}
}
@@ -0,0 +1,47 @@
import React, { PureComponent } from 'react';
import { isDefined } from '@superset-ui/core';

function checkNumber(input: any): input is number {
return isDefined(input) && typeof input === 'number';
}

type Props = {
contentWidth?: number;
contentHeight?: number;
height: number;
renderContent: ({ height, width }: { height: number; width: number }) => React.ReactNode;
width: number;
};

export default class ChartFrame extends PureComponent<Props, {}> {
static defaultProps = {
renderContent() {},
};

render() {
const { contentWidth, contentHeight, width, height, renderContent } = this.props;

const overflowX = checkNumber(contentWidth) && contentWidth > width;
const overflowY = checkNumber(contentHeight) && contentHeight > height;

if (overflowX || overflowY) {
return (
<div
style={{
height,
overflowX: overflowX ? 'auto' : 'hidden',
overflowY: overflowY ? 'auto' : 'hidden',
width,
}}
>
{renderContent({
height: Math.max(contentHeight || 0, height),
width: Math.max(contentWidth || 0, width),
})}
</div>
);
}

return renderContent({ height, width });
}
}
@@ -0,0 +1,4 @@
export { default as ChartFrame } from './ChartFrame';
export { default as WithLegend } from './legend/WithLegend';
export { default as TooltipFrame } from './tooltip/TooltipFrame';
export { default as TooltipTable } from './tooltip/TooltipTable';
@@ -0,0 +1,130 @@
/* eslint-disable sort-keys */
import React, { CSSProperties, ReactNode, PureComponent } from 'react';
import { ParentSize } from '@vx/responsive';
// eslint-disable-next-line import/no-unresolved
import { FlexDirectionProperty } from 'csstype';

const defaultProps = {
className: '',
height: 'auto' as number | string,
width: 'auto' as number | string,
position: 'top',
};

type Props = {
className: string;
debounceTime?: number;
width: number | string;
height: number | string;
legendJustifyContent?: 'center' | 'flex-start' | 'flex-end';
position: 'top' | 'left' | 'bottom' | 'right';
renderChart: (dim: { width: number; height: number }) => ReactNode;
renderLegend?: (params: { direction: string }) => ReactNode;
} & Readonly<typeof defaultProps>;

const LEGEND_STYLE_BASE: CSSProperties = {
display: 'flex',
flexGrow: 0,
flexShrink: 0,
fontSize: '0.9em',
order: -1,
paddingTop: '5px',
};

const CHART_STYLE_BASE: CSSProperties = {
flexBasis: 'auto',
flexGrow: 1,
flexShrink: 1,
position: 'relative',
};

class WithLegend extends PureComponent<Props, {}> {
static defaultProps = defaultProps;

getContainerDirection(): FlexDirectionProperty {
const { position } = this.props;

if (position === 'left') {
return 'row';
} else if (position === 'right') {
return 'row-reverse';
} else if (position === 'bottom') {
return 'column-reverse';
}

return 'column';
}

getLegendJustifyContent() {
const { legendJustifyContent, position } = this.props;
if (legendJustifyContent) {
return legendJustifyContent;
}

if (position === 'left' || position === 'right') {
return 'flex-start';
}

return 'flex-end';
}

render() {
const {
className,
debounceTime,
width,
height,
position,
renderChart,
renderLegend,
} = this.props;

const isHorizontal = position === 'left' || position === 'right';

const style: CSSProperties = {
display: 'flex',
flexDirection: this.getContainerDirection(),
height,
width,
};

const chartStyle: CSSProperties = { ...CHART_STYLE_BASE };
if (isHorizontal) {
chartStyle.width = 0;
} else {
chartStyle.height = 0;
}

const legendDirection = isHorizontal ? 'column' : 'row';
const legendStyle: CSSProperties = {
...LEGEND_STYLE_BASE,
flexDirection: legendDirection,
justifyContent: this.getLegendJustifyContent(),
};

return (
<div className={`with-legend ${className}`} style={style}>
{renderLegend && (
<div className="legend-container" style={legendStyle}>
{renderLegend({
// Pass flexDirection for @vx/legend to arrange legend items
direction: legendDirection,
})}
</div>
)}
<div className="main-container" style={chartStyle}>
<ParentSize debounceTime={debounceTime}>
{(parent: { width: number; height: number }) =>
parent.width > 0 && parent.height > 0
? // Only render when necessary
renderChart(parent)
: null
}
</ParentSize>
</div>
</div>
);
}
}

export default WithLegend;
@@ -0,0 +1,28 @@
import React, { PureComponent } from 'react';

const defaultProps = {
className: '',
};

type Props = {
className?: string;
children: React.ReactNode;
} & Readonly<typeof defaultProps>;

const CONTAINER_STYLE = { padding: 8 };

class TooltipFrame extends PureComponent<Props, {}> {
static defaultProps = defaultProps;

render() {
const { className, children } = this.props;

return (
<div className={className} style={CONTAINER_STYLE}>
{children}
</div>
);
}
}

export default TooltipFrame;
@@ -0,0 +1,44 @@
import React, { CSSProperties, PureComponent, ReactNode } from 'react';

interface TooltipRowData {
key: string | number;
keyColumn?: ReactNode;
keyStyle?: CSSProperties;
valueColumn: ReactNode;
valueStyle?: CSSProperties;
}

const defaultProps = {
className: '',
data: [] as TooltipRowData[],
};

type Props = {
className?: string;
data: TooltipRowData[];
} & Readonly<typeof defaultProps>;

const VALUE_CELL_STYLE: CSSProperties = { paddingLeft: 8, textAlign: 'right' };

export default class TooltipTable extends PureComponent<Props, {}> {
static defaultProps = defaultProps;

render() {
const { className, data } = this.props;

return (
<table className={className}>
<tbody>
{data.map(({ key, keyColumn, keyStyle, valueColumn, valueStyle }, i) => (
<tr key={key}>
<td style={keyStyle}>{keyColumn || key}</td>
<td style={valueStyle ? { ...VALUE_CELL_STYLE, ...valueStyle } : VALUE_CELL_STYLE}>
{valueColumn}
</td>
</tr>
))}
</tbody>
</table>
);
}
}

0 comments on commit e5e09f9

Please sign in to comment.