Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New chart type : Chord Diagrams #3013

Merged
merged 1 commit into from
Jun 26, 2017
Merged
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
Binary file added superset/assets/images/viz_thumbnails/chord.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 6 additions & 3 deletions superset/assets/javascripts/explore/stores/controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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} />,
Copy link
Member Author

Choose a reason for hiding this comment

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

This file (controls.jsx) holds configuration for a "pool" of controls that can be used across visualizations. When creating a new visualization, you'll want to reuse controls that already exist. If the exact control that you need doesn't exist already, you'll want to create it here.

Note that type references controls React components defined in javascripts/explore/component/controls/.

Also note that controls can be dynamic with mapStateToProps, allowing to map anything from the app's state into a prop for the component. This allows in this case to get configuration elements proper to the datasource in this case, but can also be used to change a control based on another control's value.

valueRenderer: c => <ColumnOption column={c} />,
valueKey: 'column_name',
mapStateToProps: state => ({
options: (state.datasource) ? state.datasource.columns : [],
}),
},

all_columns: {
Expand Down
33 changes: 33 additions & 0 deletions superset/assets/javascripts/explore/stores/visTypes.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { D3_TIME_FORMAT_OPTIONS } from './controls';

import * as v from '../validators';

export const sections = {
druidTimeSeries: {
label: 'Time',
Expand Down Expand Up @@ -622,6 +624,37 @@ const visTypes = {
},
},
},
chord: {
Copy link
Member Author

Choose a reason for hiding this comment

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

This file contains configuration of the new chord viz type. It defines the panels of controls that should render on the left panel of the explore view.

controlSetRows references controls defined in controls.jsx, and can have one or two items and that defines how many items will render on that row.

Choose a reason for hiding this comment

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

This file contains configuration of the new chord viz type. It defines the panels of controls that should render on the left panel of the explore view.
controlSetRows references controls defined in controls.jsx, and can have one or two items and that defines how many items will render on that row.

Could I please ask some questiones? I want to show a metric and some filter in the superset dashboard, after reading some articles, i am confused because the configuration needed to be changed does not exist in the files. For example,
superset/assets/javascripts/explore/stores/visTypes.js, this file does not exist in the current code file. So, how can i add a metric in the superset now and which files should i modify in order to achieve this?

label: 'Chord Diagram',
controlPanelSections: [
{
label: null,
controlSetRows: [
['groupby', 'columns'],
['metric'],
['row_limit', 'y_axis_format'],
],
},
],
controlOverrides: {
Copy link
Member Author

Choose a reason for hiding this comment

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

controlOverrides allows you to override the control configuration coming from controls.jsx. By reusing the same control across visualizations we allow continuity in the sense that the values are carried over when going from a viz type to the next. For instance as you go from one viz type to the next, it's nice to have metrics carried over, though you might want a slightly different label or tooltip in the different contexts, this is what the overrides are for.

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: [
Expand Down
2 changes: 1 addition & 1 deletion superset/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions superset/assets/visualizations/chord.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.chord svg #circle circle {
Copy link
Member Author

Choose a reason for hiding this comment

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

by convention each visualization has its own css file, and the entries need to be "namespaced" properly to make sure that one viz's css doesn't spill and apply to other viz types

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 changes: 101 additions & 0 deletions superset/assets/visualizations/chord.jsx
Original file line number Diff line number Diff line change
@@ -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) {
Copy link
Member Author

Choose a reason for hiding this comment

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

This function is the heart of this new visualization, the framework will pass it a slice and json object, and expects you to mutate the dom element referenced in the slice object. It will be called whenever the slice needs to be rendered or re-rendered. Knowing that this function may be called many times, you can use closures here if you want to persists certain things in the module namespace.

The json objects corresponds to the data returned from running your query and related context. To see exactly what you should expect in here you can click the json button in the explore view.

The slice object contains, amongst other things, a reference to the dom element that you should mutate.

slice.container.html('');

const div = d3.select(slice.selector);
const nodes = json.data.nodes;
Copy link
Member Author

Choose a reason for hiding this comment

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

json.data corresponds exactly to what is returned in the backend ChordViz.get_data lower in this PR.

const fd = slice.formData;
Copy link
Member Author

Choose a reason for hiding this comment

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

On of the things that the slice object exposes isf the information provided by the user controls. The keys of this slice.formData object are the control's "id" defined in controls.jsx and their values are the ones provided by the user.

const f = d3.format(fd.y_axis_format);

const width = slice.width();
Copy link
Member Author

Choose a reason for hiding this comment

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

The slice object exposes width() and height() methods that can be useful while rendering your chart.

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;
Copy link
Member Author

Choose a reason for hiding this comment

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

that function should be exported as the default export for the visualization module

1 change: 1 addition & 0 deletions superset/assets/visualizations/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Copy link
Member Author

Choose a reason for hiding this comment

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

simply adding the mapping from the chord visualization type to the appropriate javascript modules. It's possible here to have multiple visualization types target the same module (look at nvd3.js!)

dist_bar: require('./nvd3_vis.js'),
filter_box: require('./filter_box.jsx'),
heatmap: require('./heatmap.js'),
Expand Down
35 changes: 35 additions & 0 deletions superset/viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import zlib

from collections import OrderedDict, defaultdict
from itertools import product
from datetime import datetime, timedelta

import pandas as pd
Expand Down Expand Up @@ -1231,6 +1232,39 @@ def get_data(self, df):
return df.to_dict(orient='records')


class ChordViz(BaseViz):
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the backend part of this component, you can derive BaseViz or other BaseViz derivatives


"""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):
Copy link
Member Author

Choose a reason for hiding this comment

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

You need to override query_obj and it has to return a query object, which is essentially a Python dict with key/values that are expected by the query method of the connectors. Until this is better documented, you can look at the connector's Datasource query method to get a sense for it.

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):
Copy link
Member Author

Choose a reason for hiding this comment

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

get_data receives a pandas dataframe (df) that correspond to what is returned when applying your query object defined above. The shape of that dataframe is related to what you ask for in that query_obj.

Now whatever get_data returns needs to be json-serializable, and will be made available to the javascript visualization function as payload.data.

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"""
Expand Down Expand Up @@ -1574,6 +1608,7 @@ def get_data(self, df):
DirectedForceViz,
SankeyViz,
CountryMapViz,
ChordViz,
WorldMapViz,
FilterBoxViz,
IFrameViz,
Expand Down