From 5b6baa707e6c69c696e3b611b6464c94f61b9ad2 Mon Sep 17 00:00:00 2001 From: Spencer Childress Date: Tue, 18 Jul 2017 12:33:28 -0400 Subject: [PATCH] Clean up codebase. --- .gitignore | 51 ++ LICENSE | 21 - LICENSE.md | 8 + README.md | 2 +- build/interactiveSankey.js | 806 ++++++++++++----------- package.json | 24 +- rollup.config.js | 34 + scripts/rollup.chf.config.js | 16 - scripts/rollup.wrapper.config.js | 9 - src/callbacks/index.js | 17 + src/{ => callbacks}/onDataTransform.js | 2 +- src/callbacks/onDestroy.js | 3 + src/{ => callbacks}/onDraw.js | 2 +- src/callbacks/onInit.js | 11 + src/{ => callbacks}/onLayout.js | 2 +- src/callbacks/onPreprocess.js | 3 + src/callbacks/onResize.js | 216 ++++++ src/callbacks/onResize/drawLinks.js | 150 +++++ src/chartfoundry/binding.js | 244 ------- src/chartfoundry/util/Renderer.js | 86 --- src/chartfoundry/util/reactTemplate.js | 35 - src/chartfoundry/util/string-accessor.js | 17 - src/default-settings.js | 40 -- src/defaultSettings.js | 37 ++ src/onInit.js | 8 - src/onResize.js | 180 ----- src/util/addBoxPlot.js | 107 --- src/util/addViolinPlot.js | 72 -- src/util/adjust-ticks.js | 11 - src/util/clone.js | 33 + src/util/drawLinks.js | 147 ----- src/util/lengthen-raw.js | 15 - src/util/object-assign.js | 40 +- src/wrapper.js | 35 +- 34 files changed, 1035 insertions(+), 1449 deletions(-) create mode 100644 .gitignore delete mode 100644 LICENSE create mode 100644 LICENSE.md create mode 100644 rollup.config.js delete mode 100644 scripts/rollup.chf.config.js delete mode 100644 scripts/rollup.wrapper.config.js create mode 100644 src/callbacks/index.js rename src/{ => callbacks}/onDataTransform.js (65%) create mode 100644 src/callbacks/onDestroy.js rename src/{ => callbacks}/onDraw.js (60%) create mode 100644 src/callbacks/onInit.js rename src/{ => callbacks}/onLayout.js (61%) create mode 100644 src/callbacks/onPreprocess.js create mode 100644 src/callbacks/onResize.js create mode 100644 src/callbacks/onResize/drawLinks.js delete mode 100644 src/chartfoundry/binding.js delete mode 100644 src/chartfoundry/util/Renderer.js delete mode 100644 src/chartfoundry/util/reactTemplate.js delete mode 100644 src/chartfoundry/util/string-accessor.js delete mode 100644 src/default-settings.js create mode 100644 src/defaultSettings.js delete mode 100644 src/onInit.js delete mode 100644 src/onResize.js delete mode 100644 src/util/addBoxPlot.js delete mode 100644 src/util/addViolinPlot.js delete mode 100644 src/util/adjust-ticks.js create mode 100644 src/util/clone.js delete mode 100644 src/util/drawLinks.js delete mode 100644 src/util/lengthen-raw.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a3a7400 --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# ========================= +# Operating System Files +# ========================= + +# OSX +# ========================= + +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +#node packages +node_modules +npm-debug.log diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 3b3a176..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2016 Rho Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..f53f4c6 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,8 @@ +MIT License +Copyright (c) 2016 Rho Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index f726b3a..8e31ea8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ # Interactive Sankey Chart -The interactive-sankey library produces charts which visualize change in state. Check out an example [here](http://bl.ocks.org/samussiah/raw/1f590427123d7c421f57647ef2bc3487/). +The interactive-sankey library produces bar charts which visualize change in state. Check out an example [here](http://bl.ocks.org/samussiah/raw/1f590427123d7c421f57647ef2bc3487/). See the [wiki](https://github.com/RhoInc/interactive-sankey/wiki) for more details. diff --git a/build/interactiveSankey.js b/build/interactiveSankey.js index 58c208c..ed2e9a3 100644 --- a/build/interactiveSankey.js +++ b/build/interactiveSankey.js @@ -1,414 +1,456 @@ -'use strict'; - -var interactiveSankey = (function (webcharts) { - 'use strict'; - - var settings = - //Customizable template settings - { id_col: 'USUBJID', - node_col: null, - link_col: null, - - //Standard template settings - x: { type: 'ordinal' }, - y: { type: 'linear' }, - marks: [{ type: 'bar', - arrange: 'stacked', - summarizeY: 'count', - tooltip: '$y at $x' }], - legend: {} - }; - - function syncSettings(settings) { - settings.x.column = settings.node_col; - settings.x.label = settings.node_col; - settings.y.column = settings.id_col; - settings.y.label = '# of ' + settings.id_col + 's'; - settings.marks[0].per = [settings.node_col]; - settings.marks[0].split = settings.link_col; - settings.color_by = settings.link_col; - - return settings; - } - - function onInit() { - var _this = this; - - var context = this; - - //Sort raw data by node so that links are drawn between adjacent nodes. - this.raw_data = this.raw_data.sort(function (a, b) { - return a[_this.config.node_col] < b[_this.config.node_col] ? -1 : a[_this.config.node_col] > b[_this.config.node_col] ? 1 : 0; - }); - } - - function onLayout() { - var context = this; - } - - function onDataTransform() { - var context = this; - } - - function onDraw() { - var context = this; - } - - function drawLinks(chartObject, bars1, bars2, linkClass) { - - //Merge adjacent bar groups by [ config.y.column ]. - var linkData = []; - bars1.each(function (bar1, i1) { - bars2.each(function (bar2, i2) { - bar1.values.raw.forEach(function (id1) { - bar2.values.raw.forEach(function (id2) { - if (id1[chartObject.config.y.column] === id2[chartObject.config.y.column]) linkData.push({ id: id1[chartObject.config.y.column], - split1: id1[chartObject.config.color_by], - x1: id1[chartObject.config.x.column], - split2: id2[chartObject.config.color_by], - x2: id2[chartObject.config.x.column] }); +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('webcharts')) : + typeof define === 'function' && define.amd ? define(['webcharts'], factory) : + (global.interactiveSankey = factory(global.webCharts)); +}(this, (function (webcharts) { 'use strict'; + +if (typeof Object.assign != 'function') { + (function () { + Object.assign = function (target) { + 'use strict'; + + if (target === undefined || target === null) { + throw new TypeError('Cannot convert undefined or null to object'); + } + + var output = Object(target); + for (var index = 1; index < arguments.length; index++) { + var source = arguments[index]; + if (source !== undefined && source !== null) { + for (var nextKey in source) { + if (source.hasOwnProperty(nextKey)) { + output[nextKey] = source[nextKey]; + } + } + } + } + return output; + }; + })(); +} + +var defaultSettings = //Customizable template settings +{ + id_col: 'USUBJID', + node_col: null, + link_col: null, + + //Standard template settings + x: { type: 'ordinal' }, + y: { type: 'linear' }, + marks: [{ + type: 'bar', + arrange: 'stacked', + summarizeY: 'count', + tooltip: '$y at $x' + }], + legend: {} +}; + +function syncSettings(settings) { + settings.x.column = settings.node_col; + settings.x.label = settings.node_col; + settings.y.column = settings.id_col; + settings.y.label = '# of ' + settings.id_col + 's'; + settings.marks[0].per = [settings.node_col]; + settings.marks[0].split = settings.link_col; + settings.color_by = settings.link_col; + + return settings; +} + +function onDataTransform() { + var chart = this; +} + +function onDestroy() { + var chart = this; +} + +function onDraw() { + var chart = this; +} + +function onInit() { + var _this = this; + + var chart = this; + + //Sort raw data by node so that links are drawn between adjacent nodes. + this.raw_data = this.raw_data.sort(function (a, b) { + return a[_this.config.node_col] < b[_this.config.node_col] ? -1 : a[_this.config.node_col] > b[_this.config.node_col] ? 1 : 0; + }); +} + +function onLayout() { + var chart = this; +} + +function onPreprocess() { + var chart = this; +} + +function drawLinks(chartObject, bars1, bars2, linkClass) { + //Merge adjacent bar groups by [ config.y.column ]. + var linkData = []; + bars1.each(function (bar1, i1) { + bars2.each(function (bar2, i2) { + bar1.values.raw.forEach(function (id1) { + bar2.values.raw.forEach(function (id2) { + if (id1[chartObject.config.y.column] === id2[chartObject.config.y.column]) linkData.push({ + id: id1[chartObject.config.y.column], + split1: id1[chartObject.config.color_by], + x1: id1[chartObject.config.x.column], + split2: id2[chartObject.config.color_by], + x2: id2[chartObject.config.x.column] }); }); }); }); - - //Nest merged bar groups by their respective [ config.marks.color_by ] values. - var nestedLinkData = d3.nest().key(function (d) { - return d.split1; - }).key(function (d) { - return d.split2; - }).rollup(function (d) { - return { n: d.length, IDs: d.map(function (di) { - return di.id; - }) }; - }).entries(linkData); - nestedLinkData.sort(function (a, b) { - return a.key > b.key ? 1 : -1; + }); + + //Nest merged bar groups by their respective [ config.marks.color_by ] values. + var nestedLinkData = d3.nest().key(function (d) { + return d.split1; + }).key(function (d) { + return d.split2; + }).rollup(function (d) { + return { n: d.length, IDs: d.map(function (di) { + return di.id; + }) }; + }).entries(linkData); + nestedLinkData.sort(function (a, b) { + return a.key > b.key ? 1 : -1; + }); + + //Flatten nested data array to one item per left bar [ config.marks.color_by ] + //value per right bar [ config.marks.color_by ] value. + var collapsedLinkData = []; + var offsetList = []; + nestedLinkData.forEach(function (d, i) { + var cat1 = bars1.data()[0].values.x; + var bar1 = bars1.filter(function (dii) { + return dii.key === d.key; + }).data()[0].values; + var offset1 = 0; + d.values.sort(function (a, b) { + return a.key < b.key ? -1 : b.key < a.key ? 1 : 0; }); - //Flatten nested data array to one item per left bar [ config.marks.color_by ] - //value per right bar [ config.marks.color_by ] value. - var collapsedLinkData = []; - var offsetList = []; - nestedLinkData.forEach(function (d, i) { - var cat1 = bars1.data()[0].values.x; - var bar1 = bars1.filter(function (dii) { - return dii.key === d.key; + d.values.forEach(function (di) { + var cat2 = bars2.data()[0].values.x; + var bar2 = bars2.filter(function (dii) { + return dii.key === di.key; }).data()[0].values; - var offset1 = 0; - d.values.sort(function (a, b) { - return a.key < b.key ? -1 : b.key < a.key ? 1 : 0; - }); - - d.values.forEach(function (di) { - var cat2 = bars2.data()[0].values.x; - var bar2 = bars2.filter(function (dii) { - return dii.key === di.key; - }).data()[0].values; - offsetList.push({ key: di.key, offset: 0 }); - var offset2 = offsetList.filter(function (dii) { - return dii.key === di.key; - })[0].offset; + offsetList.push({ key: di.key, offset: 0 }); + var offset2 = offsetList.filter(function (dii) { + return dii.key === di.key; + })[0].offset; - collapsedLinkData.push({ split1: d.key, - split2: di.key, + collapsedLinkData.push({ + split1: d.key, + split2: di.key, - x1: chartObject.x(cat1), - x2: chartObject.x(cat2), + x1: chartObject.x(cat1), + x2: chartObject.x(cat2), - y1: bar1.y, - y2: bar2.y, + y1: bar1.y, + y2: bar2.y, - start1: bar1.start, - start2: bar2.start, + start1: bar1.start, + start2: bar2.start, - stop1: bar1.start - bar1.y, - stop2: bar2.start - bar2.y, + stop1: bar1.start - bar1.y, + stop2: bar2.start - bar2.y, - y01: bar1.start - bar1.y + offset1, - y11: bar1.start - bar1.y + di.values.n + offset1, + y01: bar1.start - bar1.y + offset1, + y11: bar1.start - bar1.y + di.values.n + offset1, - y02: bar2.start - bar2.y + offset2, - y12: bar2.start - bar2.y + di.values.n + offset2, + y02: bar2.start - bar2.y + offset2, + y12: bar2.start - bar2.y + di.values.n + offset2, - n: di.values.n, - height: chartObject.y(di.values.n), + n: di.values.n, + height: chartObject.y(di.values.n), - IDs: di.values.IDs }); - - offset1 += collapsedLinkData[collapsedLinkData.length - 1].n; - offsetList.filter(function (dii) { - return dii.key === di.key; - })[0].offset += di.values.n; + IDs: di.values.IDs }); - }); - //Draw links. - var pathDrawer = d3.svg.area().x(function (d) { - return d.x; - }).y0(function (d) { - return d.y0; - }).y1(function (d) { - return d.y1; + offset1 += collapsedLinkData[collapsedLinkData.length - 1].n; + offsetList.filter(function (dii) { + return dii.key === di.key; + })[0].offset += di.values.n; + }); + }); + + //Draw links. + var pathDrawer = d3.svg.area().x(function (d) { + return d.x; + }).y0(function (d) { + return d.y0; + }).y1(function (d) { + return d.y1; + }); + collapsedLinkData.forEach(function (d, i) { + var path = [{ + split1: d.split1, + split2: d.split2, + n: d.n, + IDs: d.IDs, + + x: d.x1 + chartObject.x.rangeBand(), + y0: chartObject.y(d.y01), + y1: chartObject.y(d.y11) + }, { + x: d.x2, + y0: chartObject.y(d.y02), + y1: chartObject.y(d.y12) + }]; + chartObject.svg.append('path').datum(path).attr({ + d: pathDrawer, + class: 'link ' + linkClass + }).style({ + fill: function fill() { + return chartObject.colorScale(d.split1); + }, + 'fill-opacity': 0.5, + stroke: function stroke() { + return chartObject.colorScale(d.split1); + }, + 'stroke-opacity': 0.5 }); - collapsedLinkData.forEach(function (d, i) { - var path = [{ split1: d.split1, - split2: d.split2, - n: d.n, - IDs: d.IDs, - - x: d.x1 + chartObject.x.rangeBand(), - y0: chartObject.y(d.y01), - y1: chartObject.y(d.y11) }, { x: d.x2, - y0: chartObject.y(d.y02), - y1: chartObject.y(d.y12) }]; - chartObject.svg.append('path').datum(path).attr({ 'd': pathDrawer, - 'class': 'link ' + linkClass }).style({ 'fill': function fill() { - return chartObject.colorScale(d.split1); + }); + + //Add event listeners and tooltips to links. + var links = d3.selectAll('path.link'); + links.on('mouseover', function () { + d3.select(this).style({ + 'fill-opacity': 1, + 'stroke-opacity': 1 + }); + }).on('mouseout', function () { + d3.select(this).style({ + 'fill-opacity': 0.5, + 'stroke-opacity': 0.5 + }); + }).append('title').text(function (d) { + var n = d[0].n; + var split1 = d[0].split1; + var split2 = d[0].split2; + var IDs = d[0].IDs; + return n + ' ' + chartObject.config.y.column + (n > 1 ? 's ' : ' ') + (split1 === split2 ? 'remained at ' + split1 : 'progressed from ' + split1 + ' to ' + split2) + ':\n - ' + IDs.slice(0, 3).join('\n - ') + (n > 3 ? '\n - and ' + (n - 3) + ' more' : ''); + }); +} + +function onResize() { + var chart = this; + + //Reset displays. + this.wrap.selectAll('path.link').remove(); + this.wrap.selectAll('.barAnnotation').remove(); + + //Update legend to represent categories represented in chart. + this.makeLegend(); + var currentLinks = d3.set(this.filtered_data.map(function (d) { + return d[chart.config.link_col]; + })).values(); + this.wrap.selectAll('.legend-item').filter(function () { + return currentLinks.indexOf(d3.select(this).select('.legend-label')[0][0].textContent) === -1; + }).remove(); + + /**-------------------------------------------------------------------------------------------\ + Default links + \-------------------------------------------------------------------------------------------**/ + + //Capture stacked bar groups. + var barGroups = this.svg.selectAll('.bar-group'); + var nBarGroups = barGroups[0].length - 1; + barGroups.each(function (barGroup, i) { + //Annotate bars and modify tooltips. + var yPosition = barGroup.total; + d3.select(this).selectAll('rect.wc-data-mark').each(function (d) { + var bar = d3.select(this); + bar.classed(d.key.replace(/[^a-z0-9]/gi, ''), true); + var IDs = d.values.raw.map(function (d) { + return d[chart.config.id_col]; + }); + var n = d.values.raw.length; + var N = chart.raw_data.filter(function (di) { + return di[chart.config.node_col] === d.values.x; + }).length; + var pct = n / N; + d3.select(bar.node().parentNode).append('text').datum([{ node: d.values.x, link: d.key, text: n + ' (' + d3.format('%')(pct) + ')' }]).attr({ + class: 'barAnnotation', + x: function x(di) { + return chart.x(d.values.x); }, - 'fill-opacity': .5, - 'stroke': function stroke() { - return chartObject.colorScale(d.split1); + y: function y(di) { + return chart.y(yPosition); }, - 'stroke-opacity': .5 }); + dx: '.25em', + dy: '.9em' + }).text(n + ' (' + d3.format('%')(pct) + ')'); + bar.select('title').text(n + ' ' + chart.config.id_col + 's at ' + d.key + ' (' + d3.format('%')(pct) + '):' + '\n - ' + IDs.slice(0, 3).join('\n - ') + (n > 3 ? '\n - and ' + (n - 3) + ' more' : '')); + yPosition -= n; }); - //Add event listeners and tooltips to links. - var links = d3.selectAll('path.link'); - links.on('mouseover', function () { - d3.select(this).style({ 'fill-opacity': 1, - 'stroke-opacity': 1 }); - }).on('mouseout', function () { - d3.select(this).style({ 'fill-opacity': .5, - 'stroke-opacity': .5 }); - }).append('title').text(function (d) { - var n = d[0].n; - var split1 = d[0].split1; - var split2 = d[0].split2; - var IDs = d[0].IDs; - return n + ' ' + chartObject.config.y.column + (n > 1 ? 's ' : ' ') + (split1 === split2 ? 'remained at ' + split1 : 'progressed from ' + split1 + ' to ' + split2) + ':\n - ' + IDs.slice(0, 3).join('\n - ') + (n > 3 ? '\n - and ' + (n - 3) + ' more' : ''); + //Draw links from any bar group except the last bar group. + if (i < nBarGroups) { + var barGroup1 = d3.select(this); + var barGroup2 = d3.select(barGroups[0][i + 1]); + drawLinks(chart, barGroup1.selectAll('.bar'), barGroup2.selectAll('.bar'), 'defaultLink'); + } + }); + + /**-------------------------------------------------------------------------------------------\ + Selected links + \-------------------------------------------------------------------------------------------**/ + + //Flatten [ this.current_data ] to one item per node per link. + var barData = []; + this.current_data.forEach(function (d) { + d.values.forEach(function (di) { + barData.push({ + node: d.key, + link: di.key, + start: di.values.start + }); + }); + }); + + //Add click event listener to bars. + var bars = this.wrap.selectAll('rect.wc-data-mark'); + bars.style('cursor', 'pointer').on('click', function (d) { + //Reduce bar opacity. + var thisBar = this; + bars.style('fill-opacity', 0.25); + chart.wrap.selectAll('.defaultLink').style('display', 'none'); + chart.wrap.selectAll('.barAnnotation').style('display', 'none'); + chart.wrap.selectAll('.selectedIDs').remove(); + chart.wrap.selectAll('.selectedLink').remove(); + //Capture [ settings.id_col ] values represented by selected bar. + var selectedIDs = d.values.raw.map(function (d) { + return d[chart.config.id_col]; }); - } - - function onResize() { - var context = this; - - //Reset displays. - this.wrap.selectAll('path.link').remove(); - this.wrap.selectAll('.barAnnotation').remove(); - - //Update legend to represent categories represented in chart. - this.makeLegend(); - var currentLinks = d3.set(this.filtered_data.map(function (d) { - return d[context.config.link_col]; - })).values(); - this.wrap.selectAll('.legend-item').filter(function () { - return currentLinks.indexOf(d3.select(this).select('.legend-label')[0][0].textContent) === -1; - }).remove(); - - /**-------------------------------------------------------------------------------------------\ - Default links - \-------------------------------------------------------------------------------------------**/ - - //Capture stacked bar groups. - var barGroups = this.svg.selectAll('.bar-group'); - var nBarGroups = barGroups[0].length - 1; - barGroups.each(function (barGroup, i) { - //Annotate bars and modify tooltips. - var yPosition = barGroup.total; - d3.select(this).selectAll('rect.wc-data-mark').each(function (d) { - var bar = d3.select(this); - bar.classed(d.key.replace(/[^a-z0-9]/gi, ''), true); - var IDs = d.values.raw.map(function (d) { - return d[context.config.id_col]; + //Filter raw data on selected [ settings.id_col ] values and nest by node and link. + var selectedData = d3.nest().key(function (d) { + return d[chart.config.node_col]; + }).key(function (d) { + return d[chart.config.link_col]; + }).rollup(function (d) { + return d.length; + }).entries(chart.raw_data.filter(function (d) { + return selectedIDs.indexOf(d[chart.config.id_col]) > -1; + })); + //Flatten nested data to one item per node per link. + var selectedBarData = []; + selectedData.forEach(function (d) { + d.values.forEach(function (di) { + return selectedBarData.push({ + key: di.key, + values: { + raw: chart.raw_data.filter(function (dii) { + return selectedIDs.indexOf(dii[chart.config.id_col]) > -1 && dii[chart.config.node_col].toString() === d.key.toString() && dii[chart.config.link_col].toString() === di.key.toString(); + }), + x: d.key, + y: di.values, + start: barData.filter(function (dii) { + return dii.node === d.key && dii.link === di.key; + })[0].start + } }); - var n = d.values.raw.length; - var N = context.raw_data.filter(function (di) { - return di[context.config.node_col] === d.values.x; - }).length; - var pct = n / N; - d3.select(bar.node().parentNode).append('text').datum([{ node: d.values.x, link: d.key, text: n + ' (' + d3.format('%')(pct) + ')' }]).attr({ 'class': 'barAnnotation', - 'x': function x(di) { - return context.x(d.values.x); - }, - 'y': function y(di) { - return context.y(yPosition); - }, - 'dx': '.25em', - 'dy': '.9em' }).text(n + ' (' + d3.format('%')(pct) + ')'); - bar.select('title').text(n + ' ' + context.config.id_col + 's at ' + d.key + ' (' + d3.format('%')(pct) + '):' + '\n - ' + IDs.slice(0, 3).join('\n - ') + (n > 3 ? '\n - and ' + (n - 3) + ' more' : '')); - yPosition -= n; }); - - //Draw links from any bar group except the last bar group. - if (i < nBarGroups) { - var barGroup1 = d3.select(this); - var barGroup2 = d3.select(barGroups[0][i + 1]); - drawLinks(context, barGroup1.selectAll('.bar'), barGroup2.selectAll('.bar'), 'defaultLink'); + }); + //Draw bars. + var selectedIDbars = chart.svg.selectAll('rect.selectedIDs').data(selectedBarData).enter().append('rect').attr({ + class: 'selectedIDs', + x: function x(d) { + return chart.x(d.values.x); + }, + y: function y(d) { + return chart.y(d.values.start); + }, + width: d3.select(this).attr('width'), + height: function height(d) { + return chart.y(d.values.start - d.values.y) - chart.y(d.values.start); + } + }).style({ + fill: function fill(d) { + return chart.colorScale(d.key); + }, + stroke: function stroke(d) { + return chart.colorScale(d.key); } }); - - /**-------------------------------------------------------------------------------------------\ - Selected links - \-------------------------------------------------------------------------------------------**/ - - //Flatten [ this.current_data ] to one item per node per link. - var barData = []; - this.current_data.forEach(function (d) { - d.values.forEach(function (di) { - barData.push({ node: d.key, - link: di.key, - start: di.values.start }); + selectedIDbars.each(function () { + d3.select(this).append('title').text(function (di) { + return di.values.y + ' ' + chart.config.id_col + 's at ' + di.values.x + ' (' + d3.format('%')(di.values.y / selectedIDs.length) + '):' + '\n - ' + di.values.raw.map(function (dii) { + return dii[chart.config.id_col]; + }).slice(0, 3).join('\n - ') + (di.values.y > 3 ? '\n - and ' + (di.values.y - 3) + ' more' : ''); }); }); - - //Add click event listener to bars. - var bars = this.wrap.selectAll('rect.wc-data-mark'); - bars.style('cursor', 'pointer').on('click', function (d) { - //Reduce bar opacity. - var thisBar = this; - bars.style('fill-opacity', .25); - context.wrap.selectAll('.defaultLink').style('display', 'none'); - context.wrap.selectAll('.barAnnotation').style('display', 'none'); - context.wrap.selectAll('.selectedIDs').remove(); - context.wrap.selectAll('.selectedLink').remove(); - //Capture [ settings.id_col ] values represented by selected bar. - var selectedIDs = d.values.raw.map(function (d) { - return d[context.config.id_col]; - }); - //Filter raw data on selected [ settings.id_col ] values and nest by node and link. - var selectedData = d3.nest().key(function (d) { - return d[context.config.node_col]; - }).key(function (d) { - return d[context.config.link_col]; - }).rollup(function (d) { - return d.length; - }).entries(context.raw_data.filter(function (d) { - return selectedIDs.indexOf(d[context.config.id_col]) > -1; - })); - //Flatten nested data to one item per node per link. - var selectedBarData = []; - selectedData.forEach(function (d) { - d.values.forEach(function (di) { - return selectedBarData.push({ key: di.key, - values: { raw: context.raw_data.filter(function (dii) { - return selectedIDs.indexOf(dii[context.config.id_col]) > -1 && dii[context.config.node_col].toString() === d.key.toString() && dii[context.config.link_col].toString() === di.key.toString(); - }), - x: d.key, - y: di.values, - start: barData.filter(function (dii) { - return dii.node === d.key && dii.link === di.key; - })[0].start } }); - }); - }); - //Draw bars. - var selectedIDbars = context.svg.selectAll('rect.selectedIDs').data(selectedBarData).enter().append('rect').attr({ 'class': 'selectedIDs', - x: function x(d) { - return context.x(d.values.x); - }, - y: function y(d) { - return context.y(d.values.start); - }, - width: d3.select(this).attr('width'), - height: function height(d) { - return context.y(d.values.start - d.values.y) - context.y(d.values.start); - } }).style({ fill: function fill(d) { - return context.colorScale(d.key); - }, - stroke: function stroke(d) { - return context.colorScale(d.key); - } }); - selectedIDbars.each(function () { - d3.select(this).append('title').text(function (di) { - return di.values.y + ' ' + context.config.id_col + 's at ' + di.values.x + ' (' + d3.format('%')(di.values.y / selectedIDs.length) + '):' + '\n - ' + di.values.raw.map(function (dii) { - return dii[context.config.id_col]; - }).slice(0, 3).join('\n - ') + (di.values.y > 3 ? '\n - and ' + (di.values.y - 3) + ' more' : ''); - }); - }); - //Annotate bars. - context.svg.selectAll('text.selectedIDs').data(selectedBarData).enter().append('text').attr({ 'class': 'selectedIDs', - x: function x(d) { - return context.x(d.values.x); - }, - y: function y(d) { - return context.y(d.values.start); - }, - dx: '.25em', - dy: '.9em' }).text(function (d) { - return d.values.y + ' (' + d3.format('%')(d.values.y / selectedIDs.length) + ')'; - }); - //Draw links. - var nodes = selectedData.map(function (d) { - return d.key; - }); - for (var i = 0; i < nodes.length; i++) { - if (i < nodes.length - 1) { - drawLinks(context, selectedIDbars.filter(function (d) { - return d.values.x === nodes[i]; - }), selectedIDbars.filter(function (d) { - return d.values.x === nodes[i + 1]; - }), 'selectedLink'); - } + //Annotate bars. + chart.svg.selectAll('text.selectedIDs').data(selectedBarData).enter().append('text').attr({ + class: 'selectedIDs', + x: function x(d) { + return chart.x(d.values.x); + }, + y: function y(d) { + return chart.y(d.values.start); + }, + dx: '.25em', + dy: '.9em' + }).text(function (d) { + return d.values.y + ' (' + d3.format('%')(d.values.y / selectedIDs.length) + ')'; + }); + //Draw links. + var nodes = selectedData.map(function (d) { + return d.key; + }); + for (var i = 0; i < nodes.length; i++) { + if (i < nodes.length - 1) { + drawLinks(chart, selectedIDbars.filter(function (d) { + return d.values.x === nodes[i]; + }), selectedIDbars.filter(function (d) { + return d.values.x === nodes[i + 1]; + }), 'selectedLink'); } - //Add click event listener to selected bars. - selectedIDbars.style('cursor', 'pointer').on('click', function () { - bars.style('fill-opacity', 1); - selectedIDbars.remove(); - context.wrap.selectAll('text.selectedIDs').remove(); - context.wrap.selectAll('.selectedLink').remove(); - context.wrap.selectAll('.defaultLink').style('display', ''); - context.wrap.selectAll('.barAnnotation').style('display', ''); - }); + } + //Add click event listener to selected bars. + selectedIDbars.style('cursor', 'pointer').on('click', function () { + bars.style('fill-opacity', 1); + selectedIDbars.remove(); + chart.wrap.selectAll('text.selectedIDs').remove(); + chart.wrap.selectAll('.selectedLink').remove(); + chart.wrap.selectAll('.defaultLink').style('display', ''); + chart.wrap.selectAll('.barAnnotation').style('display', ''); }); - } - - if (typeof Object.assign != 'function') { - (function () { - Object.assign = function (target) { - 'use strict'; - if (target === undefined || target === null) { - throw new TypeError('Cannot convert undefined or null to object'); - } - - var output = Object(target); - for (var index = 1; index < arguments.length; index++) { - var source = arguments[index]; - if (source !== undefined && source !== null) { - for (var nextKey in source) { - if (source.hasOwnProperty(nextKey)) { - output[nextKey] = source[nextKey]; - } - } - } - } - return output; - }; - })(); - } - - function interactiveSankey(element, settings$$) { - - //Merge user's settings with default settings.. - var mergedSettings = Object.assign({}, settings, settings$$); - - //Sync settings with data mappings. - mergedSettings = syncSettings(mergedSettings); - - //Sync settings with control inputs. - //let syncedControlInputs = syncControlInputs(controlInputs, mergedSettings); - //let controls = createControls(element, {location: 'top', inputs: syncedControlInputs}); - - //Create chart. - var chart = webcharts.createChart(element, mergedSettings); - chart.on('init', onInit); - chart.on('layout', onLayout); - chart.on('datatransform', onDataTransform); - chart.on('draw', onDraw); - chart.on('resize', onResize); - - return chart; - } - - return interactiveSankey; -})(webCharts); - + }); +} + +var callbacks = { + onInit: onInit, + onLayout: onLayout, + onPreprocess: onPreprocess, + onDataTransform: onDataTransform, + onDraw: onDraw, + onResize: onResize, + onDestroy: onDestroy +}; + +function interactiveSankey(element, settings) { + //Merge user's settings with default settings.. + var mergedSettings = Object.assign({}, defaultSettings, settings); + + //Sync settings with data mappings. + var syncedSettings = syncSettings(mergedSettings); + + //Create chart. + var chart = webcharts.createChart(element, syncedSettings); + for (var callback in callbacks) { + chart.on(callback.toLowerCase().substring(2), callbacks[callback]); + }return chart; +} + +return interactiveSankey; + +}))); diff --git a/package.json b/package.json index 737bf48..4e3a96c 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,25 @@ { "name": "interactive-sankey", - "version": "0.0.0", + "version": "0.1.0", "description": "Sankey diagram visualizing change in state", "main": "./build/interactiveSankey.js", - "scripts": { - "build": "rollup -c ./scripts/rollup.wrapper.config.js | babel > ./build/interactiveSankey.js", - "build-chf": "rollup -c ./scripts/rollup.chf.config.js > ./build/interactiveSankey-chf.js" - }, "author": "Rho, Inc.", "license": "MIT", "dependencies": { - "d3": "^3.5.14", - "webcharts": "^1.3.2", - "react": "^0.14.7" + "d3": "~3", + "webcharts": "~1.7" + }, + "scripts": { + "format": "prettier --print-width=100 --tab-width=4 --single-quote --write \"src/**/*.js\"", + "build": "npm run format && rollup -c", + "watch": "rollup -c -w" }, "devDependencies": { - "babel": "^5.8.35", + "babel-cli": "^6.0.0", "babel-preset-es2015-rollup": "^1.1.1", - "rollup": "^0.25.1", - "rollup-plugin-babel": "^2.4.0" + "rollup": "^0.41.6", + "rollup-plugin-babel": "^2.4.0", + "rollup-watch": "~3.2.2", + "prettier":"~1" } } diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..03c656a --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,34 @@ +import babel from 'rollup-plugin-babel'; + +module.exports = { + moduleName: 'interactiveSankey', + entry: './src/wrapper.js', + dest: './build/interactiveSankey.js', + format: 'umd', + globals: { + d3: 'd3', + webcharts: 'webCharts' + }, + external: (function() { + var dependencies = require('./package.json').dependencies; + + return Object.keys(dependencies); + }()), + plugins: [ + babel( + { + "presets": [ + [ + "es2015", + { + "modules": false + } + ] + ], + "plugins": [ + "external-helpers" + ], + "exclude": "node_modules/**" + }) + ] +}; diff --git a/scripts/rollup.chf.config.js b/scripts/rollup.chf.config.js deleted file mode 100644 index 1673438..0000000 --- a/scripts/rollup.chf.config.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - entry: './src/chartfoundry/util/Renderer.js', - format: 'umd', - globals: { - webcharts: 'webCharts', - d3: 'd3', - react: 'React' - }, - moduleName: 'interactiveSankey', - plugins: [ - babel({ - exclude: 'node_modules/**', - presets: ['es2015-rollup'] - }) - ] -}; diff --git a/scripts/rollup.wrapper.config.js b/scripts/rollup.wrapper.config.js deleted file mode 100644 index b66369a..0000000 --- a/scripts/rollup.wrapper.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - entry: './src/wrapper.js', - format: 'iife', - globals: { - webcharts: 'webCharts', - d3: 'd3' - }, - moduleName: 'interactiveSankey' -}; diff --git a/src/callbacks/index.js b/src/callbacks/index.js new file mode 100644 index 0000000..2fedce3 --- /dev/null +++ b/src/callbacks/index.js @@ -0,0 +1,17 @@ +import onDataTransform from './onDataTransform'; +import onDestroy from './onDestroy'; +import onDraw from './onDraw'; +import onInit from './onInit'; +import onLayout from './onLayout'; +import onPreprocess from './onPreprocess'; +import onResize from './onResize'; + +export default { + onInit: onInit, + onLayout: onLayout, + onPreprocess: onPreprocess, + onDataTransform: onDataTransform, + onDraw: onDraw, + onResize: onResize, + onDestroy: onDestroy +}; diff --git a/src/onDataTransform.js b/src/callbacks/onDataTransform.js similarity index 65% rename from src/onDataTransform.js rename to src/callbacks/onDataTransform.js index c8a6ed3..4c29887 100644 --- a/src/onDataTransform.js +++ b/src/callbacks/onDataTransform.js @@ -1,3 +1,3 @@ export default function onDataTransform() { - var context = this; + const chart = this; } diff --git a/src/callbacks/onDestroy.js b/src/callbacks/onDestroy.js new file mode 100644 index 0000000..fe8740b --- /dev/null +++ b/src/callbacks/onDestroy.js @@ -0,0 +1,3 @@ +export default function onDestroy() { + const chart = this; +} diff --git a/src/onDraw.js b/src/callbacks/onDraw.js similarity index 60% rename from src/onDraw.js rename to src/callbacks/onDraw.js index 6da7eb4..c269301 100644 --- a/src/onDraw.js +++ b/src/callbacks/onDraw.js @@ -1,3 +1,3 @@ export default function onDraw() { - var context = this; + const chart = this; } diff --git a/src/callbacks/onInit.js b/src/callbacks/onInit.js new file mode 100644 index 0000000..971f103 --- /dev/null +++ b/src/callbacks/onInit.js @@ -0,0 +1,11 @@ +export default function onInit() { + const chart = this; + + //Sort raw data by node so that links are drawn between adjacent nodes. + this.raw_data = this.raw_data.sort( + (a, b) => + a[this.config.node_col] < b[this.config.node_col] + ? -1 + : a[this.config.node_col] > b[this.config.node_col] ? 1 : 0 + ); +} diff --git a/src/onLayout.js b/src/callbacks/onLayout.js similarity index 61% rename from src/onLayout.js rename to src/callbacks/onLayout.js index 47fa2ef..f29df5a 100644 --- a/src/onLayout.js +++ b/src/callbacks/onLayout.js @@ -1,3 +1,3 @@ export default function onLayout() { - var context = this; + const chart = this; } diff --git a/src/callbacks/onPreprocess.js b/src/callbacks/onPreprocess.js new file mode 100644 index 0000000..affb8a4 --- /dev/null +++ b/src/callbacks/onPreprocess.js @@ -0,0 +1,3 @@ +export default function onPreprocess() { + const chart = this; +} diff --git a/src/callbacks/onResize.js b/src/callbacks/onResize.js new file mode 100644 index 0000000..d11d094 --- /dev/null +++ b/src/callbacks/onResize.js @@ -0,0 +1,216 @@ +import drawLinks from './onResize/drawLinks'; + +export default function onResize() { + const chart = this; + + //Reset displays. + this.wrap.selectAll('path.link').remove(); + this.wrap.selectAll('.barAnnotation').remove(); + + //Update legend to represent categories represented in chart. + this.makeLegend(); + var currentLinks = d3.set(this.filtered_data.map(d => d[chart.config.link_col])).values(); + this.wrap + .selectAll('.legend-item') + .filter(function() { + return ( + currentLinks.indexOf(d3.select(this).select('.legend-label')[0][0].textContent) === + -1 + ); + }) + .remove(); + + /**-------------------------------------------------------------------------------------------\ + Default links + \-------------------------------------------------------------------------------------------**/ + + //Capture stacked bar groups. + var barGroups = this.svg.selectAll('.bar-group'); + var nBarGroups = barGroups[0].length - 1; + barGroups.each(function(barGroup, i) { + //Annotate bars and modify tooltips. + var yPosition = barGroup.total; + d3.select(this).selectAll('rect.wc-data-mark').each(function(d) { + var bar = d3.select(this); + bar.classed(d.key.replace(/[^a-z0-9]/gi, ''), true); + var IDs = d.values.raw.map(d => d[chart.config.id_col]); + var n = d.values.raw.length; + var N = chart.raw_data.filter(di => di[chart.config.node_col] === d.values.x).length; + var pct = n / N; + d3 + .select(bar.node().parentNode) + .append('text') + .datum([ + { node: d.values.x, link: d.key, text: n + ' (' + d3.format('%')(pct) + ')' } + ]) + .attr({ + class: 'barAnnotation', + x: di => chart.x(d.values.x), + y: di => chart.y(yPosition), + dx: '.25em', + dy: '.9em' + }) + .text(n + ' (' + d3.format('%')(pct) + ')'); + bar + .select('title') + .text( + n + + ' ' + + chart.config.id_col + + 's at ' + + d.key + + ' (' + + d3.format('%')(pct) + + '):' + + '\n - ' + + IDs.slice(0, 3).join('\n - ') + + (n > 3 ? '\n - and ' + (n - 3) + ' more' : '') + ); + yPosition -= n; + }); + + //Draw links from any bar group except the last bar group. + if (i < nBarGroups) { + var barGroup1 = d3.select(this); + var barGroup2 = d3.select(barGroups[0][i + 1]); + drawLinks( + chart, + barGroup1.selectAll('.bar'), + barGroup2.selectAll('.bar'), + 'defaultLink' + ); + } + }); + + /**-------------------------------------------------------------------------------------------\ + Selected links + \-------------------------------------------------------------------------------------------**/ + + //Flatten [ this.current_data ] to one item per node per link. + var barData = []; + this.current_data.forEach(d => { + d.values.forEach(di => { + barData.push({ + node: d.key, + link: di.key, + start: di.values.start + }); + }); + }); + + //Add click event listener to bars. + var bars = this.wrap.selectAll('rect.wc-data-mark'); + bars.style('cursor', 'pointer').on('click', function(d) { + //Reduce bar opacity. + var thisBar = this; + bars.style('fill-opacity', 0.25); + chart.wrap.selectAll('.defaultLink').style('display', 'none'); + chart.wrap.selectAll('.barAnnotation').style('display', 'none'); + chart.wrap.selectAll('.selectedIDs').remove(); + chart.wrap.selectAll('.selectedLink').remove(); + //Capture [ settings.id_col ] values represented by selected bar. + var selectedIDs = d.values.raw.map(d => d[chart.config.id_col]); + //Filter raw data on selected [ settings.id_col ] values and nest by node and link. + var selectedData = d3 + .nest() + .key(d => d[chart.config.node_col]) + .key(d => d[chart.config.link_col]) + .rollup(d => d.length) + .entries(chart.raw_data.filter(d => selectedIDs.indexOf(d[chart.config.id_col]) > -1)); + //Flatten nested data to one item per node per link. + var selectedBarData = []; + selectedData.forEach(d => { + d.values.forEach(di => + selectedBarData.push({ + key: di.key, + values: { + raw: chart.raw_data.filter( + dii => + selectedIDs.indexOf(dii[chart.config.id_col]) > -1 && + dii[chart.config.node_col].toString() === d.key.toString() && + dii[chart.config.link_col].toString() === di.key.toString() + ), + x: d.key, + y: di.values, + start: barData.filter(dii => dii.node === d.key && dii.link === di.key)[0] + .start + } + }) + ); + }); + //Draw bars. + var selectedIDbars = chart.svg + .selectAll('rect.selectedIDs') + .data(selectedBarData) + .enter() + .append('rect') + .attr({ + class: 'selectedIDs', + x: d => chart.x(d.values.x), + y: d => chart.y(d.values.start), + width: d3.select(this).attr('width'), + height: d => chart.y(d.values.start - d.values.y) - chart.y(d.values.start) + }) + .style({ + fill: d => chart.colorScale(d.key), + stroke: d => chart.colorScale(d.key) + }); + selectedIDbars.each(function() { + d3 + .select(this) + .append('title') + .text( + di => + di.values.y + + ' ' + + chart.config.id_col + + 's at ' + + di.values.x + + ' (' + + d3.format('%')(di.values.y / selectedIDs.length) + + '):' + + '\n - ' + + di.values.raw + .map(dii => dii[chart.config.id_col]) + .slice(0, 3) + .join('\n - ') + + (di.values.y > 3 ? '\n - and ' + (di.values.y - 3) + ' more' : '') + ); + }); + //Annotate bars. + chart.svg + .selectAll('text.selectedIDs') + .data(selectedBarData) + .enter() + .append('text') + .attr({ + class: 'selectedIDs', + x: d => chart.x(d.values.x), + y: d => chart.y(d.values.start), + dx: '.25em', + dy: '.9em' + }) + .text(d => d.values.y + ' (' + d3.format('%')(d.values.y / selectedIDs.length) + ')'); + //Draw links. + var nodes = selectedData.map(d => d.key); + for (var i = 0; i < nodes.length; i++) { + if (i < nodes.length - 1) { + drawLinks( + chart, + selectedIDbars.filter(d => d.values.x === nodes[i]), + selectedIDbars.filter(d => d.values.x === nodes[i + 1]), + 'selectedLink' + ); + } + } + //Add click event listener to selected bars. + selectedIDbars.style('cursor', 'pointer').on('click', function() { + bars.style('fill-opacity', 1); + selectedIDbars.remove(); + chart.wrap.selectAll('text.selectedIDs').remove(); + chart.wrap.selectAll('.selectedLink').remove(); + chart.wrap.selectAll('.defaultLink').style('display', ''); + chart.wrap.selectAll('.barAnnotation').style('display', ''); + }); + }); +} diff --git a/src/callbacks/onResize/drawLinks.js b/src/callbacks/onResize/drawLinks.js new file mode 100644 index 0000000..6d476fb --- /dev/null +++ b/src/callbacks/onResize/drawLinks.js @@ -0,0 +1,150 @@ +export default function drawLinks(chartObject, bars1, bars2, linkClass) { + //Merge adjacent bar groups by [ config.y.column ]. + var linkData = []; + bars1.each(function(bar1, i1) { + bars2.each(function(bar2, i2) { + bar1.values.raw.forEach(function(id1) { + bar2.values.raw.forEach(function(id2) { + if (id1[chartObject.config.y.column] === id2[chartObject.config.y.column]) + linkData.push({ + id: id1[chartObject.config.y.column], + split1: id1[chartObject.config.color_by], + x1: id1[chartObject.config.x.column], + split2: id2[chartObject.config.color_by], + x2: id2[chartObject.config.x.column] + }); + }); + }); + }); + }); + + //Nest merged bar groups by their respective [ config.marks.color_by ] values. + var nestedLinkData = d3 + .nest() + .key(d => d.split1) + .key(d => d.split2) + .rollup(d => { + return { n: d.length, IDs: d.map(di => di.id) }; + }) + .entries(linkData); + nestedLinkData.sort((a, b) => (a.key > b.key ? 1 : -1)); + + //Flatten nested data array to one item per left bar [ config.marks.color_by ] + //value per right bar [ config.marks.color_by ] value. + var collapsedLinkData = []; + var offsetList = []; + nestedLinkData.forEach((d, i) => { + var cat1 = bars1.data()[0].values.x; + var bar1 = bars1.filter(dii => dii.key === d.key).data()[0].values; + var offset1 = 0; + d.values.sort((a, b) => (a.key < b.key ? -1 : b.key < a.key ? 1 : 0)); + + d.values.forEach(di => { + var cat2 = bars2.data()[0].values.x; + var bar2 = bars2.filter(dii => dii.key === di.key).data()[0].values; + offsetList.push({ key: di.key, offset: 0 }); + var offset2 = offsetList.filter(dii => dii.key === di.key)[0].offset; + + collapsedLinkData.push({ + split1: d.key, + split2: di.key, + + x1: chartObject.x(cat1), + x2: chartObject.x(cat2), + + y1: bar1.y, + y2: bar2.y, + + start1: bar1.start, + start2: bar2.start, + + stop1: bar1.start - bar1.y, + stop2: bar2.start - bar2.y, + + y01: bar1.start - bar1.y + offset1, + y11: bar1.start - bar1.y + di.values.n + offset1, + + y02: bar2.start - bar2.y + offset2, + y12: bar2.start - bar2.y + di.values.n + offset2, + + n: di.values.n, + height: chartObject.y(di.values.n), + + IDs: di.values.IDs + }); + + offset1 += collapsedLinkData[collapsedLinkData.length - 1].n; + offsetList.filter(dii => dii.key === di.key)[0].offset += di.values.n; + }); + }); + + //Draw links. + var pathDrawer = d3.svg.area().x(d => d.x).y0(d => d.y0).y1(d => d.y1); + collapsedLinkData.forEach((d, i) => { + var path = [ + { + split1: d.split1, + split2: d.split2, + n: d.n, + IDs: d.IDs, + + x: d.x1 + chartObject.x.rangeBand(), + y0: chartObject.y(d.y01), + y1: chartObject.y(d.y11) + }, + { + x: d.x2, + y0: chartObject.y(d.y02), + y1: chartObject.y(d.y12) + } + ]; + chartObject.svg + .append('path') + .datum(path) + .attr({ + d: pathDrawer, + class: 'link ' + linkClass + }) + .style({ + fill: () => chartObject.colorScale(d.split1), + 'fill-opacity': 0.5, + stroke: () => chartObject.colorScale(d.split1), + 'stroke-opacity': 0.5 + }); + }); + + //Add event listeners and tooltips to links. + var links = d3.selectAll('path.link'); + links + .on('mouseover', function() { + d3.select(this).style({ + 'fill-opacity': 1, + 'stroke-opacity': 1 + }); + }) + .on('mouseout', function() { + d3.select(this).style({ + 'fill-opacity': 0.5, + 'stroke-opacity': 0.5 + }); + }) + .append('title') + .text(d => { + var n = d[0].n; + var split1 = d[0].split1; + var split2 = d[0].split2; + var IDs = d[0].IDs; + return ( + n + + ' ' + + chartObject.config.y.column + + (n > 1 ? 's ' : ' ') + + (split1 === split2 + ? 'remained at ' + split1 + : 'progressed from ' + split1 + ' to ' + split2) + + ':\n - ' + + IDs.slice(0, 3).join('\n - ') + + (n > 3 ? '\n - and ' + (n - 3) + ' more' : '') + ); + }); +} diff --git a/src/chartfoundry/binding.js b/src/chartfoundry/binding.js deleted file mode 100644 index 4a2645c..0000000 --- a/src/chartfoundry/binding.js +++ /dev/null @@ -1,244 +0,0 @@ - -export default { - dataMappings : [ - { - source:"x", - target:"x.column" - }, - { - source:"x_order", - target:"x.order" - }, - { - source:"x_domain", - target:"x.domain" - }, - { - source:"y", - target:"y.column" - }, - { - source:"y_order", - target:"y.order" - }, - { - source:"y_domain", - target:"y.domain" - }, - { - source:"group", - target:"marks.0.per" - }, - { - source:"subgroup", - target:"marks.0.split" - }, - { - source:"subset", - target:"marks.0.values" - }, - { - source:"group2", - target:"marks.1.per" - }, - { - source:"subgroup2", - target:"marks.1.split" - }, - { - source:"subset2", - target:"marks.1.values" - }, - { - source:"color_by", - target:"color_by" - }, - { - source:"legend_order", - target:"legend.order" - }, - { - source:"tooltip", - target:"marks.0.tooltip" - } - ], - chartProperties: [ - { - source:"date_format", - target:"date_format" - }, - { - source:"x_label", - target:"x.label" - }, - - { - source:"x_type", - target:"x.type" - }, - { - source:"x_format", - target:"x.format" - }, - { - source:"x_sort", - target:"x.sort" - }, - { - source:"x_bin", - target:"x.bin" - }, - { - source:"x_behavior", - target:"x.behavior" - }, - { - source:"y_label", - target:"y.label" - }, - { - source:"y_type", - target:"y.type" - }, - { - source:"y_format", - target:"y.format" - }, - { - source:"y_sort", - target:"y.sort" - }, - { - source:"y_bin", - target:"y.bin" - }, - { - source:"y_behavior", - target:"y.behavior" - }, - { - source:"marks_type", - target:"marks.0.type" - }, - { - source:"marks_summarizeX", - target:"marks.0.summarizeX" - }, - { - source:"marks_summarizeY", - target:"marks.0.summarizeY" - }, - { - source:"marks_arrange", - target:"marks.0.arrange" - }, - { - source:"marks_fill_opacity", - target:"marks.0.attributes.fill-opacity" - }, - { - source:"marks_tooltip", - target:"marks.0.tooltip" - }, - { - source:"marks_text", - target:"marks.0.text" - }, - { - source:"marks2_type", - target:"marks.1.type" - }, - { - source:"marks2_summarizeX", - target:"marks.1.summarizeX" - }, - { - source:"marks2_summarizeY", - target:"marks.1.summarizeY" - }, - { - source:"marks2_arrange", - target:"marks.1.arrange" - }, - { - source:"marks2_fill_opacity", - target:"marks.1.attributes.fill-opacity" - }, - { - source:"marks2_tooltip", - target:"marks.1.tooltip" - }, - { - source:"marks2_text", - target:"marks.1.text" - }, - { - source:"transitions", - target:"transitions" - }, - { - source:"aspect_ratio", - target:"aspect" - }, - { - source:"range_band", - target:"range_band" - }, - { - source:"colors", - target:"colors" - }, - { - source:"gridlines", - target:"gridlines" - }, - { - source:"max_width", - target:"max_width" - }, - { - source:"width", - target:"width" - }, - { - source:"height", - target:"height" - }, - { - source:"margin_top", - target:"margin.top" - }, - { - source:"margin_bottom", - target:"margin.bottom" - }, - { - source:"margin_left", - target:"margin.left" - }, - { - source:"margin_right", - target:"margin.right" - }, - { - source:"resizable", - target:"resizable" - }, - { - source:"scale_text", - target:"scale_text" - }, - { - source: "legend_mark", - target: "legend.mark" - }, - { - source: "legend_label", - target: "legend.label" - }, - { - source: "legend_location", - target: "legend.location" - } - ] -} diff --git a/src/chartfoundry/util/Renderer.js b/src/chartfoundry/util/Renderer.js deleted file mode 100644 index 03b19b3..0000000 --- a/src/chartfoundry/util/Renderer.js +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; -import stringAccessor from './string-accessor'; -import binding from '../binding'; -import reactTemplate from './reactTemplate'; -import defaultSettings, { syncSettings } from '../../default-settings'; -import { version as d3Version } from 'd3'; -import { version as wcVersion } from 'webcharts'; - -function describeCode(props) { - const settings = this.createSettings(props); - const code = -`// uses d3 v.${d3Version} -// uses webcharts v.${wcVersion} -// uses your-library-name v.1.1.0 - -var settings = ${JSON.stringify(settings, null, 2)}; - -var myChart = yourRendererName(dataElement, settings); - -d3.csv(dataPath, function(error, csv) { - myChart.init(csv); -}); -`; - return code; -} - - -export default class Renderer extends React.Component { - constructor(props) { - super(props); - this.binding = binding; - this.describeCode = describeCode.bind(this); - this.state = { data: [], settings: {}, template: {}, loadMsg: 'Loading...' }; - } - createSettings(props) { - // set placeholders for anything the user can change - const shell = defaultSettings; - - binding.dataMappings.forEach(e => { - let chartVal = stringAccessor(props.dataMappings, e.source); - if (chartVal) { - stringAccessor(shell, e.target, chartVal); - } - else { - let defaultVal = stringAccessor(props.template.dataMappings, e.source+'.default'); - if (defaultVal && typeof defaultVal === 'string' && defaultVal.slice(0,3) === 'dm$') { - var pointerVal = stringAccessor(props.dataMappings, defaultVal.slice(3)) || null; - stringAccessor(shell, e.target, pointerVal); - } - else if(defaultVal){ - stringAccessor(shell, e.target, defaultVal); - } - } - }); - binding.chartProperties.forEach(e => { - let chartVal = stringAccessor(props.chartProperties, e.source); - if (chartVal !== undefined) { - stringAccessor(shell, e.target, chartVal); - } - else { - let defaultVal = stringAccessor(props.template.chartProperties, e.source+'.default'); - stringAccessor(shell, e.target, defaultVal); - } - }); - - return syncSettings(shell); - } - componentWillMount() { - var settings = this.createSettings(this.props); - this.setState({ settings }); - } - componentWillReceiveProps(nextProps){ - var settings = this.createSettings(nextProps); - this.setState({ settings }); - } - render() { - return ( - React.createElement(reactTemplate, { - id: this.props.id, - settings: this.state.settings, - controlInputs: this.props.template.controls, - data: this.props.data - }) - ); - } -} diff --git a/src/chartfoundry/util/reactTemplate.js b/src/chartfoundry/util/reactTemplate.js deleted file mode 100644 index edad77b..0000000 --- a/src/chartfoundry/util/reactTemplate.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { select } from 'd3'; -import mainChart from '../../wrapper'; - -export default class ReactYourProjectName extends React.Component { - constructor(props) { - super(props); - this.state = {}; - } - componentDidMount(prevProps, prevState){ - if(this.props.data.length){ - //manually clear div and redraw - select(`.chart-div.id-${this.props.id}`).selectAll('*').remove(); - let chart = mainChart(`.chart-div.id-${this.props.id}`, this.props.settings).init(this.props.data); - } - } - componentDidUpdate(prevProps, prevState){ - if(this.props.data.length){ - //manually clear div and redraw - select(`.chart-div.id-${this.props.id}`).selectAll('*').remove(); - let chart = mainChart(`.chart-div.id-${this.props.id}`, this.props.settings).init(this.props.data); - } - } - render(){ - return ( - React.createElement('div', { - key: this.props.id, - className: `chart-div id-${this.props.id} ${!(this.props.data.length) ? 'loading' : ''}`, - style: { minHeight: '1px', minWidth: '1px' } - }) - ); - } -} - -ReactYourProjectName.defaultProps = {data: [], controlInputs: [], id: 'id'} \ No newline at end of file diff --git a/src/chartfoundry/util/string-accessor.js b/src/chartfoundry/util/string-accessor.js deleted file mode 100644 index 5acd849..0000000 --- a/src/chartfoundry/util/string-accessor.js +++ /dev/null @@ -1,17 +0,0 @@ -export default function(o, s, v) { - //adapted from http://jsfiddle.net/alnitak/hEsys/ - s = s.replace(/\[(\w+)\]/g, '.$1'); - s = s.replace(/^\./, ''); - var a = s.split('.'); - for (var i = 0, n = a.length; i < n; ++i) { - var k = a[i]; - if (k in o) { - if(i == n-1 && v !== undefined) - o[k] = v; - o = o[k]; - } else { - return; - } - } - return o; -} \ No newline at end of file diff --git a/src/default-settings.js b/src/default-settings.js deleted file mode 100644 index 838e3ef..0000000 --- a/src/default-settings.js +++ /dev/null @@ -1,40 +0,0 @@ -const settings = - //Customizable template settings - {id_col: 'USUBJID' - ,node_col: null - ,link_col: null - - //Standard template settings - ,x: {type: 'ordinal'} - ,y: {type: 'linear'} - ,marks: - [ {type: 'bar' - ,arrange: 'stacked' - ,summarizeY: 'count' - ,tooltip: '$y at $x'} - ] - ,legend: - {} - }; - -export function syncSettings(settings) { - settings.x.column = settings.node_col; - settings.x.label = settings.node_col; - settings.y.column = settings.id_col; - settings.y.label = '# of ' + settings.id_col + 's'; - settings.marks[0].per = [settings.node_col]; - settings.marks[0].split = settings.link_col; - settings.color_by = settings.link_col; - - return settings; -} - -export const controlInputs = - [ - ]; - -export function syncControlInputs(controlInputs, settings) { - return controlInputs -} - -export default settings diff --git a/src/defaultSettings.js b/src/defaultSettings.js new file mode 100644 index 0000000..0ee0aa0 --- /dev/null +++ b/src/defaultSettings.js @@ -0,0 +1,37 @@ +export default //Customizable template settings +{ + id_col: 'USUBJID', + node_col: null, + link_col: null, + + //Standard template settings + x: { type: 'ordinal' }, + y: { type: 'linear' }, + marks: [ + { + type: 'bar', + arrange: 'stacked', + summarizeY: 'count', + tooltip: '$y at $x' + } + ], + legend: {} +}; + +export function syncSettings(settings) { + settings.x.column = settings.node_col; + settings.x.label = settings.node_col; + settings.y.column = settings.id_col; + settings.y.label = '# of ' + settings.id_col + 's'; + settings.marks[0].per = [settings.node_col]; + settings.marks[0].split = settings.link_col; + settings.color_by = settings.link_col; + + return settings; +} + +export const controlInputs = []; + +export function syncControlInputs(controlInputs, settings) { + return controlInputs; +} diff --git a/src/onInit.js b/src/onInit.js deleted file mode 100644 index 7f00b31..0000000 --- a/src/onInit.js +++ /dev/null @@ -1,8 +0,0 @@ -export default function onInit() { - var context = this; - - //Sort raw data by node so that links are drawn between adjacent nodes. - this.raw_data = this.raw_data.sort((a,b) => - a[this.config.node_col] < b[this.config.node_col] ? -1 : - a[this.config.node_col] > b[this.config.node_col] ? 1 : 0) -} diff --git a/src/onResize.js b/src/onResize.js deleted file mode 100644 index 3a0e136..0000000 --- a/src/onResize.js +++ /dev/null @@ -1,180 +0,0 @@ -import drawLinks from './util/drawLinks'; - -export default function onResize() { - var context = this; - - //Reset displays. - this.wrap.selectAll('path.link').remove(); - this.wrap.selectAll('.barAnnotation').remove(); - - //Update legend to represent categories represented in chart. - this.makeLegend(); - var currentLinks = d3.set(this.filtered_data.map(d => d[context.config.link_col])) - .values(); - this.wrap.selectAll('.legend-item') - .filter(function() { - return currentLinks.indexOf(d3.select(this).select('.legend-label')[0][0].textContent) === -1; }) - .remove(); - - /**-------------------------------------------------------------------------------------------\ - Default links - \-------------------------------------------------------------------------------------------**/ - - //Capture stacked bar groups. - var barGroups = this.svg.selectAll('.bar-group'); - var nBarGroups = barGroups[0].length - 1; - barGroups.each(function(barGroup,i) { - //Annotate bars and modify tooltips. - var yPosition = barGroup.total; - d3.select(this).selectAll('rect.wc-data-mark') - .each(function (d) { - var bar = d3.select(this); - bar.classed(d.key.replace(/[^a-z0-9]/gi, ''), true); - var IDs = d.values.raw.map(d => d[context.config.id_col]); - var n = d.values.raw.length; - var N = context.raw_data.filter(di => di[context.config.node_col] === d.values.x).length; - var pct = n / N; - d3.select(bar.node().parentNode) - .append('text') - .datum([{node: d.values.x, link: d.key, text: n + ' (' + d3.format('%')(pct) + ')'}]) - .attr( - {'class': 'barAnnotation' - ,'x': di => context.x(d.values.x) - ,'y': di => context.y(yPosition) - ,'dx': '.25em' - ,'dy': '.9em' }) - .text(n + ' (' + d3.format('%')(pct) + ')'); - bar.select('title') - .text(n + ' ' + context.config.id_col + 's at ' + d.key + ' (' + d3.format('%')(pct) + '):' + - '\n - ' + IDs.slice(0, 3).join('\n - ') + (n > 3 ? '\n - and ' + (n - 3) + ' more' : '')); - yPosition -= n; - }); - - //Draw links from any bar group except the last bar group. - if (i < nBarGroups) { - var barGroup1 = d3.select(this); - var barGroup2 = d3.select(barGroups[0][i + 1]); - drawLinks - (context - ,barGroup1.selectAll('.bar') - ,barGroup2.selectAll('.bar') - ,'defaultLink'); - } - }); - - /**-------------------------------------------------------------------------------------------\ - Selected links - \-------------------------------------------------------------------------------------------**/ - - //Flatten [ this.current_data ] to one item per node per link. - var barData = []; - this.current_data.forEach(d => { - d.values.forEach(di => { - barData.push( - {node: d.key - ,link: di.key - ,start: di.values.start}); - }); - }); - - //Add click event listener to bars. - var bars = this.wrap.selectAll('rect.wc-data-mark'); - bars.style('cursor', 'pointer') - .on('click', function(d) { - //Reduce bar opacity. - var thisBar = this; - bars.style('fill-opacity', .25); - context.wrap.selectAll('.defaultLink').style('display', 'none'); - context.wrap.selectAll('.barAnnotation').style('display', 'none'); - context.wrap.selectAll('.selectedIDs').remove(); - context.wrap.selectAll('.selectedLink').remove(); - //Capture [ settings.id_col ] values represented by selected bar. - var selectedIDs = d.values.raw.map(d => d[context.config.id_col]); - //Filter raw data on selected [ settings.id_col ] values and nest by node and link. - var selectedData = d3.nest() - .key(d => d[context.config.node_col]) - .key(d => d[context.config.link_col]) - .rollup(d => d.length) - .entries(context.raw_data - .filter(d => selectedIDs.indexOf(d[context.config.id_col]) > -1)); - //Flatten nested data to one item per node per link. - var selectedBarData = []; - selectedData - .forEach(d => { - d.values.forEach(di => - selectedBarData.push( - {key: di.key - ,values: - {raw: context.raw_data - .filter(dii => - selectedIDs.indexOf(dii[context.config.id_col]) > -1 && - dii[context.config.node_col].toString() === d.key.toString() && - dii[context.config.link_col].toString() === di.key.toString()) - ,x: d.key - ,y: di.values - ,start: barData.filter(dii => dii.node === d.key && dii.link === di.key)[0].start}})); - }); - //Draw bars. - var selectedIDbars = context.svg.selectAll('rect.selectedIDs') - .data(selectedBarData).enter() - .append('rect') - .attr( - {class: 'selectedIDs' - ,x: d => context.x(d.values.x) - ,y: d => context.y(d.values.start) - ,width: d3.select(this).attr('width') - ,height: d => context.y(d.values.start - d.values.y) - context.y(d.values.start)}) - .style( - {fill: d => context.colorScale(d.key) - ,stroke: d => context.colorScale(d.key)}); - selectedIDbars - .each(function() { - d3.select(this).append('title') - .text(di => - di.values.y + ' ' + - context.config.id_col + 's at ' + - di.values.x + ' (' + - d3.format('%')(di.values.y / selectedIDs.length) + '):' + '\n - ' + - di.values.raw - .map(dii => dii[context.config.id_col]) - .slice(0, 3) - .join('\n - ') + - (di.values.y > 3 ? - '\n - and ' + (di.values.y - 3) + ' more' : - '')); - }); - //Annotate bars. - context.svg.selectAll('text.selectedIDs') - .data(selectedBarData).enter() - .append('text') - .attr( - {class: 'selectedIDs' - ,x: d => context.x(d.values.x) - ,y: d => context.y(d.values.start) - ,dx: '.25em' - ,dy: '.9em' }) - .text(d => d.values.y + ' (' + d3.format('%')(d.values.y/selectedIDs.length) + ')'); - //Draw links. - var nodes = selectedData.map(d => d.key); - for (var i = 0; i < nodes.length; i++) { - if (i < (nodes.length - 1)) { - drawLinks - (context - ,selectedIDbars.filter(d => d.values.x === nodes[i ]) - ,selectedIDbars.filter(d => d.values.x === nodes[i + 1]) - ,'selectedLink'); - } - } - //Add click event listener to selected bars. - selectedIDbars - .style('cursor', 'pointer') - .on('click', function() { - bars.style('fill-opacity', 1); - selectedIDbars.remove(); - context.wrap.selectAll('text.selectedIDs').remove(); - context.wrap.selectAll('.selectedLink').remove(); - context.wrap.selectAll('.defaultLink').style('display', ''); - context.wrap.selectAll('.barAnnotation').style('display', ''); - }); - }); -} diff --git a/src/util/addBoxPlot.js b/src/util/addBoxPlot.js deleted file mode 100644 index 3eb13e5..0000000 --- a/src/util/addBoxPlot.js +++ /dev/null @@ -1,107 +0,0 @@ -export default function addBoxplot(svg, results, height, width, domain, boxPlotWidth, boxColor, boxInsideColor, format, horizontal){ - //set default orientation to "horizontal" - var horizontal = horizontal==undefined ? true : horizontal - - //make the results numeric and sort - var results = results.map(function(d){return +d}).sort(d3.ascending) - - //set up scales - var y = d3.scale.linear() - .range([height, 0]) - - var x = d3.scale.linear() - .range([0, width]) - - if(horizontal){ - y.domain(domain) - }else{ - x.domain(domain) - } - - var probs=[0.05,0.25,0.5,0.75,0.95]; - for(var i=0; i d.split1) - .key(d => d.split2) - .rollup(d => { return {n: d.length, IDs: d.map(di => di.id)}; }) - .entries(linkData); - nestedLinkData - .sort((a,b) => a.key > b.key ? 1 : -1); - - //Flatten nested data array to one item per left bar [ config.marks.color_by ] - //value per right bar [ config.marks.color_by ] value. - var collapsedLinkData = []; - var offsetList = []; - nestedLinkData - .forEach((d,i) => { - var cat1 = bars1.data()[0].values.x; - var bar1 = bars1 - .filter(dii => dii.key === d.key) - .data()[0] - .values; - var offset1 = 0; - d.values.sort((a,b) => - a.key < b.key ? -1 : - b.key < a.key ? 1 : 0); - - d.values.forEach(di => { - var cat2 = bars2.data()[0].values.x; - var bar2 = bars2 - .filter(dii => dii.key === di.key) - .data()[0] - .values; - offsetList.push({key: di.key, offset: 0}); - var offset2 = offsetList.filter(dii => dii.key === di.key)[0].offset; - - collapsedLinkData.push( - {split1: d.key - ,split2: di.key - - ,x1: chartObject.x(cat1) - ,x2: chartObject.x(cat2) - - ,y1: bar1.y - ,y2: bar2.y - - ,start1: bar1.start - ,start2: bar2.start - - ,stop1: bar1.start - bar1.y - ,stop2: bar2.start - bar2.y - - ,y01: bar1.start - bar1.y + offset1 - ,y11: bar1.start - bar1.y + di.values.n + offset1 - - ,y02: bar2.start - bar2.y + offset2 - ,y12: bar2.start - bar2.y + di.values.n + offset2 - - ,n: di.values.n - ,height: chartObject.y(di.values.n) - - ,IDs: di.values.IDs}); - - offset1 += collapsedLinkData[collapsedLinkData.length - 1].n; - offsetList.filter(dii => dii.key === di.key)[0] - .offset += di.values.n; - }); - }); - - //Draw links. - var pathDrawer = d3.svg.area() - .x(d => d.x) - .y0(d => d.y0) - .y1(d => d.y1); - collapsedLinkData - .forEach((d,i) => { - var path = - [ {split1: d.split1 - ,split2: d.split2 - ,n: d.n - ,IDs: d.IDs - - ,x: d.x1 + chartObject.x.rangeBand() - ,y0: chartObject.y(d.y01) - ,y1: chartObject.y(d.y11)} - , {x: d.x2 - ,y0: chartObject.y(d.y02) - ,y1: chartObject.y(d.y12)}]; - chartObject.svg - .append('path') - .datum(path) - .attr( - {'d': pathDrawer - ,'class': 'link ' + linkClass}) - .style( - {'fill': () => chartObject.colorScale(d.split1) - ,'fill-opacity': .5 - ,'stroke': () => chartObject.colorScale(d.split1) - ,'stroke-opacity': .5}); - }); - - //Add event listeners and tooltips to links. - var links = d3.selectAll('path.link'); - links - .on('mouseover', function() { - d3.select(this) - .style( - {'fill-opacity': 1 - ,'stroke-opacity': 1}); }) - .on('mouseout', function() { - d3.select(this) - .style( - {'fill-opacity': .5 - ,'stroke-opacity': .5}); }) - .append('title') - .text(d => { - var n = d[0].n; - var split1 = d[0].split1; - var split2 = d[0].split2; - var IDs = d[0].IDs; - return n + ' ' + chartObject.config.y.column - + (n > 1 ? 's ' : ' ') - + (split1 === split2 ? - 'remained at ' + split1 : - 'progressed from ' + split1 + ' to ' + split2) + ':\n - ' - + IDs.slice(0,3).join('\n - ') - + (n > 3 ? '\n - and ' + (n - 3) + ' more' : ''); - }); -} diff --git a/src/util/lengthen-raw.js b/src/util/lengthen-raw.js deleted file mode 100644 index 009a042..0000000 --- a/src/util/lengthen-raw.js +++ /dev/null @@ -1,15 +0,0 @@ -export default function lengthenRaw(data, columns){ - let my_data = []; - - data.forEach(e => { - columns.forEach(g => { - let obj = Object.assign({}, e); - obj.wc_category = g; - obj.wc_value = e[g]; - my_data.push(obj); - }); - - }); - - return my_data; -} \ No newline at end of file diff --git a/src/util/object-assign.js b/src/util/object-assign.js index 4490ee4..02ef855 100644 --- a/src/util/object-assign.js +++ b/src/util/object-assign.js @@ -1,23 +1,23 @@ if (typeof Object.assign != 'function') { - (function () { - Object.assign = function (target) { - 'use strict'; - if (target === undefined || target === null) { - throw new TypeError('Cannot convert undefined or null to object'); - } + (function() { + Object.assign = function(target) { + 'use strict'; + if (target === undefined || target === null) { + throw new TypeError('Cannot convert undefined or null to object'); + } - var output = Object(target); - for (var index = 1; index < arguments.length; index++) { - var source = arguments[index]; - if (source !== undefined && source !== null) { - for (var nextKey in source) { - if (source.hasOwnProperty(nextKey)) { - output[nextKey] = source[nextKey]; + var output = Object(target); + for (var index = 1; index < arguments.length; index++) { + var source = arguments[index]; + if (source !== undefined && source !== null) { + for (var nextKey in source) { + if (source.hasOwnProperty(nextKey)) { + output[nextKey] = source[nextKey]; + } + } + } } - } - } - } - return output; - }; - })(); -} \ No newline at end of file + return output; + }; + })(); +} diff --git a/src/wrapper.js b/src/wrapper.js index 1f9527b..209788e 100644 --- a/src/wrapper.js +++ b/src/wrapper.js @@ -1,32 +1,19 @@ -import { createChart, createControls, createTable } from 'webcharts'; -import { controlInputs, syncControlInputs, syncSettings } from './default-settings' -import config from './default-settings'; -import onInit from './onInit'; -import onLayout from './onLayout'; -import onDataTransform from './onDataTransform'; -import onDraw from './onDraw'; -import onResize from './onResize'; import './util/object-assign'; +import defaultSettings, { syncSettings } from './defaultSettings'; +import callbacks from './callbacks/index'; +import { createChart } from 'webcharts'; export default function interactiveSankey(element, settings) { + //Merge user's settings with default settings.. + const mergedSettings = Object.assign({}, defaultSettings, settings); - //Merge user's settings with default settings.. - let mergedSettings = Object.assign({}, config, settings); + //Sync settings with data mappings. + const syncedSettings = syncSettings(mergedSettings); - //Sync settings with data mappings. - mergedSettings = syncSettings(mergedSettings); - - //Sync settings with control inputs. - //let syncedControlInputs = syncControlInputs(controlInputs, mergedSettings); - //let controls = createControls(element, {location: 'top', inputs: syncedControlInputs}); - - //Create chart. - let chart = createChart(element, mergedSettings); - chart.on('init', onInit); - chart.on('layout', onLayout); - chart.on('datatransform', onDataTransform); - chart.on('draw', onDraw); - chart.on('resize', onResize); + //Create chart. + const chart = createChart(element, syncedSettings); + for (const callback in callbacks) + chart.on(callback.toLowerCase().substring(2), callbacks[callback]); return chart; }