mirror of https://github.com/apache/superset.git
New chart type : Chord Diagrams (#3013)
This commit is contained in:
parent
a55f963e52
commit
7045018d86
Binary file not shown.
After Width: | Height: | Size: 398 KiB |
|
@ -334,11 +334,14 @@ export const controls = {
|
|||
type: 'SelectControl',
|
||||
multi: true,
|
||||
label: 'Columns',
|
||||
mapStateToProps: state => ({
|
||||
choices: (state.datasource) ? state.datasource.gb_cols : [],
|
||||
}),
|
||||
default: [],
|
||||
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: {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { D3_TIME_FORMAT_OPTIONS } from './controls';
|
||||
|
||||
import * as v from '../validators';
|
||||
|
||||
export const sections = {
|
||||
druidTimeSeries: {
|
||||
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: {
|
||||
label: 'Country Map',
|
||||
controlPanelSections: [
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
"react-ace": "^5.0.1",
|
||||
"react-addons-css-transition-group": "^15.6.0",
|
||||
"react-addons-shallow-compare": "^15.4.2",
|
||||
"react-alert": "^2.0.1",
|
||||
"react-alert": "^1.0.14",
|
||||
"react-bootstrap": "^0.31.0",
|
||||
"react-bootstrap-table": "^3.1.7",
|
||||
"react-dom": "^15.5.1",
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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'),
|
||||
compare: require('./nvd3_vis.js'),
|
||||
directed_force: require('./directed_force.js'),
|
||||
chord: require('./chord.jsx'),
|
||||
dist_bar: require('./nvd3_vis.js'),
|
||||
filter_box: require('./filter_box.jsx'),
|
||||
heatmap: require('./heatmap.js'),
|
||||
|
|
|
@ -16,6 +16,7 @@ import uuid
|
|||
import zlib
|
||||
|
||||
from collections import OrderedDict, defaultdict
|
||||
from itertools import product
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pandas as pd
|
||||
|
@ -1231,6 +1232,39 @@ class DirectedForceViz(BaseViz):
|
|||
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):
|
||||
|
||||
"""A country centric"""
|
||||
|
@ -1574,6 +1608,7 @@ viz_types_list = [
|
|||
DirectedForceViz,
|
||||
SankeyViz,
|
||||
CountryMapViz,
|
||||
ChordViz,
|
||||
WorldMapViz,
|
||||
FilterBoxViz,
|
||||
IFrameViz,
|
||||
|
|
Loading…
Reference in New Issue