diff --git a/docs/README.md b/docs/README.md
index 6ec24b9b..42285f32 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -20,6 +20,7 @@ The philosophy of the Visualization Framework is to provide a library to quickly
- [ChordGraph](#chordgraph)
- [SimpleTextGraph](#simpletextgraph)
- [VariationTextGraph](#variationtextgraph)
+ - [SankeyGraph](#sankeygraph)
- [Query configuration](#query-configuration)
- [Services](#services)
@@ -344,6 +345,47 @@ This graph shows a value and its variation from the previous one.
- **fontSize** font size
- **fontColor** font color
+##### SankeyGraph
+
+Display nice Sankey graph
+
+
+
+This graph shows a value between source and destination columns.
+
+- **sourceColumn** source column
+- **targetColumn** target column
+- **valueColumn** value between source and target column.
+
+*Note:- Data should not be cyclic.*
+
+#### Sample Data
+```javascript
+
+const data = [
+ {
+ "source": "Barry",
+ "target": "Elvis",
+ "value": 2
+ },
+ {
+ "source": "Frodo",
+ "target": "Elvis",
+ "value": 2
+ },
+ {
+ "source": "Frodo",
+ "target": "Sarah",
+ "value": 2
+ },
+ {
+ "source": "Barry",
+ "target": "Alice",
+ "value": 2
+ }
+];
+```
+
### Query configuration
The query configuration allows the Visualization to know which [service](#services) it should use and what is the query it should trigger.
diff --git a/public/configurations/dashboards/example.json b/public/configurations/dashboards/example.json
index c5464a50..29b40f36 100644
--- a/public/configurations/dashboards/example.json
+++ b/public/configurations/dashboards/example.json
@@ -5,6 +5,8 @@
"title": "Example",
"visualizations": [
{ "id": "vsd-from-nsgs-table", "x": 0, "y": 0, "w": 6, "h": 12, "minW": 2, "minH": 12, "static": true},
- { "id": "vsd-to-nsgs-table", "x": 6, "y": 0, "w": 6, "h": 12, "minW": 2, "minH": 12, "static": true}
+ { "id": "vsd-to-nsgs-table", "x": 6, "y": 0, "w": 6, "h": 12, "minW": 2, "minH": 12, "static": true},
+ { "id": "sample-sankey-graph", "x": 0, "y": 12, "w": 12, "h": 20, "minW": 6, "minH": 12}
+
]
}
diff --git a/public/configurations/dashboards/kitchenSink.json b/public/configurations/dashboards/kitchenSink.json
index c72069e3..2b97290c 100644
--- a/public/configurations/dashboards/kitchenSink.json
+++ b/public/configurations/dashboards/kitchenSink.json
@@ -70,7 +70,7 @@
{
"id": "effective-score",
"x": 0,
- "y": 15,
+ "y": 30,
"w": 3,
"h": 15,
"minW": 2,
@@ -79,7 +79,7 @@
{
"id": "vss-domain-acl-time",
"x": 3,
- "y": 15,
+ "y": 30,
"w": 3,
"h": 15,
"minW": 2,
@@ -88,7 +88,7 @@
{
"id": "aar-nsg-line-chart",
"x": 6,
- "y": 15,
+ "y": 30,
"w": 3,
"h": 15,
"minW": 2,
@@ -96,8 +96,8 @@
},
{
"id": "aar-nsg-gauge-chart",
- "x": 6,
- "y": 15,
+ "x": 9,
+ "y": 30,
"w": 3,
"h": 15,
"minW": 2,
@@ -105,13 +105,21 @@
},
{
"id": "aar-flow-sla-heatmap",
- "x": 9,
- "y": 15,
+ "x": 0,
+ "y": 45,
"w": 6,
"h": 15,
"minW": 2,
"minH": 12
+ },
+ {
+ "id": "test-area-graph",
+ "x": 6,
+ "y": 45,
+ "w": 6,
+ "h": 15,
+ "minW": 6,
+ "minH": 15
}
-
]
}
diff --git a/public/configurations/visualizations/sample-sankey-graph.json b/public/configurations/visualizations/sample-sankey-graph.json
new file mode 100644
index 00000000..f31dc602
--- /dev/null
+++ b/public/configurations/visualizations/sample-sankey-graph.json
@@ -0,0 +1,18 @@
+{
+ "id": "sample-sankey-graph",
+ "graph": "SankeyGraph",
+ "title": "Sample Sankey Graph",
+ "author": "Anil Chauhan",
+ "creationDate": "25/09/2017",
+ "data": {
+ "dateHistogram": true,
+ "sourceColumn": "source",
+ "sourceColumnLabel": "Source",
+ "targetColumn": "target",
+ "targetColumnLabel": "Target",
+ "valueColumn": "value",
+ "valueColumnLabel": "Value",
+ "valueFormat": ",.1f"
+ },
+ "query": "top5-app-vertical-bar"
+}
diff --git a/public/configurations/visualizations/test-area-graph.json b/public/configurations/visualizations/test-area-graph.json
new file mode 100644
index 00000000..88652c8d
--- /dev/null
+++ b/public/configurations/visualizations/test-area-graph.json
@@ -0,0 +1,32 @@
+{
+ "id": "test-area-graph",
+ "graph": "AreaGraph",
+ "title": "Area Graph",
+ "description": "New Graph - Area Graph Visualization",
+ "author": "Anil Chauhan",
+ "creationDate": "19/08/2017",
+ "data": {
+ "dateHistogram": true,
+ "xColumn": "ts",
+ "xLabel": "Time",
+ "yColumn": ["CPU", "MEMORY", "DISK"],
+ "yTickFormat": ",.1f",
+ "yLabel": "",
+ "yTicks": 5,
+ "xTickGrid": true,
+ "linesColumn": ["CPU", "MEMORY", "DISK"],
+ "legend": {
+ "orientation": "vertical",
+ "show": true,
+ "circleSize": 5,
+ "labelOffset": 5
+ },
+ "tooltip": [
+ { "column": "CPU", "label": "CPU", "format": "0.2f"},
+ { "column": "MEMORY", "label": "MEMORY", "format": "0.2f"},
+ { "column": "DISK", "label": "DISK", "format": "0.2f"},
+ { "column": "ts", "label": "Timestamp", "timeFormat": "%b %d, %y %X"}
+ ]
+ },
+ "query": "vnf-status-linechart"
+}
\ No newline at end of file
diff --git a/src/components/Graphs/AbstractGraph.js b/src/components/Graphs/AbstractGraph.js
index 08bab81d..4a6d41f0 100644
--- a/src/components/Graphs/AbstractGraph.js
+++ b/src/components/Graphs/AbstractGraph.js
@@ -9,14 +9,23 @@ import { GraphManager } from "./index";
import columnAccessor from "../../utils/columnAccessor";
import crossfilter from "crossfilter2";
+import {
+ format
+} from "d3";
+
export default class AbstractGraph extends React.Component {
constructor(props, properties = {}) {
super(props);
+ this.configuredProperties = {};
+
this.defaults = GraphManager.getDefaultProperties(properties);
+ // set all configuration into single object
+ this.setConfiguredProperties();
+
// Provide tooltips for subclasses.
const { tooltip } = this.getConfiguredProperties();
if(tooltip) {
@@ -26,7 +35,6 @@ export default class AbstractGraph extends React.Component {
// This function is invoked to produce the content of a tooltip.
this.getTooltipContent = () => {
-
// The value of this.hoveredDatum should be set by subclasses
// on mouseEnter and mouseMove of visual marks
// to the data entry corresponding to the hovered mark.
@@ -67,6 +75,8 @@ export default class AbstractGraph extends React.Component {
type="dark"
effect="float"
getContent={[() => this.getTooltipContent(), 200]}
+ afterHide={() => this.handleHideEvent()}
+ afterShow={() => this.handleShowEvent()}
/>
);
@@ -87,6 +97,10 @@ export default class AbstractGraph extends React.Component {
}
}
+ handleShowEvent() {}
+
+ handleHideEvent() {}
+
wrapD3Text (text, width) {
text.each(function() {
var text = d3.select(this),
@@ -109,8 +123,12 @@ export default class AbstractGraph extends React.Component {
});
};
+ setConfiguredProperties() {
+ this.configuredProperties = Object.assign({}, this.defaults, this.props.configuration.data);
+ }
+
getConfiguredProperties() {
- return Object.assign({}, this.defaults, this.props.configuration.data);
+ return this.configuredProperties;
}
getMappedScaleColor(data, defaultColumn) {
@@ -290,6 +308,85 @@ export default class AbstractGraph extends React.Component {
);
}
+ setYlabelWidth(data) {
+ const {
+ chartWidthToPixel,
+ yTickFormat
+ } = this.getConfiguredProperties();
+
+ const yLabelFn = (d) => {
+ if(!yTickFormat) {
+ return d['yColumn'];
+ }
+ const formatter = format(yTickFormat);
+ return formatter(d['yColumn']);
+ };
+
+ this.yLabelWidth = this.longestLabelLength(data, yLabelFn) * chartWidthToPixel;
+ }
+
+ getYlabelWidth() {
+ return this.yLabelWidth;
+ }
+
+ setLeftMargin() {
+ const {
+ margin
+ } = this.getConfiguredProperties();
+
+ this.leftMargin = margin.left + this.getYlabelWidth();
+ }
+
+ getLeftMargin() {
+ return this.leftMargin;
+ }
+
+ setAvailableWidth({width}) {
+
+ const {
+ margin,
+ } = this.getConfiguredProperties();
+
+ this.availableWidth = width - (margin.left + margin.right + this.getYlabelWidth());
+ }
+
+ getAvailableWidth() {
+ return this.availableWidth;
+ }
+
+ // height of x-axis
+ getXAxisHeight() {
+ const {
+ chartHeightToPixel,
+ xLabel
+ } = this.getConfiguredProperties();
+
+ return xLabel ? chartHeightToPixel : 0;
+ }
+
+ setAvailableHeight({height}) {
+
+ const {
+ chartHeightToPixel,
+ margin
+ } = this.getConfiguredProperties();
+
+ this.availableHeight = height - (margin.top + margin.bottom + chartHeightToPixel + this.getXAxisHeight());
+
+ }
+
+ getAvailableHeight() {
+ return this.availableHeight;
+ }
+
+ // Check whether to display legend as vertical or horizontal
+ checkIsVerticalLegend() {
+ const {
+ legend
+ } = this.getConfiguredProperties();
+
+ return legend.orientation === 'vertical';
+ }
getGroupedData(data, settings) {
if(settings.otherOptions && settings.otherOptions.limit) {
diff --git a/src/components/Graphs/AreaGraph/default.config.js b/src/components/Graphs/AreaGraph/default.config.js
new file mode 100644
index 00000000..d9bd1784
--- /dev/null
+++ b/src/components/Graphs/AreaGraph/default.config.js
@@ -0,0 +1,23 @@
+import { theme } from "../../../theme"
+
+export const properties = {
+ stroke: {
+ color: "red",
+ width: "2px",
+ opacity: "0.4"
+ },
+ legend: {
+ show: false
+ },
+ colors: [
+ theme.palette.orangeLightColor,
+ theme.palette.blueLightColor,
+ theme.palette.pinkLightColor,
+ theme.palette.orangeLighterColor,
+ theme.palette.greenColor,
+ theme.palette.yellowLightColor,
+ theme.palette.yellowDarkColor,
+ ],
+ zeroStart: true,
+ circleRadius: 5
+}
diff --git a/src/components/Graphs/AreaGraph/index.js b/src/components/Graphs/AreaGraph/index.js
new file mode 100644
index 00000000..b6b3c72a
--- /dev/null
+++ b/src/components/Graphs/AreaGraph/index.js
@@ -0,0 +1,361 @@
+import React from "react";
+import XYGraph from "../XYGraph";
+import { connect } from "react-redux";
+import * as d3 from "d3";
+
+import {
+ line,
+ select,
+ brushX,
+ area
+} from "d3";
+
+import {properties} from "./default.config";
+
+class AreaGraph extends XYGraph {
+
+ constructor(props) {
+ super(props, properties);
+ this.brush = brushX();
+ }
+
+ componentWillMount() {
+ this.initiate(this.props);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if(this.props !== nextProps) {
+ this.initiate(nextProps);
+ }
+ }
+
+ initiate(props) {
+ const {
+ data,
+ } = props;
+
+ if (!data || !data.length)
+ return;
+
+ this.parseData();
+ this.setDimensions(props);
+ this.updateLegend();
+ this.generateXYGraph(props);
+ this.generateElements();
+ }
+
+ parseData() {
+ const {
+ data
+ } = this.props;
+
+ const {
+ linesColumn,
+ yColumn
+ } = this.getConfiguredProperties();
+
+ this.legendsData = [];
+ let updatedLinesLabel = [];
+ let finalYColumn = typeof yColumn === 'object' ? yColumn : [yColumn];
+
+ if(linesColumn)
+ updatedLinesLabel = typeof linesColumn === 'object' ? linesColumn : [linesColumn];
+
+ this.legendsData = finalYColumn.map((d, i) => {
+ return {
+ 'key' : d,
+ 'value' : updatedLinesLabel[i] ? updatedLinesLabel[i] : d
+ };
+ });
+
+ this.filterDatas = [];
+
+ data.forEach((d) => {
+ this.getLegendsData().forEach((ld) => {
+ if(d[ld['key']] !== null) {
+ this.filterDatas.push(Object.assign({
+ yColumn: d[ld['key']] !== null ? d[ld['key']] : 0,
+ columnType: ld['key']
+ }, d));
+ }
+ });
+ });
+ }
+
+ getFilterDatas() {
+ return this.filterDatas;
+ }
+
+ setDimensions(props) {
+ this.setYlabelWidth(this.getFilterDatas());
+ this.setAvailableWidth(props);
+ this.setAvailableHeight(props);
+ this.setLeftMargin();
+ this.setXBandScale(this.props.data);
+ this.setYBandScale(this.props.data)
+ }
+
+ updateLegend() {
+
+ const {
+ chartHeightToPixel,
+ chartWidthToPixel,
+ circleToPixel,
+ legend,
+ } = this.getConfiguredProperties();
+
+ const legendFn = (d) => d['value'];
+ let legendWidth = legend.show && this.getLegendsData().length >= 1 ? this.longestLabelLength(this.getLegendsData(), legendFn) * chartWidthToPixel : 0;
+
+ if (legend.show)
+ {
+ legend.width = legendWidth;
+
+ // Compute the available space considering a legend
+ if (this.checkIsVerticalLegend())
+ {
+ this.leftMargin += legend.width;
+ this.availableWidth -= legend.width;
+ }
+ else {
+ const nbElementsPerLine = parseInt(this.getAvailableWidth() / legend.width, 10);
+ const nbLines = parseInt(this.getLegendsData().length / nbElementsPerLine, 10);
+ this.availableHeight -= nbLines * legend.circleSize * circleToPixel + chartHeightToPixel;
+ }
+ }
+
+ this.legend = legend;
+ }
+
+ getLegend() {
+ return this.legend;
+ }
+
+ getLegendsData() {
+ return this.legendsData;
+ }
+
+ generateXYGraph(props) {
+
+ const {
+ data,
+ } = props;
+
+ this.setXScale(data);
+ this.setXaxis(data);
+ this.setYScale(this.getFilterDatas());
+ this.setYaxis();
+ this.setXTitlePositions();
+ this.setYTitlePositions();
+ }
+
+ generateElements() {
+
+ this.areas = [];
+ this.lines = [];
+ this.circle = [];
+ this.hoverCircle = [];
+
+ this.getLegendsData().map((d, i) =>
+ this.generateElement(this.getFilterDatas(), d, this.props.data.length === 1 ? true: false)
+ )
+ }
+
+ generateElement(filterData, data, isCircle = false) {
+
+ const {
+ circleRadius,
+ colors,
+ xColumn,
+ stroke
+ } = this.getConfiguredProperties();
+
+ const scale = this.scaleColor(this.getLegendsData(), 'key');
+
+ this.getColor = (d) => scale ? scale(d['key']) : stroke.color || colors[0];
+
+ if(isCircle) {
+ this.circle = filterData.map( (d) =>
+
+ )
+ return;
+ }
+
+ let xScale = this.getXScale();
+ let yScale = this.getYScale();
+ let availableHeight = this.getAvailableHeight();
+
+ this.hoverCircle.push(
+
+ )
+
+ var lineGenerator = line()
+ .x(function(d) { return xScale(d[xColumn]); })
+ .y(function(d) { return yScale(d[data['key']]); });
+
+ var areaGenerator = area()
+ .x(function(d, i) { return xScale(d[xColumn]); })
+ .y0(function(d) { return availableHeight; })
+ .y1(function(d) { return yScale(d[data['key']]); })
+
+
+ this.areas.push(
+ )
+
+ this.lines.push(
+ )
+ }
+
+ getLines() {
+ return this.lines ? this.lines : [];
+ }
+
+ getAreas() {
+ return this.areas ? this.areas : [];
+ }
+
+ getCircle() {
+ return this.circle ? this.circle : [];
+ }
+
+ getHoverCircle() {
+ return this.hoverCircle ? this.hoverCircle : [];
+ }
+
+ handleShowEvent() {
+ d3.select("#tooltip-line").style("opacity", 1);
+ }
+
+ handleHideEvent() {
+ this.hoveredDatum = null;
+ d3.select("#tooltip-line").style("opacity", 0);
+ }
+
+ renderTooltip() {
+ const {
+ xColumn,
+ } = this.getConfiguredProperties();
+
+ let xScale = this.getXScale();
+ let bandwidth = this.getXBandScale().bandwidth() * 0.8;
+
+ return this.getFilterDatas().map((d, i) =>
+ this.updateVerticalLine(d)}
+
+ >
+
+
+
+
+
+ )
+ }
+
+ updateVerticalLine(data) {
+ const {
+ xColumn,
+ } = this.getConfiguredProperties();
+
+ const yScale = this.getYScale();
+ const rightMargin = this.hoveredDatum ? this.getXScale()(data[xColumn]) : 0;
+
+ d3.select("#tooltip-line").attr("transform", "translate("+rightMargin+", 0)");
+
+ this.getLegendsData().forEach(function(d) {
+ d3.select(`#circle_${d['key']}`)
+ .attr("cy", yScale(data[d['key']]));
+ });
+
+ }
+
+ renderVerticalLine() {
+ return
+ }
+
+ render() {
+
+ const {
+ data,
+ width,
+ height
+ } = this.props;
+
+ if (!data || !data.length)
+ return;
+
+ const {
+ margin
+ } = this.getConfiguredProperties();
+
+ const label = (d) => d['value'];
+
+ return (
+
+ {
+ this.tooltip
+ }
+
+
+ );
+ }
+}
+AreaGraph.propTypes = {
+ configuration: React.PropTypes.object,
+ response: React.PropTypes.object
+};
+
+const actionCreators = (dispatch) => ({
+});
+
+export default connect(null, actionCreators)(AreaGraph);
diff --git a/src/components/Graphs/SankeyGraph/default.config.js b/src/components/Graphs/SankeyGraph/default.config.js
new file mode 100644
index 00000000..85b10607
--- /dev/null
+++ b/src/components/Graphs/SankeyGraph/default.config.js
@@ -0,0 +1,21 @@
+import { theme } from "../../../theme";
+
+export const properties = {
+ stroke: {
+ color: "red",
+ width: "2px",
+ opacity: "0.4"
+ },
+ colors: [
+ theme.palette.orangeLightColor,
+ theme.palette.blueLightColor,
+ theme.palette.pinkLightColor,
+ theme.palette.orangeLighterColor,
+ theme.palette.greenColor,
+ theme.palette.yellowLightColor,
+ theme.palette.yellowDarkColor,
+ ],
+ nodeWidth: 20,
+ nodePadding: 20,
+ layout : 30
+}
diff --git a/src/components/Graphs/SankeyGraph/index.js b/src/components/Graphs/SankeyGraph/index.js
new file mode 100644
index 00000000..7987fb82
--- /dev/null
+++ b/src/components/Graphs/SankeyGraph/index.js
@@ -0,0 +1,306 @@
+import React from "react";
+import AbstractGraph from "../AbstractGraph";
+import * as d3 from "d3";
+import "./sankey.js"
+import "./style.css";
+import {properties} from "./default.config";
+import { CardOverlay } from "../../CardOverlay";
+
+class SankeyGraph extends AbstractGraph {
+
+ constructor(props) {
+ super(props, properties);
+ this.sankey = d3.sankey();
+ this.filterDatas = {};
+ }
+
+ componentWillMount() {
+ this.initiate(this.props);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if(this.props !== nextProps) {
+ this.initiate(nextProps);
+ }
+ }
+
+ componentDidMount() {
+ this.createSankeyChart()
+ }
+
+ componentDidUpdate() {
+ this.createSankeyChart()
+ }
+
+ // get sankey chart object.
+ getSankey() {
+ return this.sankey;
+ }
+
+ //get formatted nodes data.
+ getNodes() {
+ return this.filterDatas.nodes;
+ }
+
+ // get formatted links data.
+ getLinks() {
+ return this.filterDatas.links;
+ }
+
+ // get parent node (svg).
+ getParentNode() {
+ return this.node;
+ }
+
+ // get formatted function to format value.
+ getFormat() {
+ const {
+ valueFormat
+ } = this.getConfiguredProperties();
+
+ return d => d3.format(valueFormat)(d);
+ }
+
+ // function to set initial data to render sankey chart
+ initiate(props) {
+ const {
+ data
+ } = props;
+
+ if (!data || !data.length)
+ return;
+
+
+ this.dataError = false;
+ if (this.checkData(data)) {
+ this.dataError = true;
+ } else {
+ this.parseData(props);
+ this.setProperties(props);
+ }
+ }
+
+ // manipulate data in sankey chart format
+ parseData(props) {
+ const {
+ data
+ } = props;
+
+ const {
+ sourceColumn,
+ targetColumn,
+ valueColumn
+ } = this.getConfiguredProperties();
+
+ let nodes = [], links = [];
+
+ data.forEach((d) => {
+ nodes.push(...[d[sourceColumn], d[targetColumn]]);
+ links.push({
+ "source": d[sourceColumn],
+ "target": d[targetColumn],
+ "value": +d[valueColumn] }
+ );
+ });
+
+ nodes = nodes.filter( (item, i, ar) => ar.indexOf(item) === i );
+
+ // loop through each link replacing the text with its index from node
+ links.forEach((d, i) => {
+ links[i][sourceColumn] = nodes.indexOf(links[i][sourceColumn]);
+ links[i][targetColumn] = nodes.indexOf(links[i][targetColumn]);
+ });
+
+ nodes = nodes.map( (d) => {
+ return { name: d };
+ });
+
+ this.filterDatas = {"nodes" : nodes, "links" : links};
+ }
+
+ // set the data and properties of the sankey chart.
+ setProperties(props) {
+ const {
+ width,
+ height
+ } = props;
+
+ const {
+ margin
+ } = this.getConfiguredProperties();
+
+ const
+ availableWidth = width - margin.left - margin.right,
+ availableHeight = height - margin.top - margin.bottom,
+ nodeWidth = (availableWidth/ this.getNodes().length) * 0.1;
+
+ // Set the sankey diagram properties.
+ let sankey = this.getSankey();
+
+ sankey
+ .nodeWidth(nodeWidth)
+ .nodePadding(properties.nodePadding)
+ .size([availableWidth, availableHeight]);
+
+ sankey
+ .nodes(this.getNodes())
+ .links(this.getLinks())
+ .layout(properties.layout);
+ }
+
+ // Create sankey chart.
+ createSankeyChart() {
+ if(this.dataError)
+ return;
+
+ // append the g object to the svg object of the page
+ var node = d3.select(this.getParentNode());
+
+ // Create links for chart
+ this.generateLinks(node);
+
+ // Create path for chart
+ this.generateNodes(node);
+ }
+
+ // Add links in chart.
+ generateLinks(parentLink) {
+ const {
+ valueColumnLabel
+ } = this.getConfiguredProperties();
+
+ // add in the links
+ const links = parentLink.select(".linkContainer").selectAll(".link")
+ .data(this.getLinks(), d => d.source.name + "-" + d.target.name );
+
+ const newLinks = links.enter().append("path")
+ .attr("class", "link");
+
+ newLinks.append("title");
+
+ const allLinks = newLinks.merge(links);
+
+ allLinks
+ .style("stroke-width", d => Math.max(1, d.dy) )
+ .sort( (a, b) => b.dy - a.dy )
+ .attr("d", this.getSankey().link())
+
+ // add the link titles
+ allLinks.select("title")
+ .text((d) =>
+ `${d.source.name} -> ${d.target.name} \n${valueColumnLabel} : ${this.getFormat()(d.value)}`
+ )
+
+ // Remove all remaining links
+ links.exit().remove();
+ }
+
+ // Add paths in chart.
+ generateNodes(parentNode) {
+ const {
+ width
+ } = this.props;
+
+ const {
+ valueColumnLabel
+ } = this.getConfiguredProperties();
+
+ const color = d3.scaleOrdinal(properties.colors);
+
+ // add in the nodes
+ var nodes = parentNode.select(".nodeContainer").selectAll(".node")
+ .data(this.getNodes(), d => d.name);
+
+ const newNodes = nodes.enter().append("g")
+ .attr("class", "node");
+
+ newNodes.append("rect")
+ .style("fill", d => d.color = color(d.name.replace(/ .*/, "")))
+ .style("stroke", d => d3.rgb(d.color).darker(2) )
+ .append("title");
+
+ newNodes.append("text")
+ .attr("fill", "#423838");
+
+ const allNodes = newNodes.merge(nodes);
+
+ allNodes.attr("transform", d => `translate(${d.x}, ${d.y})` );
+
+ // add the rectangles for the nodes
+ allNodes.select("rect")
+ .attr("height", d => d.dy )
+ .attr("width", this.getSankey().nodeWidth())
+ .select("title")
+ .text( d =>
+ `${d.name} \n${valueColumnLabel} : ${this.getFormat()(d.value)}` );
+
+ // add in the title for the nodes
+ allNodes.select("text")
+ .attr("x", "-6")
+ .attr("y", d => d.dy / 2 )
+ .text( d => d.name )
+ .attr("text-anchor", "end")
+ .filter( d => d.x < width / 2 )
+ .attr("x", 6 + this.getSankey().nodeWidth())
+ .attr("text-anchor", "start");
+
+ // Remove all remaining nodes
+ nodes.exit().remove();
+ }
+
+ checkData(data) {
+ const {
+ sourceColumn,
+ targetColumn,
+ } = this.getConfiguredProperties();
+
+ let result = data.filter( (item, i, ar) =>
+ ar.find( d => d[sourceColumn] === item[targetColumn] && d[targetColumn] === item[sourceColumn])
+ );
+ return result.length ? true : false;
+ }
+
+ render() {
+ const {
+ data,
+ width,
+ height
+ } = this.props;
+
+ const {
+ margin
+ } = this.getConfiguredProperties();
+
+ if (!data || !data.length)
+ return;
+
+ if(this.dataError)
+
+ return (
+
+ );
+
+ return (
+
+
+
+ );
+ }
+}
+
+SankeyGraph.propTypes = {
+ configuration: React.PropTypes.object
+};
+
+export default SankeyGraph;
\ No newline at end of file
diff --git a/src/components/Graphs/SankeyGraph/sankey.js b/src/components/Graphs/SankeyGraph/sankey.js
new file mode 100644
index 00000000..b8c55138
--- /dev/null
+++ b/src/components/Graphs/SankeyGraph/sankey.js
@@ -0,0 +1,296 @@
+import * as d3 from "d3";
+
+d3.sankey = function() {
+ var sankey = {},
+ nodeWidth = 24,
+ nodePadding = 8,
+ size = [1, 1],
+ nodes = [],
+ links = [];
+
+ sankey.nodeWidth = function(_) {
+ if (!arguments.length) return nodeWidth;
+ nodeWidth = +_;
+ return sankey;
+ };
+
+ sankey.nodePadding = function(_) {
+ if (!arguments.length) return nodePadding;
+ nodePadding = +_;
+ return sankey;
+ };
+
+ sankey.nodes = function(_) {
+ if (!arguments.length) return nodes;
+ nodes = _;
+ return sankey;
+ };
+
+ sankey.links = function(_) {
+ if (!arguments.length) return links;
+ links = _;
+ return sankey;
+ };
+
+ sankey.size = function(_) {
+ if (!arguments.length) return size;
+ size = _;
+ return sankey;
+ };
+
+ sankey.layout = function(iterations) {
+ computeNodeLinks();
+ computeNodeValues();
+ computeNodeBreadths();
+ computeNodeDepths(iterations);
+ computeLinkDepths();
+ return sankey;
+ };
+
+ sankey.relayout = function() {
+ computeLinkDepths();
+ return sankey;
+ };
+
+ sankey.link = function() {
+ var curvature = .5;
+
+ function link(d) {
+ var x0 = d.source.x + d.source.dx,
+ x1 = d.target.x,
+ xi = d3.interpolateNumber(x0, x1),
+ x2 = xi(curvature),
+ x3 = xi(1 - curvature),
+ y0 = d.source.y + d.sy + d.dy / 2,
+ y1 = d.target.y + d.ty + d.dy / 2;
+ return "M" + x0 + "," + y0
+ + "C" + x2 + "," + y0
+ + " " + x3 + "," + y1
+ + " " + x1 + "," + y1;
+ }
+
+ link.curvature = function(_) {
+ if (!arguments.length) return curvature;
+ curvature = +_;
+ return link;
+ };
+
+ return link;
+ };
+
+ // Populate the sourceLinks and targetLinks for each node.
+ // Also, if the source and target are not objects, assume they are indices.
+ function computeNodeLinks() {
+ nodes.forEach(function(node) {
+ node.sourceLinks = [];
+ node.targetLinks = [];
+ });
+ links.forEach(function(link) {
+ var source = link.source,
+ target = link.target;
+ if (typeof source === "number") source = link.source = nodes[link.source];
+ if (typeof target === "number") target = link.target = nodes[link.target];
+ source.sourceLinks.push(link);
+ target.targetLinks.push(link);
+ });
+ }
+
+ // Compute the value (size) of each node by summing the associated links.
+ function computeNodeValues() {
+ nodes.forEach(function(node) {
+ node.value = Math.max(
+ d3.sum(node.sourceLinks, value),
+ d3.sum(node.targetLinks, value)
+ );
+ });
+ }
+
+ // Iteratively assign the breadth (x-position) for each node.
+ // Nodes are assigned the maximum breadth of incoming neighbors plus one;
+ // nodes with no incoming links are assigned breadth zero, while
+ // nodes with no outgoing links are assigned the maximum breadth.
+ function computeNodeBreadths() {
+ var remainingNodes = nodes,
+ nextNodes,
+ x = 0;
+
+ while (remainingNodes.length) {
+ nextNodes = [];
+ remainingNodes.forEach(function(node) {
+ node.x = x;
+ node.dx = nodeWidth;
+ node.sourceLinks.forEach(function(link) {
+ if (nextNodes.indexOf(link.target) < 0) {
+ nextNodes.push(link.target);
+ }
+ });
+ });
+ remainingNodes = nextNodes;
+ ++x;
+ }
+
+ //
+ moveSinksRight(x);
+ scaleNodeBreadths((size[0] - nodeWidth) / (x - 1));
+ }
+
+ function moveSourcesRight() {
+ nodes.forEach(function(node) {
+ if (!node.targetLinks.length) {
+ node.x = d3.min(node.sourceLinks, function(d) { return d.target.x; }) - 1;
+ }
+ });
+ }
+
+ function moveSinksRight(x) {
+ nodes.forEach(function(node) {
+ if (!node.sourceLinks.length) {
+ node.x = x - 1;
+ }
+ });
+ }
+
+ function scaleNodeBreadths(kx) {
+ nodes.forEach(function(node) {
+ node.x *= kx;
+ });
+ }
+
+ function computeNodeDepths(iterations) {
+ var nodesByBreadth = d3.nest()
+ .key(function(d) { return d.x; })
+ .sortKeys(d3.ascending)
+ .entries(nodes)
+ .map(function(d) { return d.values; });
+
+ //
+ initializeNodeDepth();
+ resolveCollisions();
+ for (var alpha = 1; iterations > 0; --iterations) {
+ relaxRightToLeft(alpha *= .99);
+ resolveCollisions();
+ relaxLeftToRight(alpha);
+ resolveCollisions();
+ }
+
+ function initializeNodeDepth() {
+ var ky = d3.min(nodesByBreadth, function(nodes) {
+ return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value);
+ });
+
+ nodesByBreadth.forEach(function(nodes) {
+ nodes.forEach(function(node, i) {
+ node.y = i;
+ node.dy = node.value * ky;
+ });
+ });
+
+ links.forEach(function(link) {
+ link.dy = link.value * ky;
+ });
+ }
+
+ function relaxLeftToRight(alpha) {
+ nodesByBreadth.forEach(function(nodes, breadth) {
+ nodes.forEach(function(node) {
+ if (node.targetLinks.length) {
+ var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value);
+ node.y += (y - center(node)) * alpha;
+ }
+ });
+ });
+
+ function weightedSource(link) {
+ return center(link.source) * link.value;
+ }
+ }
+
+ function relaxRightToLeft(alpha) {
+ nodesByBreadth.slice().reverse().forEach(function(nodes) {
+ nodes.forEach(function(node) {
+ if (node.sourceLinks.length) {
+ var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
+ node.y += (y - center(node)) * alpha;
+ }
+ });
+ });
+
+ function weightedTarget(link) {
+ return center(link.target) * link.value;
+ }
+ }
+
+ function resolveCollisions() {
+ nodesByBreadth.forEach(function(nodes) {
+ var node,
+ dy,
+ y0 = 0,
+ n = nodes.length,
+ i;
+
+ // Push any overlapping nodes down.
+ nodes.sort(ascendingDepth);
+ for (i = 0; i < n; ++i) {
+ node = nodes[i];
+ dy = y0 - node.y;
+ if (dy > 0) node.y += dy;
+ y0 = node.y + node.dy + nodePadding;
+ }
+
+ // If the bottommost node goes outside the bounds, push it back up.
+ dy = y0 - nodePadding - size[1];
+ if (dy > 0) {
+ y0 = node.y -= dy;
+
+ // Push any overlapping nodes back up.
+ for (i = n - 2; i >= 0; --i) {
+ node = nodes[i];
+ dy = node.y + node.dy + nodePadding - y0;
+ if (dy > 0) node.y -= dy;
+ y0 = node.y;
+ }
+ }
+ });
+ }
+
+ function ascendingDepth(a, b) {
+ return a.y - b.y;
+ }
+ }
+
+ function computeLinkDepths() {
+ nodes.forEach(function(node) {
+ node.sourceLinks.sort(ascendingTargetDepth);
+ node.targetLinks.sort(ascendingSourceDepth);
+ });
+ nodes.forEach(function(node) {
+ var sy = 0, ty = 0;
+ node.sourceLinks.forEach(function(link) {
+ link.sy = sy;
+ sy += link.dy;
+ });
+ node.targetLinks.forEach(function(link) {
+ link.ty = ty;
+ ty += link.dy;
+ });
+ });
+
+ function ascendingSourceDepth(a, b) {
+ return a.source.y - b.source.y;
+ }
+
+ function ascendingTargetDepth(a, b) {
+ return a.target.y - b.target.y;
+ }
+ }
+
+ function center(node) {
+ return node.y + node.dy / 2;
+ }
+
+ function value(link) {
+ return link.value;
+ }
+
+ return sankey;
+};
\ No newline at end of file
diff --git a/src/components/Graphs/SankeyGraph/style.css b/src/components/Graphs/SankeyGraph/style.css
new file mode 100644
index 00000000..205e34f0
--- /dev/null
+++ b/src/components/Graphs/SankeyGraph/style.css
@@ -0,0 +1,20 @@
+.node rect {
+ cursor: move;
+ fill-opacity: .9;
+ shape-rendering: crispEdges;
+}
+
+.node text {
+ pointer-events: none;
+ text-shadow: 0 1px 0 #fff;
+}
+
+.link {
+ fill: none;
+ stroke: #CAC6C6;
+ stroke-opacity: .4;
+}
+
+.link:hover {
+ stroke-opacity: .5;
+}
\ No newline at end of file
diff --git a/src/components/Graphs/XYGraph.js b/src/components/Graphs/XYGraph.js
index 9e3595dc..983ecc2f 100644
--- a/src/components/Graphs/XYGraph.js
+++ b/src/components/Graphs/XYGraph.js
@@ -1,6 +1,17 @@
import React from "react";
import AbstractGraph from "./AbstractGraph";
+import {
+ format,
+ scaleTime,
+ extent,
+ scaleLinear,
+ axisBottom,
+ axisLeft,
+ scaleBand,
+ map
+} from "d3";
+
export default class XYGraph extends AbstractGraph {
constructor(props, properties = {}) {
@@ -15,6 +26,181 @@ export default class XYGraph extends AbstractGraph {
}
+ setYBandScale(data) {
+
+ const yLabelFn = (d) => d['yColumn'];
+
+ const distYDatas = map(data, yLabelFn).keys().sort();
+
+ this.yBandScale = scaleBand()
+ .domain(distYDatas);
+ this.yBandScale.rangeRound([0, this.getAvailableHeight()]);
+ }
+
+ getYBandScale() {
+ return this.yBandScale;
+ }
+
+ setXBandScale(data) {
+ const {
+ xColumn
+ } = this.getConfiguredProperties();
+
+ const xLabelFn = (d) => d[xColumn];
+
+ const distXDatas = map(data, xLabelFn).keys().sort();
+
+ this.xBandScale = scaleBand()
+ .domain(distXDatas);
+ this.xBandScale.rangeRound([0, this.getAvailableWidth()]);
+ }
+
+ getXBandScale() {
+ //return min([this.xBandScale.bandwidth(), this.yBandScale.bandwidth()]);
+ return this.xBandScale;
+ }
+
+ setXScale(data) {
+
+ if (!data || !data.length)
+ return;
+
+ const {
+ dateHistogram,
+ xColumn
+ } = this.getConfiguredProperties();
+
+ const xLabelFn = (d) => d[xColumn];
+
+ this.xScale = null;
+
+ if (dateHistogram) {
+ this.xScale = scaleTime()
+ .domain(extent(data, xLabelFn));
+ } else {
+ this.xScale = scaleLinear()
+ .domain(extent(data, xLabelFn));
+ }
+
+ this.xScale.range([0, this.getAvailableWidth()]);
+ }
+
+ getXScale() {
+ return this.xScale;
+ }
+
+ setXaxis(data) {
+
+ if (!data || !data.length)
+ return;
+
+ const {
+ xTickSizeInner,
+ xTickSizeOuter,
+ xTickFormat,
+ xTickGrid,
+ xTicks
+ } = this.getConfiguredProperties();
+
+ this.xAxis = axisBottom(this.getXScale())
+ .tickSizeInner(xTickGrid ? -this.getAvailableHeight() : xTickSizeInner)
+ .tickSizeOuter(xTickSizeOuter);
+
+ if(xTickFormat){
+ this.xAxis.tickFormat(format(xTickFormat));
+ }
+
+ if(xTicks){
+ this.xAxis.ticks(xTicks);
+ }
+ }
+
+ getXAxis() {
+ return this.xAxis;
+ }
+
+ setYScale(data) {
+
+ const {
+ zeroStart,
+ } = this.getConfiguredProperties();
+
+ const yLabelFn = (d) => d['yColumn'];
+
+ let yExtent = this.updateYExtent(extent(data, yLabelFn), zeroStart);
+
+ this.yScale = scaleLinear()
+ .domain(yExtent);
+
+ this.yScale.range([this.getAvailableHeight(), 0]);
+ }
+
+ getYScale() {
+ return this.yScale;
+ }
+
+ setYaxis() {
+
+ const {
+ yTickFormat,
+ yTickGrid,
+ yTicks,
+ yTickSizeInner,
+ yTickSizeOuter,
+ } = this.getConfiguredProperties();
+
+ this.yAxis = axisLeft(this.getYScale())
+ .tickSizeInner(yTickGrid ? -this.getAvailableWidth() : yTickSizeInner)
+ .tickSizeOuter(yTickSizeOuter);
+
+ if(yTickFormat){
+ this.yAxis.tickFormat(format(yTickFormat));
+ }
+
+ if(yTicks){
+ this.yAxis.ticks(yTicks);
+ }
+ }
+
+ getYAxis() {
+ return this.yAxis;
+ }
+
+ setXTitlePositions() {
+
+ const {
+ chartHeightToPixel,
+ margin,
+ } = this.getConfiguredProperties();
+
+ this.xTitlePosition = {
+ left: this.getLeftMargin() + this.getAvailableWidth() / 2,
+ top: margin.top + this.getAvailableHeight() + chartHeightToPixel + this.getXAxisHeight()
+ }
+ }
+
+ getXTitlePositions() {
+ return this.xTitlePosition;
+ }
+
+ setYTitlePositions() {
+
+ const {
+ chartWidthToPixel,
+ margin,
+ } = this.getConfiguredProperties();
+
+ this.yTitlePosition = {
+ // We use chartWidthToPixel to compensate the rotation of the title
+ left: margin.left + chartWidthToPixel + (this.checkIsVerticalLegend() ? this.getLegend().width : 0),
+ top: margin.top + this.getAvailableHeight() / 2
+ }
+ }
+
+ getYTitlePositions() {
+ return this.yTitlePosition;
+ }
+
axisTitles(xLabelPosition, yLabelPosition) {
const {
diff --git a/src/components/Graphs/index.js b/src/components/Graphs/index.js
index 87872f33..95ffce6e 100644
--- a/src/components/Graphs/index.js
+++ b/src/components/Graphs/index.js
@@ -9,6 +9,8 @@ import Table from "./Table";
import ChordGraph from "./ChordGraph";
import GaugeGraph from "./GaugeGraph";
import HeatmapGraph from "./HeatmapGraph";
+import AreaGraph from "./AreaGraph";
+import SankeyGraph from "./SankeyGraph";
import { theme } from "../../theme";
@@ -25,7 +27,9 @@ let registry = {
ChordGraph,
GaugeGraph,
VariationTextGraph,
- HeatmapGraph
+ HeatmapGraph,
+ AreaGraph,
+ SankeyGraph
};
/*