feat: add sankey chart with loops (#77)

* feat(chart): add basic working look sankey chart

* fix(doc): update readme and remove dead code

* fix(clean): clean up both style and code

* fix(reorg): reogranize for clarity

* fix(path): remove dev path to chart

* fix(lint): remove lint

* fix(pr): fixes for PR

* fix(path): fix dev path

* fix(lint): remove unused
This commit is contained in:
Robert Harris 2019-05-02 13:02:19 -07:00 committed by Yongjie Zhao
parent e2300812d7
commit c0920f6397
12 changed files with 412 additions and 0 deletions

View File

@ -0,0 +1,34 @@
## @superset-ui/legacy-plugin-chart-sankey-loop
[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-sankey.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-sankey.svg?style=flat-square)
[![David (path)](https://img.shields.io/david/apache-superset/superset-ui-plugins.svg?path=packages%2Fsuperset-ui-legacy-plugin-chart-sankey&style=flat-square)](https://david-dm.org/apache-superset/superset-ui-plugins?path=packages/superset-ui-legacy-plugin-chart-sankey)
This plugin provides Sankey Diagram with loops for Superset.
### Usage
Configure `key`, which can be any `string`, and register the plugin. This `key` will be used to lookup this chart throughout the app.
```js
import SankeyLoopChartPlugin from '@superset-ui/legacy-plugin-chart-sankey-loop';
new SankeyLoopChartPlugin()
.configure({ key: 'sankey' })
.register();
```
Then use it via `SuperChart`. See [storybook](https://apache-superset.github.io/superset-ui-plugins/?selectedKind=plugin-chart-sankey-loop) for more details.
```js
<SuperChart
chartType="sankey-loop"
chartProps={{
width: 600,
height: 600,
formData: {...},
payload: {
data: {...},
},
}}
/>
```

View File

@ -0,0 +1,41 @@
{
"name": "@superset-ui/legacy-plugin-chart-sankey-loop",
"version": "0.10.11",
"description": "Superset Legacy Chart - Sankey Diagram with Loops",
"sideEffects": [
"*.css"
],
"main": "lib/index.js",
"module": "esm/index.js",
"files": [
"esm",
"lib"
],
"repository": {
"type": "git",
"url": "git+https://github.com/apache-superset/superset-ui-plugins.git"
},
"keywords": [
"superset"
],
"author": "Superset",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/apache-superset/superset-ui-plugins/issues"
},
"homepage": "https://github.com/apache-superset/superset-ui-plugins#readme",
"publishConfig": {
"access": "public"
},
"dependencies": {
"d3-sankey-diagram": "^0.7.3",
"d3-selection": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"@superset-ui/chart": "^0.10.0 || ^0.11.0",
"@superset-ui/color": "^0.10.0 || ^0.11.0",
"@superset-ui/number-format": "^0.10.0 || ^0.11.0",
"@superset-ui/translation": "^0.10.0 || ^0.11.0"
}
}

View File

@ -0,0 +1,22 @@
/**
* 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 { reactify } from '@superset-ui/chart';
import Component from './SankeyLoop';
export default reactify(Component);

View File

@ -0,0 +1,58 @@
/**
* 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.
*/
.superset-legacy-chart-sankey-loop .node rect {
cursor: move;
fill-opacity: .9;
shape-rendering: crispEdges;
}
.superset-legacy-chart-sankey-loop .node text {
pointer-events: none;
text-shadow: 0 1px 0 #fff;
}
.superset-legacy-chart-sankey-loop .link {
fill: none;
stroke: #000;
stroke-opacity: .2;
}
.superset-legacy-chart-sankey-loop .link:hover {
stroke-opacity: .5;
}
.superset-legacy-chart-sankey-loop .link path {
opacity: .2;
stroke-opacity: 0;
}
.superset-legacy-chart-sankey-loop .link:hover path {
opacity: .5
}
.superset-legacy-chart-sankey-loop .link text {
fill: #666;
font-size: 10px
}
.superset-legacy-chart-sankey-loop .link:hover text {
opacity: 1
}

View File

@ -0,0 +1,121 @@
/**
* 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.
*/
/* eslint-disable no-param-reassign, no-magic-numbers, sort-keys, babel/no-invalid-this */
import PropTypes from 'prop-types';
import { select } from 'd3-selection';
import { sankeyDiagram, sankey } from 'd3-sankey-diagram';
import { CategoricalColorNamespace } from '@superset-ui/color';
import { getNumberFormatter, NumberFormats } from '@superset-ui/number-format';
import './SankeyLoop.css';
// a problem with 'd3-sankey-diagram' is that the sankey().extent() paramters, which
// informs the layout of the bounding box of the sankey columns, does not account
// for labels and paths which happen to be layedout outside that rectangle.
// for that reason i've selected relatively large default left/right margins, and have
// made 'margin' a property. i have raised an issue in the chart repo:
//
// https://github.com/ricklupton/d3-sankey-diagram/issues/20
const defaultMargin = {
top: 0,
right: 80,
bottom: 0,
left: 80,
};
const propTypes = {
data: PropTypes.arrayOf(
PropTypes.shape({
source: PropTypes.string,
target: PropTypes.string,
value: PropTypes.number,
}),
),
width: PropTypes.number,
height: PropTypes.number,
colorScheme: PropTypes.string,
margin: PropTypes.shape({
top: PropTypes.number,
right: PropTypes.number,
bottom: PropTypes.number,
left: PropTypes.number,
}),
};
const percentFormat = getNumberFormatter(NumberFormats.PERCENT_1_POINT);
const countFormat = getNumberFormatter();
function computeGraph(links) {
// this assumes source and target are string values
const nodes = Array.from(
links.reduce((set, { source, target }) => set.add(source).add(target), new Set()),
).map(id => ({ id, name: id }));
return {
nodes,
// links are shallow copied as the chart layout modifies them, and it is best to
// leave the passed data un-altered
links: links.map(d => ({ ...d })),
};
}
function SankeyLoop(element, props) {
const { data, width, height, colorScheme } = props;
const color = CategoricalColorNamespace.getScale(colorScheme);
const margin = { ...defaultMargin, ...props.margin };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const layout = sankey()
.nodeId(d => d.id)
.extent([[margin.left, margin.top], [innerWidth, innerHeight]]);
const diagram = sankeyDiagram()
.nodeTitle(d => d.name)
.linkTitle(
({ source: { name: sName, value: sValue }, target: { name: tName }, value }) =>
`${sName}${tName}: ${countFormat(value)} (${percentFormat(value / sValue)})`,
)
.linkColor(d => color(d.source.name));
const svg = select(element)
.append('svg')
.classed('superset-legacy-chart-sankey-loop', true)
.style('width', width)
.style('height', height)
.datum(layout(computeGraph(data)))
.call(diagram);
svg
.selectAll('g.link')
.classed('link', true)
.append('text')
.attr('x', d => d.points[0].x)
.attr('y', d => d.points[0].y)
.attr('dy', 3)
.attr('dx', 2)
.text(d => `${countFormat(d.value)} (${percentFormat(d.value / d.source.value)})`);
}
SankeyLoop.displayName = 'SankeyLoop';
SankeyLoop.propTypes = propTypes;
export default SankeyLoop;

View File

@ -0,0 +1,40 @@
/**
* 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 { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
const metadata = new ChartMetadata({
credits: ['https://github.com/ricklupton/d3-sankey-diagram'],
description: '',
name: t('Sankey Diagram with Loops'),
thumbnail,
useLegacyApi: true,
});
export default class SankeyChartPlugin extends ChartPlugin {
constructor() {
super({
loadChart: () => import('./ReactSankeyLoop.js'),
metadata,
transformProps,
});
}
}

View File

@ -0,0 +1,31 @@
/**
* 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.
*/
/* eslint-disable sort-keys */
export default function transformProps(chartProps) {
const { width, height, formData, payload, margin } = chartProps;
const { colorScheme } = formData;
return {
width,
height,
data: payload.data,
colorScheme,
margin,
};
}

View File

@ -0,0 +1,24 @@
/* eslint-disable no-magic-numbers */
import React from 'react';
import { SuperChart } from '@superset-ui/chart';
import data from './data';
export default [
{
renderStory: () => (
<SuperChart
chartType="sankey-loop"
chartProps={{
formData: {
colorScheme: 'd3Category10',
},
height: 400,
payload: { data },
width: 400,
}}
/>
),
storyName: 'Basic',
storyPath: 'legacy-|plugin-chart-sankey-loop|SankeyLoopChartPlugin',
},
];

View File

@ -0,0 +1,33 @@
/* eslint-disable sort-keys, no-magic-numbers */
export default [
{
source: 'Lisdoonvarna',
target: 'Cliffs of Moher',
value: 50,
},
{
source: 'Cliffs of Moher',
target: 'Lisdoonvarna',
value: 35,
},
{
source: 'Cliffs of Moher',
target: 'Killarney',
value: 25,
},
{
source: 'Lisdoonvarna',
target: 'Killarney',
value: 25,
},
{
source: 'Lisdoonvarna',
target: 'Kinvarra',
value: 25,
},
{
source: 'Kinvarra',
target: 'Lisdoonvarna',
value: 25,
},
];

View File

@ -0,0 +1,8 @@
import SankeyLoopChartPlugin from '../../../../superset-ui-legacy-plugin-chart-sankey-loop';
import Stories from './Stories';
new SankeyLoopChartPlugin().configure({ key: 'sankey-loop' }).register();
export default {
examples: [...Stories],
};