mirror of
https://github.com/apache/superset.git
synced 2024-09-17 11:09:47 -04:00
New chart type : Chord Diagrams (#3013)
This commit is contained in:
parent
a55f963e52
commit
7045018d86
BIN
superset/assets/images/viz_thumbnails/chord.png
Normal file
BIN
superset/assets/images/viz_thumbnails/chord.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 398 KiB |
@ -334,11 +334,14 @@ export const controls = {
|
|||||||
type: 'SelectControl',
|
type: 'SelectControl',
|
||||||
multi: true,
|
multi: true,
|
||||||
label: 'Columns',
|
label: 'Columns',
|
||||||
mapStateToProps: state => ({
|
|
||||||
choices: (state.datasource) ? state.datasource.gb_cols : [],
|
|
||||||
}),
|
|
||||||
default: [],
|
default: [],
|
||||||
description: 'One or many controls to pivot as columns',
|
description: 'One or many controls to pivot as columns',
|
||||||
|
optionRenderer: c => <ColumnOption column={c} />,
|
||||||
|
valueRenderer: c => <ColumnOption column={c} />,
|
||||||
|
valueKey: 'column_name',
|
||||||
|
mapStateToProps: state => ({
|
||||||
|
options: (state.datasource) ? state.datasource.columns : [],
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
all_columns: {
|
all_columns: {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { D3_TIME_FORMAT_OPTIONS } from './controls';
|
import { D3_TIME_FORMAT_OPTIONS } from './controls';
|
||||||
|
|
||||||
|
import * as v from '../validators';
|
||||||
|
|
||||||
export const sections = {
|
export const sections = {
|
||||||
druidTimeSeries: {
|
druidTimeSeries: {
|
||||||
label: 'Time',
|
label: 'Time',
|
||||||
@ -635,6 +637,37 @@ const visTypes = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
chord: {
|
||||||
|
label: 'Chord Diagram',
|
||||||
|
controlPanelSections: [
|
||||||
|
{
|
||||||
|
label: null,
|
||||||
|
controlSetRows: [
|
||||||
|
['groupby', 'columns'],
|
||||||
|
['metric'],
|
||||||
|
['row_limit', 'y_axis_format'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
controlOverrides: {
|
||||||
|
y_axis_format: {
|
||||||
|
label: 'Number format',
|
||||||
|
description: 'Choose a number format',
|
||||||
|
},
|
||||||
|
groupby: {
|
||||||
|
label: 'Source',
|
||||||
|
multi: false,
|
||||||
|
validators: [v.nonEmpty],
|
||||||
|
description: 'Choose a source',
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
label: 'Target',
|
||||||
|
multi: false,
|
||||||
|
validators: [v.nonEmpty],
|
||||||
|
description: 'Choose a target',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
country_map: {
|
country_map: {
|
||||||
label: 'Country Map',
|
label: 'Country Map',
|
||||||
controlPanelSections: [
|
controlPanelSections: [
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
"react-ace": "^5.0.1",
|
"react-ace": "^5.0.1",
|
||||||
"react-addons-css-transition-group": "^15.6.0",
|
"react-addons-css-transition-group": "^15.6.0",
|
||||||
"react-addons-shallow-compare": "^15.4.2",
|
"react-addons-shallow-compare": "^15.4.2",
|
||||||
"react-alert": "^2.0.1",
|
"react-alert": "^1.0.14",
|
||||||
"react-bootstrap": "^0.31.0",
|
"react-bootstrap": "^0.31.0",
|
||||||
"react-bootstrap-table": "^3.1.7",
|
"react-bootstrap-table": "^3.1.7",
|
||||||
"react-dom": "^15.5.1",
|
"react-dom": "^15.5.1",
|
||||||
|
17
superset/assets/visualizations/chord.css
Normal file
17
superset/assets/visualizations/chord.css
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
.chord svg #circle circle {
|
||||||
|
fill: none;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chord svg .group path {
|
||||||
|
fill-opacity: .6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chord svg path.chord {
|
||||||
|
stroke: #000;
|
||||||
|
stroke-width: .25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chord svg #circle:hover path.fade {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
101
superset/assets/visualizations/chord.jsx
Normal file
101
superset/assets/visualizations/chord.jsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
import d3 from 'd3';
|
||||||
|
import { category21 } from '../javascripts/modules/colors';
|
||||||
|
import './chord.css';
|
||||||
|
|
||||||
|
function chordViz(slice, json) {
|
||||||
|
slice.container.html('');
|
||||||
|
|
||||||
|
const div = d3.select(slice.selector);
|
||||||
|
const nodes = json.data.nodes;
|
||||||
|
const fd = slice.formData;
|
||||||
|
const f = d3.format(fd.y_axis_format);
|
||||||
|
|
||||||
|
const width = slice.width();
|
||||||
|
const height = slice.height();
|
||||||
|
|
||||||
|
const outerRadius = Math.min(width, height) / 2 - 10;
|
||||||
|
const innerRadius = outerRadius - 24;
|
||||||
|
|
||||||
|
let chord;
|
||||||
|
|
||||||
|
const arc = d3.svg.arc()
|
||||||
|
.innerRadius(innerRadius)
|
||||||
|
.outerRadius(outerRadius);
|
||||||
|
|
||||||
|
const layout = d3.layout.chord()
|
||||||
|
.padding(0.04)
|
||||||
|
.sortSubgroups(d3.descending)
|
||||||
|
.sortChords(d3.descending);
|
||||||
|
|
||||||
|
const path = d3.svg.chord()
|
||||||
|
.radius(innerRadius);
|
||||||
|
|
||||||
|
const svg = div.append('svg')
|
||||||
|
.attr('width', width)
|
||||||
|
.attr('height', height)
|
||||||
|
.on('mouseout', () => chord.classed('fade', false))
|
||||||
|
.append('g')
|
||||||
|
.attr('id', 'circle')
|
||||||
|
.attr('transform', `translate(${width / 2}, ${height / 2})`);
|
||||||
|
|
||||||
|
svg.append('circle')
|
||||||
|
.attr('r', outerRadius);
|
||||||
|
|
||||||
|
// Compute the chord layout.
|
||||||
|
layout.matrix(json.data.matrix);
|
||||||
|
|
||||||
|
const group = svg.selectAll('.group')
|
||||||
|
.data(layout.groups)
|
||||||
|
.enter().append('g')
|
||||||
|
.attr('class', 'group')
|
||||||
|
.on('mouseover', (d, i) => {
|
||||||
|
chord.classed('fade', p => p.source.index !== i && p.target.index !== i);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a mouseover title.
|
||||||
|
group.append('title').text((d, i) => `${nodes[i]}: ${f(d.value)}`);
|
||||||
|
|
||||||
|
// Add the group arc.
|
||||||
|
const groupPath = group.append('path')
|
||||||
|
.attr('id', (d, i) => 'group' + i)
|
||||||
|
.attr('d', arc)
|
||||||
|
.style('fill', (d, i) => category21(nodes[i]));
|
||||||
|
|
||||||
|
// Add a text label.
|
||||||
|
const groupText = group.append('text')
|
||||||
|
.attr('x', 6)
|
||||||
|
.attr('dy', 15);
|
||||||
|
|
||||||
|
groupText.append('textPath')
|
||||||
|
.attr('xlink:href', (d, i) => `#group${i}`)
|
||||||
|
.text((d, i) => nodes[i]);
|
||||||
|
// Remove the labels that don't fit. :(
|
||||||
|
groupText.filter(function (d, i) {
|
||||||
|
return groupPath[0][i].getTotalLength() / 2 - 16 < this.getComputedTextLength();
|
||||||
|
})
|
||||||
|
.remove();
|
||||||
|
|
||||||
|
// Add the chords.
|
||||||
|
chord = svg.selectAll('.chord')
|
||||||
|
.data(layout.chords)
|
||||||
|
.enter().append('path')
|
||||||
|
.attr('class', 'chord')
|
||||||
|
.on('mouseover', (d) => {
|
||||||
|
chord.classed('fade', p => p !== d);
|
||||||
|
})
|
||||||
|
.style('fill', d => category21(nodes[d.source.index]))
|
||||||
|
.attr('d', path);
|
||||||
|
|
||||||
|
// Add an elaborate mouseover title for each chord.
|
||||||
|
chord.append('title').text(function (d) {
|
||||||
|
return nodes[d.source.index]
|
||||||
|
+ ' → ' + nodes[d.target.index]
|
||||||
|
+ ': ' + f(d.source.value)
|
||||||
|
+ '\n' + nodes[d.target.index]
|
||||||
|
+ ' → ' + nodes[d.source.index]
|
||||||
|
+ ': ' + f(d.target.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = chordViz;
|
@ -10,6 +10,7 @@ const vizMap = {
|
|||||||
cal_heatmap: require('./cal_heatmap.js'),
|
cal_heatmap: require('./cal_heatmap.js'),
|
||||||
compare: require('./nvd3_vis.js'),
|
compare: require('./nvd3_vis.js'),
|
||||||
directed_force: require('./directed_force.js'),
|
directed_force: require('./directed_force.js'),
|
||||||
|
chord: require('./chord.jsx'),
|
||||||
dist_bar: require('./nvd3_vis.js'),
|
dist_bar: require('./nvd3_vis.js'),
|
||||||
filter_box: require('./filter_box.jsx'),
|
filter_box: require('./filter_box.jsx'),
|
||||||
heatmap: require('./heatmap.js'),
|
heatmap: require('./heatmap.js'),
|
||||||
|
@ -16,6 +16,7 @@ import uuid
|
|||||||
import zlib
|
import zlib
|
||||||
|
|
||||||
from collections import OrderedDict, defaultdict
|
from collections import OrderedDict, defaultdict
|
||||||
|
from itertools import product
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@ -1231,6 +1232,39 @@ class DirectedForceViz(BaseViz):
|
|||||||
return df.to_dict(orient='records')
|
return df.to_dict(orient='records')
|
||||||
|
|
||||||
|
|
||||||
|
class ChordViz(BaseViz):
|
||||||
|
|
||||||
|
"""A Chord diagram"""
|
||||||
|
|
||||||
|
viz_type = "chord"
|
||||||
|
verbose_name = _("Directed Force Layout")
|
||||||
|
credits = '<a href="https://github.com/d3/d3-chord">Bostock</a>'
|
||||||
|
is_timeseries = False
|
||||||
|
|
||||||
|
def query_obj(self):
|
||||||
|
qry = super(ChordViz, self).query_obj()
|
||||||
|
fd = self.form_data
|
||||||
|
qry['groupby'] = [fd.get('groupby'), fd.get('columns')]
|
||||||
|
qry['metrics'] = [fd.get('metric')]
|
||||||
|
return qry
|
||||||
|
|
||||||
|
def get_data(self, df):
|
||||||
|
df.columns = ['source', 'target', 'value']
|
||||||
|
|
||||||
|
# Preparing a symetrical matrix like d3.chords calls for
|
||||||
|
nodes = list(set(df['source']) | set(df['target']))
|
||||||
|
matrix = {}
|
||||||
|
for source, target in product(nodes, nodes):
|
||||||
|
matrix[(source, target)] = 0
|
||||||
|
for source, target, value in df.to_records(index=False):
|
||||||
|
matrix[(source, target)] = value
|
||||||
|
m = [[matrix[(n1, n2)] for n1 in nodes] for n2 in nodes]
|
||||||
|
return {
|
||||||
|
'nodes': list(nodes),
|
||||||
|
'matrix': m,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CountryMapViz(BaseViz):
|
class CountryMapViz(BaseViz):
|
||||||
|
|
||||||
"""A country centric"""
|
"""A country centric"""
|
||||||
@ -1574,6 +1608,7 @@ viz_types_list = [
|
|||||||
DirectedForceViz,
|
DirectedForceViz,
|
||||||
SankeyViz,
|
SankeyViz,
|
||||||
CountryMapViz,
|
CountryMapViz,
|
||||||
|
ChordViz,
|
||||||
WorldMapViz,
|
WorldMapViz,
|
||||||
FilterBoxViz,
|
FilterBoxViz,
|
||||||
IFrameViz,
|
IFrameViz,
|
||||||
|
Loading…
Reference in New Issue
Block a user