From 602c13e1daacc7eff3e12086eb70a1d9df641c91 Mon Sep 17 00:00:00 2001 From: David Dolphin Date: Sun, 15 Jan 2017 09:57:12 -0800 Subject: [PATCH] Add Color to Force Directed Graph (#1992) --- .../javascripts/explorev2/stores/fields.js | 17 +++++++++++++++++ .../javascripts/explorev2/stores/visTypes.js | 2 ++ .../assets/visualizations/directed_force.js | 15 ++++++++++++--- superset/data/__init__.py | 2 ++ superset/data/energy.json.gz | Bin 985 -> 1022 bytes superset/forms.py | 11 +++++++++++ superset/viz.py | 9 ++++++++- 7 files changed, 52 insertions(+), 4 deletions(-) diff --git a/superset/assets/javascripts/explorev2/stores/fields.js b/superset/assets/javascripts/explorev2/stores/fields.js index 7a8cf48ba31d..a6a5d5faab4a 100644 --- a/superset/assets/javascripts/explorev2/stores/fields.js +++ b/superset/assets/javascripts/explorev2/stores/fields.js @@ -376,6 +376,23 @@ export const fields = { 'domain_granularity. Should be larger or equal to Time Grain', }, + graph_color: { + type: 'SelectField', + label: 'Node Color', + default: [], + description: 'Choose a source node dimension to color the nodes.', + mapStateToProps: (state) => ({ + choices: (state.datasource) ? state.datasource.all_cols : [], + }), + }, + + graph_labels: { + type: 'CheckboxField', + label: 'Node Lables', + default: true, + description: 'Display lables on all node?', + }, + link_length: { type: 'SelectField', freeForm: true, diff --git a/superset/assets/javascripts/explorev2/stores/visTypes.js b/superset/assets/javascripts/explorev2/stores/visTypes.js index 1aea323b0584..8ba987f58685 100644 --- a/superset/assets/javascripts/explorev2/stores/visTypes.js +++ b/superset/assets/javascripts/explorev2/stores/visTypes.js @@ -544,6 +544,8 @@ const visTypes = { { label: 'Force Layout', fieldSetRows: [ + ['graph_color'], + ['graph_labels'], ['link_length'], ['charge'], ], diff --git a/superset/assets/visualizations/directed_force.js b/superset/assets/visualizations/directed_force.js index e2003f122eb1..626d9371771a 100644 --- a/superset/assets/visualizations/directed_force.js +++ b/superset/assets/visualizations/directed_force.js @@ -10,6 +10,7 @@ const directedForceVis = function (slice, json) { const height = slice.height() - 25; const linkLength = json.form_data.link_length || 200; const charge = json.form_data.charge || -500; + const labels = json.form_data.graph_labels | 0; const links = json.data; const nodes = {}; @@ -46,6 +47,7 @@ const directedForceVis = function (slice, json) { } nodes[targetName].total += link.value; + nodes[targetName].color = link.color; }); /* eslint-disable no-use-before-define */ @@ -124,11 +126,11 @@ const directedForceVis = function (slice, json) { .select('circle') .transition() .style('stroke-width', 5); - d3.select(this) .select('text') .transition() - .style('font-size', 25); + .style('font-size', 25) + .style('opacity', 1); }) .on('mouseleave', function () { d3.select(this) @@ -138,7 +140,8 @@ const directedForceVis = function (slice, json) { d3.select(this) .select('text') .transition() - .style('font-size', 12); + .style('font-size', 12) + .style('opacity', labels); }) .call(force.drag); @@ -150,15 +153,21 @@ const directedForceVis = function (slice, json) { .domain(ext) .range([3, 30]); + const color = d3.scale.category20(); + node.append('circle') .attr('r', function (d) { return circleScale(Math.sqrt(d.total)); + }) + .style('fill', function (d) { + return color(d.color); }); // add the text node.append('text') .attr('x', 6) .attr('dy', '.35em') + .style('opacity', labels) .text(function (d) { return d.name; }); diff --git a/superset/data/__init__.py b/superset/data/__init__.py index e96ef36f35af..fddbeab8c29c 100644 --- a/superset/data/__init__.py +++ b/superset/data/__init__.py @@ -55,6 +55,7 @@ def load_energy(): if_exists='replace', chunksize=500, dtype={ + 'cluster': String(255), 'source': String(255), 'target': String(255), 'value': Float(), @@ -123,6 +124,7 @@ def load_energy(): ], "having": "", "link_length": "200", + "graph_color": "cluster", "metric": "sum__value", "row_limit": "5000", "slice_name": "Force", diff --git a/superset/data/energy.json.gz b/superset/data/energy.json.gz index 624d71db683a9c08d19ad7e3625d502ca6d2e7d8..6c51dc3cac07cbeac03dcc690034ff1e4c74ad3a 100644 GIT binary patch literal 1022 zcmVZOO+OL*$O?X>=ljWjA--92+JqI(K!`==rnusf?1mVZ?`>Y{W}h4DxnvK(Y>Hd z6$pmy-u?vF^o1xbl3xh(DZ0UY{d~(2VG*}_P9!Qn+(zmB{seb4A!uaA=?m6W@Dl#8 z8U7~R8-kuFLUAp%XMQzY0Vhc$`LLLAVA@e+ZxcXigS1fySk8mp&NS6$Gmu!Kf|~h6Gm_ zGrKYS$Y-EHblTlLp^ZZ3n3qKtFx|z1$b=f?&nab7gALoj({>-qaZa9vG(Py|9)MHR zX@3YXmJ3D_ZJtoQ%8hnTa7o_oM6=xmupGT)^~d-CjGObk#Rl(<7rSsww(^PxZfz)64A^xd@NVe=sA0EuL)tz@_>)y1}AfBM7%l-{4Hhq_!q3uM2dtc zwKkjsD0f}E6#ny#qX$pXK8{JsmRY92OGA~tFp-q&-6fsIqPru?c$pz^N*e6VKB|X& z^c-CarHCp;FB-=q$Bc;tYcfUSlrUsY?2|z$*MYqN`Q`Roq;O1OgFUe40vPx>ol#S` z5_GYkM#zE}<22Z3+7{Jz2LZCJ0Im;%JIH4NBCUWwNX*qzNL?x62XHsHcxcoL*t%JT zTv0548YH87c(3pEgKmVESY~%W^y6K2@53{A%^k3!*RjlNUZ+e-Yl>fs_!uj-NUwbs z4cl6aM4P%Z83oh_g|WkL!m2j#UAS#p@uCE~?8Q+^FdMS7?CQhK$A;_mgm86REZ-uz z{(iWyng<^8-SGNNEFiUBXUOI{hDPQZJ1e!Gc5jZp`(Yrw%cC1OamMmYS-G8HyP1bH zFleR00SP*|^-;MJ zZ(S?@kksIFksH!4s0OmBp<8c9lc3kT4J5mQoexM=6=r~C06E;r5X|%qn}ThW9*$CTmD@rL?;?kiras z443Jm?Lych(bTarl1!WF^xZ2vkl69BOPsP{#Q&qVh?kb?&*&5~s@6*`3FKs&%bGt(ZhX{-;^{fL7D_Qy*?tq; zV@i`mL{zhwj5C%Hlk`uYN%Nrd8 z8Us?KJHRp^APP$3nGVjC0 zyreRpZ^`#}`D{yP;WH05@-^CdvAtTsDrO$kx_t0j>*}@7nqXk-YGRO8Byo=^efVD; zwsJbD3)KKF-PbWyHQBf2B8auk_F?rQgtt(*DTiT>l|X{ zQZh1=_^fi!+#QoW!?1&76B@Ui>C<4B^Se$FZNL(3Z-RUDKvw zP2rGZCTf{}VBJ}`Owb*NGjd2ioRIz8Z5tu>IHXx(-~)TkEjZ!5RR1{J$k?y}wCe~t zY#8=KIm|*U$adp_lE5V;3c+!oKE3kfvQfxAIx z{XwGyjYDlf>uaZ@5bvPP)CTu$*%sH3?gn6A?T@EBY7X++bsh(~%PmC3QI!W$>sf#c zDG+DZV+8$5m9Ac}ms-3#oVBIJ1MTo#3vOdtXfk!JJ*|=*Q7NkbPz< HD;WR)$o=Zd diff --git a/superset/forms.py b/superset/forms.py index 150c0b7097bb..d8f42d36f954 100755 --- a/superset/forms.py +++ b/superset/forms.py @@ -321,6 +321,17 @@ def __init__(self, viz): "choices": self.choicify(datasource.groupby_column_names), "description": _("One or many fields to pivot as columns") }), + 'graph_color': (SelectField, { + "label": _("Node Color"), + "default": None, + "choices": self.choicify([None] + datasource.column_names), + "description": _("Choose a source node dimension to color the nodes"), + }), + 'graph_labels': (BetterBooleanField, { + "label": _("Node Labels"), + "default": True, + "description": _("Display labels on all node?") + }), 'all_columns': (SelectMultipleSortableField, { "label": _("Columns"), "choices": self.choicify(datasource.column_names), diff --git a/superset/viz.py b/superset/viz.py index eaf5a6374088..235869a18891 100755 --- a/superset/viz.py +++ b/superset/viz.py @@ -1759,6 +1759,8 @@ class DirectedForceViz(BaseViz): }, { 'label': _('Force Layout'), 'fields': ( + 'graph_color', + 'graph_labels', 'link_length', 'charge', ) @@ -1775,11 +1777,16 @@ def query_obj(self): if len(self.form_data['groupby']) != 2: raise Exception("Pick exactly 2 columns to 'Group By'") qry['metrics'] = [self.form_data['metric']] + if self.form_data['graph_color'] != 'None': + qry['groupby'] += [self.form_data['graph_color']] return qry def get_data(self): df = self.get_df() - df.columns = ['source', 'target', 'value'] + if self.form_data['graph_color'] == 'None': + df.columns = ['source', 'target', 'value'] + else: + df.columns = ['source', 'target', 'color', 'value'] return df.to_dict(orient='records')