I'm working on a Shiny dashboard where I would like to display a barplot corresponding to a selected dataset a user can choose. While I can create the barplot and switch between the inputs, I have a problem with the switch of the graphs:
Switching between my datasets causes overlapping of the bars in the plot (the columns are added to the previous plot).
I thought about trying it with ".transition()" to somehow make a swift between the datasets but unfortunately, I do not get it to work. And I'm not sure if it makes any sense at all. I also tried to use ".remove" but it would always remove a bar with each switch. ".exit().remove()" did not work for me.
Has anybody an idea how I could prevent the overlapping?
My data:
Histogram.js (I did comment out my approaches, mentioned above)
// !preview r2d3 data=readr::read_tsv("Beispiel.tsv"), d3_version = "5", container = "div"
//r2d3: https://rstudio.github.io/r2d3, d3_version=3
// d3.tip
// Returns a tip
/* !preview r2d3 data=readr::read_tsv("Beispiel.tsv"), d3_version = "3", container = "div"*/
/* !preview r2d3 data=readr::read_tsv("Histogram_Data_Barplot.tsv"), d3_version = "3", container = "div" */
d3.tip = function() {
var direction = d3TipDirection,
offset = d3TipOffset,
html = d3TipHTML,
rootElement = document.body,
node = initNode(),
svg = null,
point = null,
target = null;
function tip(vis) {
svg = getSVGNode(vis);
if (!svg) return;
point = svg.createSVGPoint();
rootElement.appendChild(node);
}
// Public - show the tooltip on the screen
//
// Returns a tip
tip.show = function() {
var args = Array.prototype.slice.call(arguments);
if (args[args.length - 1] instanceof SVGElement) target = args.pop();
var content = html.apply(this, args),
poffset = offset.apply(this, args),
dir = direction.apply(this, args),
nodel = getNodeEl(),
i = directions.length,
coords,
scrollTop = document.documentElement.scrollTop ||
rootElement.scrollTop,
scrollLeft = document.documentElement.scrollLeft ||
rootElement.scrollLeft;
nodel.html(content)
.style('opacity', 1).style('pointer-events', 'all');
while (i--) nodel.classed(directions[i], false);
coords = directionCallbacks.get(dir).apply(this);
nodel.classed(dir, true)
.style('top', (coords.top + poffset[0]) + scrollTop + 'px')
.style('left', (coords.left + poffset[1]) + scrollLeft + 'px');
return tip;
};
// Public - hide the tooltip
//
// Returns a tip
tip.hide = function() {
var nodel = getNodeEl();
nodel.style('opacity', 0).style('pointer-events', 'none');
return tip;
};
// Public: Proxy attr calls to the d3 tip container.
// Sets or gets attribute value.
//
// n - name of the attribute
// v - value of the attribute
//
// Returns tip or attribute value
// eslint-disable-next-line no-unused-vars
tip.attr = function(n, v) {
if (arguments.length < 2 && typeof n === 'string') {
return getNodeEl().attr(n);
}
var args = Array.prototype.slice.call(arguments);
d3.selection.prototype.attr.apply(getNodeEl(), args);
return tip;
};
// Public: Proxy style calls to the d3 tip container.
// Sets or gets a style value.
//
// n - name of the property
// v - value of the property
//
// Returns tip or style property value
// eslint-disable-next-line no-unused-vars
tip.style = function(n, v) {
if (arguments.length < 2 && typeof n === 'string') {
return getNodeEl().style(n);
}
var args = Array.prototype.slice.call(arguments);
selection.prototype.style.apply(getNodeEl(), args);
return tip;
};
// Public: Set or get the direction of the tooltip
//
// v - One of n(north), s(south), e(east), or w(west), nw(northwest),
// sw(southwest), ne(northeast) or se(southeast)
//
// Returns tip or direction
tip.direction = function(v) {
if (!arguments.length) return direction;
direction = v == null ? v : functor(v)
return tip;
};
// Public: Sets or gets the offset of the tip
//
// v - Array of [x, y] offset
//
// Returns offset or
tip.offset = function(v) {
if (!arguments.length) return offset;
offset = v == null ? v : functor(v)
return tip;
};
// Public: sets or gets the html value of the tooltip
//
// v - String value of the tip
//
// Returns html value or tip
tip.html = function(v) {
if (!arguments.length) return html;
html = v == null ? v : functor(v)
return tip;
};
// Public: sets or gets the root element anchor of the tooltip
//
// v - root element of the tooltip
//
// Returns root node of tip
tip.rootElement = function(v) {
if (!arguments.length) return rootElement;
rootElement = v == null ? v : functor(v)
return tip;
};
// Public: destroys the tooltip and removes it from the DOM
//
// Returns a tip
tip.destroy = function() {
if (node) {
getNodeEl().remove()
node = null;
}
return tip;
};
function d3TipDirection() { return 'n' }
function d3TipOffset() { return [0, 0] }
function d3TipHTML() { return ' ' }
var directionCallbacks = d3.map({
n: directionNorth,
s: directionSouth,
e: directionEast,
w: directionWest,
nw: directionNorthWest,
ne: directionNorthEast,
sw: directionSouthWest,
se: directionSouthEast
}),
directions = directionCallbacks.keys();
function directionNorth() {
var bbox = getScreenBBox(this);
return {
top: bbox.n.y - node.offsetHeight,
left: bbox.n.x - node.offsetWidth / 2
};
}
function directionSouth() {
var bbox = getScreenBBox(this);
return {
top: bbox.s.y,
left: bbox.s.x - node.offsetWidth / 2
};
}
function directionEast() {
var bbox = getScreenBBox(this);
return {
top: bbox.e.y - node.offsetHeight / 2,
left: bbox.e.x
};
}
function directionWest() {
var bbox = getScreenBBox(this);
return {
top: bbox.w.y - node.offsetHeight / 2,
left: bbox.w.x - node.offsetWidth
};
}
function directionNorthWest() {
var bbox = getScreenBBox(this);
return {
top: bbox.nw.y - node.offsetHeight,
left: bbox.nw.x - node.offsetWidth
}
}
function directionNorthEast() {
var bbox = getScreenBBox(this)
return {
top: bbox.ne.y - node.offsetHeight,
left: bbox.ne.x
}
}
function directionSouthWest() {
var bbox = getScreenBBox(this)
return {
top: bbox.sw.y,
left: bbox.sw.x - node.offsetWidth
}
}
function directionSouthEast() {
var bbox = getScreenBBox(this)
return {
top: bbox.se.y,
left: bbox.se.x
}
}
function initNode() {
var div = d3.select(document.createElement('div'))
div
.style('position', 'absolute')
.style('top', 0)
.style('opacity', 0)
.style('pointer-events', 'none')
.style('box-sizing', 'border-box')
return div.node()
}
function getSVGNode(element) {
var svgNode = element.node()
if (!svgNode) return null
if (svgNode.tagName.toLowerCase() === 'svg') return svgNode
return svgNode.ownerSVGElement
}
function getNodeEl() {
if (node == null) {
node = initNode()
// re-add node to DOM
rootElement.appendChild(node)
}
return d3.select(node)
}
// Private - gets the screen coordinates of a shape
//
// Given a shape on the screen, will return an SVGPoint for the directions
// n(north), s(south), e(east), w(west), ne(northeast), se(southeast),
// nw(northwest), sw(southwest).
//
// +-+-+
// | |
// + +
// | |
// +-+-+
//
// Returns an Object {n, s, e, w, nw, sw, ne, se}
function getScreenBBox(targetShape) {
var targetel = target || targetShape
while (targetel.getScreenCTM == null && targetel.parentNode != null) {
targetel = targetel.parentNode
}
var bbox = {},
matrix = targetel.getScreenCTM(),
tbbox = targetel.getBBox(),
width = tbbox.width,
height = tbbox.height,
x = tbbox.x,
y = tbbox.y
point.x = x
point.y = y
bbox.nw = point.matrixTransform(matrix)
point.x += width
bbox.ne = point.matrixTransform(matrix)
point.y += height
bbox.se = point.matrixTransform(matrix)
point.x -= width
bbox.sw = point.matrixTransform(matrix)
point.y -= height / 2
bbox.w = point.matrixTransform(matrix)
point.x += width
bbox.e = point.matrixTransform(matrix)
point.x -= width / 2
point.y -= height / 2
bbox.n = point.matrixTransform(matrix)
point.y += height
bbox.s = point.matrixTransform(matrix)
return bbox
}
// Private - replace D3JS 3.X d3.functor() function
function functor(v) {
return typeof v === 'function' ? v : function() {
return v
}
}
return tip
}
//d3----------------------------------
var margin = {top: 40, right: 20, bottom: 30, left: 40},
/// width and height will be generated by R automatically. Removed parts: Numbers after width = xxx and height = xxx.
width = width - margin.left - margin.right,
height = height - margin.top - margin.bottom;
var formatPercent = d3.format("");
var x = d3.scaleBand()
.rangeRound([0, width])
.padding(.1);
var y = d3.scaleLinear()
.rangeRound([height, 0]);
var xAxis = d3.axisBottom()
.scale(x);
var yAxis = d3.axisLeft()
.scale(y)
// .tickFormat(formatPercent);
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "<strong>SubCluster:</strong> <span style='color:red'>" + d.SubCluster + "</span>";
})
//R already created a SVG element. So we do not need to create it again. Removing the following part:
//d3.select("body")
var svg = div.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.call(tip);
// To make the data input dynamically, we would do this: Create .tsv file (only in this example)
// Then change the header to read this specific .tsv file
// r2d3.onRender = To read the data into r2d3.
r2d3.onRender(function(data, s, w, h, options) {
x.domain(data.map(function(d) { return d.DimensionName; }));
y.domain([0, d3.max(data, function(d) { return d.Amount;})]);
//d3.count(data, d => d.amount);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Occurence");
svg.selectAll(".bar")
.data(data)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.DimensionName); })
.attr("width", x.bandwidth())
.attr("y", function(d) { return y(d.Amount); })
.attr("height", function(d) { return height - y(d.Amount); })
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
// .remove()
;
/* svg.select(".bar")
.transition()
.duration(500)
.attr("x", function(d) { return x(d.DimensionName); })
.attr("width", function(d) { return y(d.Amount); })
.attr("y")
.remove(); */
// svg.exit().remove();
/* svg.transition()
.data(data)
.duration(500)
.attr("class", "bar")
.attr("x", function(d) { return x(d.DimensionName); })
.attr("width", x.bandwidth())
.attr("y", function(d) { return y(d.Amount); })
.attr("height", function(d) { return height - y(d.Amount); })
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
svg.exit().remove() */
});
function type(d) {
d.Amount = +d.Amount;
return d;
}
.R File (I'll only include how I call the plot in the UI and in the server, because the code is quite lengthy:
if (interactive()) {
ui <- [Code about navigation bar etc.]
body = argonDashBody(
argonTabItems(
argonTabItem(
tabName = "subcluster_analysis",
fluidRow(
column(width=4,
#FM
argonTable(
cardWrap = FALSE,
#title = "Barplots",
headTitles = "D3JS Barplot - Cluster Per Feature",
shinycssloaders::withSpinner(
# verbatimTextOutput("summary"),
d3Output(outputId = "out_barplotD3_clusterPerFeature_NEU")),
)
),
[...rest of the body...]
#### Server initiation ---------------------------------
server <- function(input, output, session){
[Server output]
#Barplot in D3.Js
output$out_barplotD3_clusterPerFeature_NEU <- renderD3({
r2d3(data = cleanedSubClusteringData_barplot(),
script= "Histogram.js",
d3_version = "4",
container = "div")
})
}
shinyApp(ui,server)
}
Data:
https://drive.google.com/drive/folders/1BXQPgEBYax0mwB8eTA5Uf-PuYs27C_kD?usp=sharing
I'm grateful for any advice! :)
Related
I am trying to replicate the visualization I see on this github profile
https://github.com/mojoaxel/d3-sunburst/tree/master/examples
I copied the following code and changed the path to the respective files.
suburst.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sequences sunburst</title>
<link rel="stylesheet" type="text/css" href="sunburst.css"/>
<link rel="stylesheet" type="text/css" href="examples.css"/>
<script src="http://d3js.org/d3.v3.min.js" type="text/javascript"></script>
<script src="sunburst.js" type="text/javascript"></script>
</head>
<body>
<div id="main">
<div id="sunburst-breadcrumbs"></div>
<div id="sunburst-chart">
<div id="sunburst-description"></div>
</div>
</div>
<div id="sidebar">
<input type="checkbox" id="togglelegend"> Legend<br/>
<div id="sunburst-legend" style="visibility: hidden;"></div>
</div>
<script type="text/javascript">
(function() {
var sunburst = new Sunburst({
colors: {
"home": "#5687d1",
"product": "#7b615c",
"search": "#de783b",
"account": "#6ab975",
"other": "#a173d1",
"end": "#bbbbbb"
}
});
sunburst.loadCsv("/Users/Documents/data/visit-sequences.csv");
d3.select("#togglelegend").on("click", function() {
var legend = d3.select('#sunburst-legend');
if (legend.style("visibility") == "hidden") {
legend.style("visibility", "");
} else {
legend.style("visibility", "hidden");
}
});
})();
</script>
</body>
</html>
sunburst.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['d3'], factory);
} else {
root.Sunburst = factory(root.d3);
}
}(this, function (d3) {
var defaultOptions = {
// DOM Selectors
selectors: {
breadcrumbs: '#sunburst-breadcrumbs',
chart: '#sunburst-chart',
description: '#sunburst-description',
legend: '#sunburst-legend'
},
// Dimensions of sunburst.
width: 750,
height: 600,
// Mapping of step names to colors.
colors: {},
// If a color-name is missing this color-scale is used
colorScale: d3.scale.category20(),
colorScaleLength: 20,
// Breadcrumb dimensions: width, height, spacing, width of tip/tail.
breadcrumbs: {
w: 75,
h: 30,
s: 3,
t: 10
},
// parser settings
separator: '-'
};
/**
* This hashing function returns a number between 0 and 4294967295 (inclusive) from the given string.
* #see https://github.com/darkskyapp/string-hash
* #param {String} str
*/
function hash(str) {
var hash = 5381;
var i = str.length;
while(i) {
hash = (hash * 33) ^ str.charCodeAt(--i);
}
return hash >>> 0;
}
var Sunburst = function(options, data) {
this.opt = Object.assign({}, defaultOptions, options);
// Total size of all segments; we set this later, after loading the data.
this.totalSize = 0;
if (data) {
this.setData(data);
}
}
Sunburst.prototype.getColorByName = function(name) {
return this.opt.colors[name] || this.opt.colorScale(hash(name) % this.opt.colorScaleLength);
}
Sunburst.prototype.setData = function(data) {
var json = this.buildHierarchy(data);
this.createVisualization(json);
}
Sunburst.prototype.loadCsv = function(csvFile) {
// Use d3.text and d3.csv.parseRows so that we do not need to have a header
// row, and can receive the csv as an array of arrays.
d3.text(csvFile, function(text) {
var array = d3.csv.parseRows(text);
var json = this.buildHierarchy(array);
this.createVisualization(json);
}.bind(this));
}
// Main function to draw and set up the visualization, once we have the data.
Sunburst.prototype.createVisualization = function(json) {
var that = this;
var radius = Math.min(this.opt.width, this.opt.height) / 2
this.vis = d3.select(this.opt.selectors.chart).append("svg:svg")
.attr("width", this.opt.width)
.attr("height", this.opt.height)
.append("svg:g")
.attr("id", "sunburst-container")
.attr("transform", "translate(" + this.opt.width / 2 + "," + this.opt.height / 2 + ")");
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
var partition = d3.layout.partition()
.size([2 * Math.PI, radius * radius])
.value(function(d) { return d.size; });
// Basic setup of page elements.
this.initializeBreadcrumbTrail();
this.drawLegend();
// For efficiency, filter nodes to keep only those large enough to see.
var nodes = partition.nodes(json)
.filter(function(d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
});
var all = this.vis.data([json])
.selectAll("path")
.data(nodes)
.enter();
all.append("svg:path")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d) { return that.getColorByName(d.name); })
.style("opacity", 1)
.on("mouseover", that.mouseover.bind(this));
// some tests with text
/*
var arcText = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y * 0.4); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy * 0.4); })
var arcsText = arcs.append("svg:path")
.attr("d", arcText)
.style("fill", "none")
.attr("id", function(d, i){
return "s" + i;
});
var texts = all.append("svg:text")
.attr("dx", "0")
.attr("dy", "0")
.style("text-anchor","middle")
.append("textPath")
.attr("xlink:href", function(d, i){
return "#s" + i;
})
.attr("startOffset",function(d,i){return "25%";})
.text(function (d) {
return d.depth === 1 ? d.name : '';
});
*/
// Add the mouseleave handler to the bounding circle.
d3.select(this.opt.selectors.chart).on("mouseleave", that.mouseleave.bind(this));
// Get total size of the tree = value of root node from partition.
var node = all.node();
this.totalSize = node ? node.__data__.value : 0;
}
// Fade all but the current sequence, and show it in the breadcrumb trail.
Sunburst.prototype.mouseover = function(d) {
var percentage = (100 * d.value / this.totalSize).toPrecision(3);
var sequenceArray = this.getAncestors(d);
this.updateDescription(sequenceArray, d.value, percentage)
this.updateBreadcrumbs(sequenceArray, d.value, percentage);
// Fade all the segments.
this.vis.selectAll("path")
.style("opacity", 0.3);
// Then highlight only those that are an ancestor of the current segment.
this.vis.selectAll("path")
.filter(function(node) {
return (sequenceArray.indexOf(node) >= 0);
})
.style("opacity", 1);
}
// Restore everything to full opacity when moving off the visualization.
Sunburst.prototype.mouseleave = function(d) {
var that = this;
// Hide the breadcrumb trail
d3.select("#trail")
.style("visibility", "hidden");
// Deactivate all segments during transition.
d3.selectAll("path").on("mouseover", null);
// Transition each segment to full opacity and then reactivate it.
//TODO cancel this transition on mouseover
d3.selectAll("path")
.transition()
.duration(1000)
.style("opacity", 1)
.each("end", function() {
d3.select(this).on("mouseover", that.mouseover.bind(that));
});
d3.select(this.opt.selectors.description)
.style("visibility", "hidden");
}
// Given a node in a partition layout, return an array of all of its ancestor
// nodes, highest first, but excluding the root.
Sunburst.prototype.getAncestors = function(node) {
var path = [];
var current = node;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
return path;
}
Sunburst.prototype.initializeBreadcrumbTrail = function() {
// Add the svg area.
var trail = d3.select(this.opt.selectors.breadcrumbs).append("svg:svg")
.attr("width", this.opt.width)
.attr("height", 50)
.attr("id", "trail");
// Add the label at the end, for the percentage.
trail.append("svg:text")
.attr("id", "endlabel")
.style("fill", "#000");
}
// Generate a string that describes the points of a breadcrumb polygon.
Sunburst.prototype.breadcrumbPoints = function(d, i) {
var points = [];
var b = this.opt.breadcrumbs;
points.push("0,0");
points.push(b.w + ",0");
points.push(b.w + b.t + "," + (b.h / 2));
points.push(b.w + "," + b.h);
points.push("0," + b.h);
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
points.push(b.t + "," + (b.h / 2));
}
return points.join(" ");
}
// format the description string in the middle of the chart
Sunburst.prototype.formatDescription = function(sequence, value, percentage) {
return percentage < 0.1 ? "< 0.1%" : percentage + '%';
}
Sunburst.prototype.updateDescription = function(sequence, value, percentage) {
d3.select(this.opt.selectors.description)
.html(this.formatDescription(sequence, value, percentage))
.style("visibility", "");
}
// format the text at the end of the breadcrumbs
Sunburst.prototype.formatBreadcrumbText = function(sequence, value, percentage) {
return value + " (" + (percentage < 0.1 ? "< 0.1%" : percentage + "%") + ")";
}
// Update the breadcrumb trail to show the current sequence and percentage.
Sunburst.prototype.updateBreadcrumbs = function(sequence, value, percentage) {
var that = this;
var b = this.opt.breadcrumbs;
// Data join; key function combines name and depth (= position in sequence).
var g = d3.select("#trail")
.selectAll("g")
.data(sequence, function(d) { return d.name + d.depth; });
// Add breadcrumb and label for entering nodes.
var entering = g.enter().append("svg:g");
entering.append("svg:polygon")
.attr("points", this.breadcrumbPoints.bind(that))
.style("fill", function(d) { return that.getColorByName(d.name); });
entering.append("svg:text")
.attr("x", (b.w + b.t) / 2)
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
// Set position for entering and updating nodes.
g.attr("transform", function(d, i) {
return "translate(" + i * (b.w + b.s) + ", 0)";
});
// Remove exiting nodes.
g.exit().remove();
// Now move and update the percentage at the end.
d3.select("#trail").select("#endlabel")
.attr("x", (sequence.length + 1) * (b.w + b.s))
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.html(this.formatBreadcrumbText(sequence, value, percentage));
// Make the breadcrumb trail visible, if it's hidden.
d3.select("#trail")
.style("visibility", "");
}
Sunburst.prototype.drawLegend = function() {
// Dimensions of legend item: width, height, spacing, radius of rounded rect.
var li = {
w: 75, h: 30, s: 3, r: 3
};
var legend = d3.select(this.opt.selectors.legend).append("svg:svg")
.attr("width", li.w)
.attr("height", d3.keys(this.opt.colors).length * (li.h + li.s));
var g = legend.selectAll("g")
.data(d3.entries(this.opt.colors))
.enter().append("svg:g")
.attr("transform", function(d, i) {
return "translate(0," + i * (li.h + li.s) + ")";
});
g.append("svg:rect")
.attr("rx", li.r)
.attr("ry", li.r)
.attr("width", li.w)
.attr("height", li.h)
.style("fill", function(d) { return d.value; });
g.append("svg:text")
.attr("x", li.w / 2)
.attr("y", li.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.key; });
}
// Take a 2-column CSV and transform it into a hierarchical structure suitable
// for a partition layout. The first column is a sequence of step names, from
// root to leaf, separated by hyphens. The second column is a count of how
// often that sequence occurred.
Sunburst.prototype.buildHierarchy = function(array) {
var root = {"name": "root", "children": []};
for (var i = 0; i < array.length; i++) {
var sequence = array[i][0];
var size = +array[i][1];
if (isNaN(size)) { // e.g. if this is a header row
continue;
}
var parts = sequence.split(this.opt.separator);
var currentNode = root;
for (var j = 0; j < parts.length; j++) {
var children = currentNode["children"] || [];
var nodeName = parts[j];
var childNode;
if (j + 1 < parts.length) {
// Not yet at the end of the sequence; move down the tree.
var foundChild = false;
for (var k = 0; k < children.length; k++) {
if (children[k]["name"] == nodeName) {
childNode = children[k];
foundChild = true;
break;
}
}
// If we don't already have a child node for this branch, create it.
if (!foundChild) {
childNode = {"name": nodeName, "children": []};
children.push(childNode);
}
currentNode = childNode;
} else {
// Reached the end of the sequence; create a leaf node.
childNode = {"name": nodeName, "size": size};
children.push(childNode);
}
}
}
return root;
}
return Sunburst;
}));
All the html, js and css files are stored in same folder.
But when I run it, I get the browser window like this with no visualization but just a legend box on the right.
What am I missing? The dataset is downloaded from the same github page.
Provided the path to visit-sequences.csv is correct, its local loading will be blocked by the browser (see CORS).
You can either:
run it via a (even local) webserver
start Chrome with the --allow-file-access-from-files flag (if you're using Chrome)
The problem is here
sunburst.loadCsv("/Users/i854319/Documents/Mike_UX_web/data/visit-sequences.csv");
Your browser cannot just load a file from your PC.
I was trying to get the sink nodes to align centrally in the vertical direction in d3 JS Sankey implementation. Toward the top it is (almost) properly aligned like this
Correct Alignment
but towards the bottom, the sink nodes are no longer centrally aligned when compared to their inputs
Wrong Alignment
Here's the code `
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 () {
//Original value of 0.5
var curvature = 0.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 = 15;
//Original code
//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) {
nextNodes.push(link.target);
});
});
remainingNodes = nextNodes;
//Original value of x+=1
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;
//Make some changes here
//Originally there in code
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 0;
//Original code +node.dy/2
return node.y+node.dy/2 ;
// return node.y ;
}
function value(link) {
return link.value;
}
return sankey;
`
and here are the user parameters that are usually set
var units = "Widgets";
var VariableHeight = graphData.nodes.length*25; //Change to suit the needs of the
graph,
//reduce factor of 100 for sleeker design
var margin = {top: 10, right: 10, bottom: 10, left: 10},
//Original Values are 700 and 300, 2700 is definitely a dangerous value for width
width = 1200 - margin.left - margin.right,
height = VariableHeight - margin.top - margin.bottom;
var formatNumber = d3.format(",.0f"), // zero decimal places
format = function(d) { return formatNumber(d) + " " + units; },
color = d3.scale.category20();
// append the svg canvas to the html page
var svg = d3.select("#sankeyContainer").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Set the sankey diagram properties
var sankey = d3sankey()
.nodeWidth(36)
.nodePadding(40)
.size([width, height]);
//Changes to connect links to centre of nodes
//Original Value
//var path = sankey.link();
var path = d3.svg.diagonal()
.source(function(d) {
return {"x":d.source.y + d.source.dy / 2,
"y":d.source.x + sankey.nodeWidth()/2};
})
.target(function(d) {
return {"x":d.target.y + d.target.dy / 2,
"y":d.target.x + sankey.nodeWidth()/2};
})
.projection(function(d) { return [d.y, d.x]; });
// load the data
var graph = graphData;
sankey.nodes(graph.nodes)
.links(graph.links)
.layout(32);
// add in the links
var link = svg.append("g").selectAll(".link")
.data(graph.links)
.enter().append("path")
.attr("class", "link")
.attr("d", path)
.style("fill", "none")
.style("stroke", "black")
.style("stroke-opacity", ".1")
//.style("stroke-opacity", ".2")
.on("mouseover", function() { d3.select(this).style("stroke-opacity", ".4") } )
.on("mouseout", function() { d3.select(this).style("stroke-opacity", ".1") } )
.style("stroke-width", function (d) {
return 15;
//return Math.max(1, Math.sqrt(d.dy));
//Original value
//return Math.max(1, d.dy);
})
.sort(function (a, b) {
return b.dy - a.dy;
});
// add the link titles
link.append("title")
.text(function (d) {
return d.source.name + " → " + d.target.name + "\n" + format(d.value);
});
// add in the nodes
var node = svg.append("g").selectAll(".node")
.data(graph.nodes).enter().append("g").attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
}).on("click",function(d){
if (d3.event.defaultPrevented) {
document.getElementById("ErrorDisplay").innerHTML="";
return;}
document.getElementById("ErrorDisplay").innerHTML="You Have Clicked "+ d.name;
}).call(d3.behavior.drag().origin(function(d) {
return d;
}).on("dragstart", function() {
//Removing the following line's comment status will make nodes unclickable
//this.parentNode.appendChild(this);
}).on("drag", dragmove));
// add the rectangles for the nodes, Original Code
/*
node.append("rect")
.attr("height", function (d) {
//Changed to make sure all node heights are the same
//Original Value
//return d.dy;
return 15;
})
.attr("width", sankey.nodeWidth())
*/// add the circles for the nodes
node.append("circle")
.attr("cx", sankey.nodeWidth()/2)
.attr("cy", function (d) {
return d.dy/2;
})
.attr("r", function (d) {
return Math.sqrt(d.dy);
})
.style("fill", function (d) {
return d.color = color(d.name.replace(/ .*/, ""));
})
.style("fill-opacity", ".9")
.style("shape-rendering", "crispEdges")
.style("stroke", function (d) {
return d3.rgb(d.color).darker(2);
})
.append("title")
.text(function (d) {
return d.name + "\n" + format(d.value);
});
// add in the title for the nodes
node.append("text")
.attr("x", -6)
.attr("y", function (d) {
return d.dy / 2+15; //Original value of only d.dy/2
})
.attr("dy", ".35em")
.attr("text-anchor", "end")
.attr("text-shadow", "0 1px 0 #fff")
.attr("transform", null)
.text(function (d) {
return d.name;
})
.filter(function (d) {
return d.x < width / 2;
})
.attr("x", 6 + sankey.nodeWidth())
.attr("text-anchor", "start");
// the function for moving the nodes
function dragmove(d) {
d3.select(this).attr("transform",
"translate(" + (
d.x = Math.max(0, Math.min(width - d.dx, d3.event.x))) + "," + (
d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))) + ")");
sankey.relayout();
link.attr("d", path);
};
`
What I have tried until now
1) Make the center(node) function node return 0, which doesnt work out well
2) Tried removing +node.dy in the expression y0 = node.y + node.dy + nodePadding in nodes.sort, which results in the opposite happening, the bottom half is aligned, but the top alignment is out of order. How do I make the sink nodes align with the center of the corresponding source nodes?
Okay, I got it (quite by chance), in the relaxRightToLeft function, simply comment out the node.y += (y - center(node)) * alpha; line, which causes the sink nodes to misalign
I made a zoomable sunburst visualisation with labels (see in action, or check out the code). When clicking on an item, the innermost visible node has its label turned sideways. I'd like to fix the label just for this one node, but I haven't found a way to do this.
Is there a way to say "if (current node is the root of visible nodes)"? Any other ideas?
The full visualization:
Zoomed in after click. I'd like to make the 'Calm' node text horizontal:
While not perfect, this modified version of the code you were using adjusts the text of the currently selected node as it animates and makes it horizontal.
var width = 960,
height = 700,
radius = (Math.min(width, height) / 2) - 10;
var formatNumber = d3.format(",d");
var x = d3.scaleLinear()
.range([0, 2 * Math.PI]);
var y = d3.scaleLinear()
.range([0, radius]);
var color = d3.scaleOrdinal(d3.schemeCategory20);
var partition = d3.partition();
function startAngle(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x0))); }
function endAngle(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x1))); }
function innerRadius(d) { return Math.max(0, y(d.y0)); }
function outerRadius(d) { return Math.max(0, y(d.y1)); }
var arc = d3.arc()
.startAngle( function(d) { return startAngle(d); })
.endAngle( function(d) { return endAngle(d); })
.innerRadius(function(d) { return innerRadius(d); })
.outerRadius(function(d) { return outerRadius(d); })
var texttransform = function(d) {
var translation = y(d.y0);
var rotation = computeTextRotation(d);
if (rotation > 90 && rotation < 270) {
rotation = rotation + 180;
translation = -translation;
}
return (
"rotate(" + rotation + ")" +
"translate(" + translation + ",0)"
);
}
var transition = {};
function calcTransitionPercentage(){
var now = Date.now()-transition.clockNow;
if(!transition.delay || now > transition.delay){
return Math.min(1,(now-(transition.delay||0))/transition.duration);
}
return 0;
}
function computeTextRotation(d) {
if (d.depth === 0) {
return 0;
}
var current = x((d.x0 + d.x1)/2);
var angle = (current - Math.PI / 2) / Math.PI * 180;
if(transition.node === d){
angle -= 90 * calcTransitionPercentage();
}
return (angle > 90 || angle < 270) ? angle : 180 + angle ;
}
var textanchor = function(d) {
if (d.depth === 0) {
return "middle";
}
var rotation = computeTextRotation(d);
return (rotation > 90 && rotation < 270) ? "end" : "start";
}
var textdx = function(d) {
if (d.depth === 0) {
return 0;
}
var rotation = computeTextRotation(d);
return (rotation > 90 && rotation < 270) ? -6 : 6;
}
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2) + ")");
function calcFontSize(d) {
const xFactor = 12, yFactor = 7.5 ; // stub
if (d.depth === 0) {
return "30px";
}
// use inner arc len as text height delimiter
var innerArc = (endAngle(d) - startAngle(d)) * 2 * Math.PI * innerRadius(d);
var len = (d.y1-d.y0) * radius;
return Math.min(innerArc / yFactor, len / d.data.textlen * xFactor) + "px";
}
function click(d = { x0: 0, x1: 1, y0: 0, y1: 1 }) {
transition = {clockNow: Date.now(), duration: 750, node: d }
var trans = svg.transition().duration(750);
trans.selectAll("path")
.attrTween("d", function(n) { return function() { return arc(n); }; })
.tween("scale", function() {
var xd = d3.interpolate(x.domain(), [d.x0, d.x1]),
yd = d3.interpolate(y.domain(), [d.y0, 1]),
yr = d3.interpolate(y.range(), [d.y0 ? 20 : 0, radius]);
return function(t) {
x.domain(xd(t));
y.domain(yd(t)).range(yr(t));
};
});
trans.selectAll("text")
.attrTween("transform", function(n) { return function() { return texttransform(n); }; })
.attrTween("text-anchor", function(n) { return function() { return textanchor(n); }; })
.attrTween("dx", function(n) { return function() { return textdx(n); }; })
.styleTween("font-size", function(n) { return function() { return calcFontSize(n); }; });
trans.selectAll("text")
.delay(400)
.attrTween("opacity", function(n) { return function() {
if (d === n || n.ancestors().includes(d)) {
return 1;
} else {
return 0;
}
}; });
}
d3.text('https://raw.githubusercontent.com/manooh/NVSee/master/data/feelings_EN.txt', function(error, raw){
if (error) throw error;
// replace two-space indentation with pipes
raw = raw.replace(new RegExp(' ', 'g'), '|');
//read pipe-delimited data
var dsv = d3.dsvFormat('|');
var flatData = dsv.parse(raw);
var rData = currentNode = tree(flatData);
rData = d3.hierarchy(rData);
var nodes = partition(rData
.sum(function(d) { return 1; }) // each leaf gets a size of 1
.sort(function(a, b) { d3.ascending(a.name, b.name) }) // not working?
)
.descendants();
g = svg.selectAll("path")
.data(nodes)
.enter().append("g");
path = g.append("path")
.attr("d", arc)
.style("fill", function(d, i) {
var c;
if (d.depth === 0) {
return "white";
} else if (d.depth === 1) {
c = color((d.children ? d : d.parent).data.name);
} else if (d.depth > 1) {
c = d3.color(d.parent.data.color).darker();
}
d.data.color = c;
return c;
})
.on("click", click)
.append("title")
.text(function(d) { return d.data.name });
text = g.append("text")
.style("fill", function(d) {
if (d.depth === 0) {
return "#CCC";
} else {
return "#FFF";
}})
.attr("class", "svglabel")
.attr("transform", texttransform)
.attr("text-anchor", textanchor)
.attr("dx", textdx)
.attr("dy", ".35em") // vertical-align
.text(function(d) { return d.data.name; })
.style("font-size", function(d) {
// hack. save text len as property to make accessible in transiton
d.data.textlen = this.getComputedTextLength();
return calcFontSize(d);
});
});
function tree(nodes) {
var curr, parent, root;
var lev = 1;
nodes.forEach(function(d) {
if (!root) {
// handle root (first node)
curr = {
name: d.d1,
children: []
};
root = curr;
parent = curr;
} else {
if (d['d' + (lev+1)]) {
// handle children
lev = lev+1;
parent = curr;
} else if (d['d' + (lev-1)]) {
// handle moving up the hierarchy
lev = lev-1;
parent = parent.parent;
} else if (!d['d' + lev]) {
// if it's neither child, nor moving up, nor a sibling, handle exception
throw "unhandled tree level";
}
curr = {
name: d['d' + lev],
children: []
};
curr.parent = parent;
parent.children.push(curr);
}
});
return root;
}
.svglabel {
font-family: sans-serif;
pointer-events: none;
}
body {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome and Opera */
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>
I'm trying to customize a sunburst chart from different examples that I've found accross the internet. I'm working in R (and Shiny as well) and there is one package that I thought could a good working base, that is SunburstR.
A working example can be viewed here.
What I want to have :
breadcrumbs
zoomable (forward when cliking a child, and backward when clicking the center)
fading as you hover the childs it fades other elements except current curent hierarchy
the percentage in the middle of the chart (or elsewhere relevant) as well as the breadcrumbs. Could be one or the other as long as it is somewhere on the page.
emphasizing with a bigger circle all around the chart. You'd see the whole circle if hovering the center, and you'd see the current footprint of current hovered element (from startangle to endangle). This is just to help seeing how much what you're hovering is reprensenting compare to the whole thing. This circle would also be the bounding circle, eg when you are exiting it, it cancels the fading since you're not hovering the chart anymore.
I have a little bit of code of everything but struggle to put everything together. Here is where things are currently standing :
I've got the fading ok.
I've got the percentages (both center and breadcrumbs)
I've got the outerRadius defined to allow for the bounding circle around the chart, currently visible at all times. I can't find a way to make it appear when I'm hovering (only the arc of the current hovered element, whole circle when hovering the center) and put it back to #fff
when not hovering anymore this circle.
I don't have (at all) the abitlity to zoom
Current sunburst.js :
HTMLWidgets.widget({
name: 'sunburst',
type: 'output',
factory: function(el, width, height) {
var instance = {};
instance.chart = {};
var dispatch = d3.dispatch("mouseover","mouseleave","click");
d3.rebind(instance.chart, dispatch, 'on');
var draw = function(el, instance) {
// would be much nicer to implement transitions/animation
// remove previous in case of Shiny/dynamic
d3.select(el).select(".sunburst-chart svg").remove();
var x = instance.x;
var json = instance.json;
var chart = instance.chart;
// Dimensions of sunburst
var width = el.getBoundingClientRect().width - (x.options.legend.w ? x.options.legend.w : 75);
var height = el.getBoundingClientRect().height - 70;
var radius = Math.min(width, height) / 2;
var outerRadius = radius/3.5; // reserved pixels all around the vis
d3.select(el).select(".sunburst-chart").append("svg")
.style("width", width + "px") // shouldnt have to do this
.style("height", height + "px"); // shouldnt have to do this
// Breadcrumb dimensions: width, height, spacing, width of tip/tail.
// these will be the defaults
var b = {
w: 0, h: 30, s: 3, t: 10
};
// if breadcrumb is provided in the option, we will overwrite
// with what is provided
Object.keys(x.options.breadcrumb).map(function(ky){
b[ky] = x.options.breadcrumb[ky];
});
/*
// Mapping of step names to colors.
var colors = {
"home": "#5687d1",
"product": "#7b615c",
"search": "#de783b",
"account": "#6ab975",
"other": "#a173d1",
"end": "#bbbbbb"
};
*/
var colors = d3.scale.category20();
if(x.options.colors !== null){
// if an array then we assume the colors
// represent an array of hexadecimal colors to be used
if(Array.isArray(x.options.colors)) {
try{
colors.range(x.options.colors)
} catch(e) {
}
}
// if an object with range then we assume
// that this is an array of colors to be used as range
if(x.options.colors.range){
try{
colors.range(x.options.colors.range)
} catch(e) {
}
}
// if an object with domain then we assume
// that this is an array of colors to be used as domain
// for more precise control of the colors assigned
if(x.options.colors.domain){
try{
colors.domain(x.options.colors.domain);
} catch(e) {
}
}
// if a function then set to the function
if(typeof(x.options.colors) === "function") {
colors = x.options.colors;
}
}
// Total size of all segments; we set this later, after loading the data.
var totalSize = 0;
var vis = d3.select(el).select(".sunburst-chart").select("svg")
.append("g")
.attr("id", el.id + "-container")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var partition = d3.layout.partition()
//.size([2 * Math.PI, radius * radius])
.size([2 * Math.PI, (radius - outerRadius) * (radius - outerRadius)])
.value(function(d) { return d[x.options.valueField || "size"]; });
// check for sort function
if(x.options.sortFunction){
partition.sort(x.options.sortFunction);
}
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return (d.dy == d.y) ? Math.sqrt(d.dy)/3 : Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
createVisualization(json);
// set up a container for tasks to perform after completion
// one example would be add callbacks for event handling
// styling
if (!(typeof x.tasks === "undefined") ){
if ( (typeof x.tasks.length === "undefined") ||
(typeof x.tasks === "function" ) ) {
// handle a function not enclosed in array
// should be able to remove once using jsonlite
x.tasks = [x.tasks];
}
x.tasks.map(function(t){
// for each tasks call the task with el supplied as `this`
t.call({el:el,x:x,instance:instance});
});
}
// Main function to draw and set up the visualization, once we have the data.
function createVisualization(json) {
// Basic setup of page elements.
initializeBreadcrumbTrail();
// Bounding circle underneath the sunburst, to make it easier to detect
// when the mouse leaves the parent g.
vis.append("circle")
.attr("r", radius - outerRadius)
.style("opacity", 0);
var highlight = vis.append("circle")
.attr("r", radius)
.style("fill", "#eee");
// For efficiency, filter nodes to keep only those large enough to see.
var nodes = partition.nodes(json)
.filter(function(d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
});
var path = vis.data([json]).selectAll("path")
.data(nodes)
.enter().append("path")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d) { return colors.call(this, d.name); })
.style("opacity", 1)
.on("mouseover", mouseover)
//.on("mouseleave", mouseleave)
.on("click", click);
// Add the mouseleave handler to the bounding circle.
d3.select(el).select("#"+ el.id + "-container").on("mouseleave", mouseleave);
// Get total size of the tree = value of root node from partition.
totalSize = path.node().__data__.value;
drawLegend(nodes);
d3.select(el).select(".sunburst-togglelegend").on("click", toggleLegend);
}
// Fade all but the current sequence, and show it in the breadcrumb trail.
function mouseover(d) {
//highlight.attr("d", arc(d));
//highlight.style("fill", "#eee");
var percentage = (100 * d.value / totalSize).toPrecision(3);
var percentageString = percentage + "%";
if (percentage < 0.1) {
percentageString = "< 0.1%";
}
var countString = [
'<span style = "font-size:.7em">',
d3.format("1.2s")(d.value) + ' of ' + d3.format("1.2s")(totalSize),
'</span>'
].join('')
var explanationString = "";
if(x.options.percent && x.options.count){
explanationString = percentageString + '<br/>' + countString;
} else if(x.options.percent){
explanationString = percentageString;
} else if(x.options.count){
explanationString = countString;
}
//if explanation defined in R then use this instead
if(x.options.explanation !== null){
explanationString = x.options.explanation.bind(totalSize)(d);
}
d3.select(el).selectAll(".sunburst-explanation")
.style("visibility", "")
.style("top",((height - 35)/2) + "px")
.style("width",width + "px")
.html(explanationString);
var sequenceArray = getAncestors(d);
chart._selection = sequenceArray.map(
function(d){return d.name}
);
dispatch.mouseover(chart._selection);
updateBreadcrumbs(sequenceArray, percentageString);
// Fade all the segments.
d3.select(el).selectAll("path")
.style("opacity", 0.3);
// Then highlight only those that are an ancestor of the current segment.
vis.selectAll("path")
.filter(function(node) {
return (sequenceArray.indexOf(node) >= 0);
})
.style("opacity", 1);
}
// Restore everything to full opacity when moving off the visualization.
function mouseleave(d) {
//highlight.attr("d", null);
//highlight.style("fill", "#fff");
dispatch.mouseleave(chart._selection);
chart._selection = [];
// Hide the breadcrumb trail
d3.select(el).select("#" + el.id + "-trail")
.style("visibility", "hidden");
// Deactivate all segments during transition.
d3.select(el).selectAll("path").on("mouseover", null);
// Transition each segment to full opacity and then reactivate it.
d3.select(el).selectAll("path")
.transition()
.duration(250)
.style("opacity", 1)
.each("end", function() {
d3.select(this).on("mouseover", mouseover);
});
d3.select(el).selectAll(".sunburst-explanation")
.style("visibility", "hidden");
}
function click(d,i) {
var sequenceArray = getAncestors(d);
dispatch.click(sequenceArray.map(
function(d){return d.name}
));
}
// Given a node in a partition layout, return an array of all of its ancestor
// nodes, highest first, but excluding the root.
function getAncestors(node) {
var path = [];
var current = node;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
return path;
}
function initializeBreadcrumbTrail() {
// Add the svg area.
var trail = d3.select(el).select(".sunburst-sequence").append("svg")
.attr("width", width)
//.attr("height", 50)
.attr("id", el.id + "-trail");
// Add the label at the end, for the percentage.
trail.append("text")
.attr("id", el.id + "-endlabel")
.style("fill", "#000");
}
// Generate a string that describes the points of a breadcrumb polygon.
function breadcrumbPoints(d, i) {
var points = [];
points.push("0,0");
if (b.w <= 0) {
// calculate breadcrumb width based on string length
points.push(d.string_length + ",0");
points.push(d.string_length + b.t + "," + (b.h / 2));
points.push(d.string_length + "," + b.h);
} else {
points.push(b.w + ",0");
points.push(b.w + b.t + "," + (b.h / 2));
points.push(b.w + "," + b.h);
}
points.push("0," + b.h);
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
points.push(b.t + "," + (b.h / 2));
}
return points.join(" ");
}
// Update the breadcrumb trail to show the current sequence and percentage.
function updateBreadcrumbs(nodeArray, percentageString) {
// Data join; key function combines name and depth (= position in sequence).
var g = d3.select(el).select("#" + el.id + "-trail")
.selectAll("g")
.data(nodeArray, function(d) { return d.name + d.depth; });
// Add breadcrumb and label for entering nodes.
var entering = g.enter().append("g");
if (b.w <= 0) {
// Create a node array that contains all the breadcrumb widths
// Calculate positions of breadcrumbs based on string lengths
var curr_breadcrumb_x = 0;
nodeArray[0].breadcrumb_x = 0;
nodeArray[0].breadcrumb_h = 0;
entering.append("polygon")
.style("z-index",function(d,i) { return(999-i); })
.style("fill", function(d) { return colors.call(this, d.name); });
entering.append("text")
.attr("x", b.t + 2)
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "left")
.text(function(d) { return d.name; });
// Remove exiting nodes.
g.exit().remove();
// loop through each g element
// calculate string length
// draw the breadcrumb polygon
// and determine if breadcrumb should be wrapped to next row
g.each(function(d,k){
var crumbg = d3.select(this);
var my_string_length = crumbg.select("text").node().getBoundingClientRect().width;
nodeArray[k].string_length = my_string_length + 12;
crumbg.select("polygon").attr("points", function(d){
return breadcrumbPoints(d, k);
});
var my_g_length = crumbg.node().getBoundingClientRect().width;
curr_breadcrumb_x += k===0 ? 0 : nodeArray[k-1].string_length + b.s;
nodeArray[k].breadcrumb_h = k===0 ? 0 : nodeArray[k-1].breadcrumb_h;
if (curr_breadcrumb_x + my_g_length > width*0.99) {
nodeArray[k].breadcrumb_h += b.h; // got to next line
curr_breadcrumb_x = b.t + b.s; // restart counter
}
nodeArray[k].breadcrumb_x = curr_breadcrumb_x;
});
// Set position for entering and updating nodes.
g.attr("transform", function(d, i) {
return "translate(" + d.breadcrumb_x + ", "+d.breadcrumb_h+")";
});
// Now move and update the percentage at the end.
d3.select(el).select("#" + el.id + "-trail").select("#" + el.id + "-endlabel")
.attr("x", function(d){
var bend = d3.select(this);
var curr_breadcrumb_x = nodeArray[nodeArray.length-1].breadcrumb_x + nodeArray[nodeArray.length-1].string_length + b.t + b.s;
var my_g_length = bend.node().getBoundingClientRect().width;
var curr_breadcrumb_h = nodeArray[nodeArray.length-1].breadcrumb_h + b.h/2;
if (curr_breadcrumb_x + my_g_length > width*0.99) {
curr_breadcrumb_h += b.h + b.h/2;
curr_breadcrumb_x = b.t + b.s; // restart counter
}
bend.datum({
"breadcrumb_x": curr_breadcrumb_x,
"breadcrumb_h": curr_breadcrumb_h
});
return curr_breadcrumb_x;
})
.attr("y", function(d){return d.breadcrumb_h})
.attr("dy", "0.35em")
.attr("text-anchor", "start")
.text(percentageString);
} else {
entering.append("polygon")
.attr("points", breadcrumbPoints)
.style("fill", function(d) { return colors.call(this, d.name); });
entering.append("text")
.attr("x", (b.w + b.t) / 2)
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
// Set position for entering and updating nodes.
g.attr("transform", function(d, i) {
return "translate(" + i * (b.w + b.s) + ", 0)";
});
// Remove exiting nodes.
g.exit().remove();
// Now move and update the percentage at the end.
d3.select(el).select("#" + el.id + "-trail").select("#" + el.id + "-endlabel")
.attr("x", (nodeArray.length + 0.5) * (b.w + b.s))
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(percentageString);
}
// Make the breadcrumb trail visible, if it's hidden.
d3.select(el).select("#" + el.id + "-trail")
.style("visibility", "");
}
function drawLegend(nodes) {
// Dimensions of legend item: width, height, spacing, radius of rounded rect.
var li = {
w: 75, h: 30, s: 3, r: 3
};
// if legend is provided in the option, we will overwrite
// with what is provided
Object.keys(x.options.legend).map(function(ky){
li[ky] = x.options.legend[ky];
});
// remove if already drawn
d3.select(el).select(".sunburst-legend svg").remove();
// get labels from node names
var labels = d3.nest()
.key(function(d) {return d.name})
.entries(
nodes.sort(
function(a,b) {return d3.ascending(a.depth,b.depth)}
)
)
.map(function(d) {
return d.values[0];
})
.filter(function(d) {
return d.name !== "root";
});
var legend = d3.select(el).select(".sunburst-legend").append("svg")
.attr("width", li.w)
.attr("height", labels.length * (li.h + li.s));
var g = legend.selectAll("g")
.data( function(){
if(x.options.legendOrder !== null){
return x.options.legendOrder;
} else {
// get sorted by top level
return labels;
}
})
.enter().append("g")
.attr("transform", function(d, i) {
return "translate(0," + i * (li.h + li.s) + ")";
});
g.append("rect")
.attr("rx", li.r)
.attr("ry", li.r)
.attr("width", li.w)
.attr("height", li.h)
.style("fill", function(d) { return colors.call(this, d.name); });
g.append("text")
.attr("x", li.w / 2)
.attr("y", li.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
}
function toggleLegend() {
var legend = d3.select(el).select(".sunburst-legend")
if (legend.style("visibility") == "hidden") {
legend.style("visibility", "");
} else {
legend.style("visibility", "hidden");
}
}
};
// Take a 2-column CSV and transform it into a hierarchical structure suitable
// for a partition layout. The first column is a sequence of step names, from
// root to leaf, separated by hyphens. The second column is a count of how
// often that sequence occurred.
function buildHierarchy(csv) {
var root = {"name": "root", "children": []};
for (var i = 0; i < csv.length; i++) {
var sequence = csv[i][0];
var size = +csv[i][1];
if (isNaN(size)) { // e.g. if this is a header row
continue;
}
var parts = sequence.split("-");
var currentNode = root;
for (var j = 0; j < parts.length; j++) {
var children = currentNode["children"];
var nodeName = parts[j];
var childNode;
if (j + 1 < parts.length) {
// Not yet at the end of the sequence; move down the tree.
var foundChild = false;
for (var k = 0; k < children.length; k++) {
if (children[k]["name"] == nodeName) {
childNode = children[k];
foundChild = true;
break;
}
}
// If we don't already have a child node for this branch, create it.
if (!foundChild) {
childNode = {"name": nodeName, "children": []};
children.push(childNode);
}
currentNode = childNode;
} else {
// Reached the end of the sequence; create a leaf node.
childNode = {"name": nodeName, "size": size};
children.push(childNode);
}
}
}
return root;
};
return {
renderValue: function(x) {
instance.x = x;
// x.data should be a data.frame in R so an Javascript Object of Objects
// but buildHierarchy expects an Array of Arrays
// so use d3.zip and apply to do this
var json = [];
if(x.csvdata !== null){
json = buildHierarchy(
d3.zip.apply(
null,
Object.keys(x.csvdata).map(function(ky){return x.csvdata[ky]})
)
);
} else {
json = x.jsondata
}
instance.json = json;
draw(el, instance);
},
resize: function(width, height) {
draw(el, instance);
},
instance: instance
};
}
});
I have found this question where Skip Jack gives this pen, but I can't get it to work along with the rest of the sunburstR package... And still this pen would miss the bounding circle and the percentages.
I must emphasize that I'm pretty new to JS so please excuse me if this actually quite easy to solve.
Any help would be welcome, thanks ahead of time for your support !
I am working on a project to plot a stream of csv/Json data (bar chart) where the order of arrival of the data is important.
The Y axis is unique, but there are multiple X axes that correspond to different measures of the data.
I am having trouble producing a nice graph that looks like this, given the following data:
x0,x1,x2,y,idx
-1,z,w2,10,0
0,z,w2,9,1
1,z,w2,8,2
-1,k,w2,11,3
0,k,5q,5,4
1,k,5q,8,5
idx represent the order the data arrives in.
this is what I get
X=["idx","x0","x1","x2"];
Y=["y"];
var margin = {
top: 80,
right: 180,
bottom: 180,
left: 180
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = [],
x = [];
var x_uid = d3.scale.ordinal()
.rangeRoundPoints([0, width]);
for (var idx = 0; idx < X.length; idx++) {
x[idx] = d3.scale.ordinal()
.rangeRoundPoints([0, width]);
xAxis[idx] = d3.svg.axis()
.scale(x[idx])
.orient("bottom");
}
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
// .ticks(8, "%");
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var data = [{
x0:-1,
x1:z,
x2:w2,
y:10,
idx:0
},
{
x0:0,
x1:z,
x2:w2,
y:10,
idx:1
},
{
x0:1,
x1:z,
x2:w2,
y:10,
idx:2
},
{
x0:-1,
x1:j,
x2:w2,
y:10,
idx:3
},
{
x0:0,
x1:j,
x2:5q,
y:10,
idx:4
},
{
x0:1,
x1:j,
x2:5q,
y:10,
idx:5
}]
if(data) {
for (var idx = 0; idx < X.length; idx++) {
x[idx].domain(data.map(function(d) {
return d[X[idx]];
}));
}
x_uid.domain(data.map(function(d) {
return d.idx;
}));
y.domain([0, d3.max(data, function(d) {
d.value = d[Y[0]];
return d.value;
})]);
for (var idx = 0; idx < X.length; idx++)
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (height + idx * 25) + ")")
.call(xAxis[idx]);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) {
return x_uid(d.idx);
})
.attr("width", 1)
.attr("y", function(d) {
return y(d.value);
})
.attr("height", function(d) {
return height - y(d.value);
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.6/d3.min.js"></script>
<div id="chart"></div>
Offsetting the ticks' text is not an issue, but I am having problems with the interpolation due to the multiplicities of the values:
e.g. width of w2 > width of 5q
e.g. x0 axis should be -1 0 1 -1 0 1 but d3 interpolates as -1 0 1
I tried using rangeRoundBand instead of rangeRoundPoint but the issue is similar.
I also tried playing around with tickValues but to no avail.
I tried doing my own interpolation using linear scales instead of ordinal, but it becomes very messy very quickly because is forces me to manually calculate and adjust all the ticks' positions and texts while taking into account the d3.behavior zoom level etc...
function adjustTickPosition(selection, count, scale, translate, rotate) {
//selection = axis
//count = multiplicity of each tick
//scale = d3.behavior.zoom scale
//translate = d3.behavior.zoom translation
//rotate = irrelevent here (additional styling)
console.info( selection.selectAll("g.tick"))
// cancel previous position
//
// /!\ For some reason there is always 100 ticks instead of the appropriate number
//
selection.selectAll("g.tick")
.attr("transform", "translate(0,0)");
// align tick marks
selection.selectAll("g.tick line")
.attr('transform', function (d, k) {
if (k <= count.length - 1) {
var newPosition = scaleTranslate(count[k]);
if (newPosition > width || newPosition < 0) {
d3.select(this.parentNode).style("visibility", "hidden");
} else
d3.select(this.parentNode).style("visibility", "visible");
return 'translate(' + newPosition + ',0)';
} else
return 'translate(0,0)';
});
// offset tick label compared to tick marks
selection.selectAll("g.tick text")
.attr('transform', function (d, k) {
if (k <= count.length - 1) {
var pos, transform;
if (k > 0) pos = (count[k - 1] + count[k]) / 2;
else pos = count[k] / 2;
var newPosition = scaleTranslate(pos);
if (newPosition > width || newPosition < 0) {
d3.select(this.parentNode).style("visibility", "hidden");
} else
d3.select(this.parentNode).style("visibility", "visible");
var transform = 'translate(' + newPosition + ',0)';
if (rotate) transform += ' rotate(-65)';
return transform;
} else
return 'translate(0,0)';
});
if (rotate) selection.selectAll("g.tick text").style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em");
return selection;
function scaleTranslate(v) {
return v / count[count.length - 1] * width * scale + translate[0];
}
}
Could someone please show me how to properly use axes ticks for this kind of purpose?
Thank you in advance
I made my own class/object because d3 was apparently not meant for this kind of graph
function chartAxis(key, args) {
//***************************
// PRIVATE
//***************************
var _direction = args ? (args.direction || "x") : "x";
var _width = args ? (args.width || 500) : 500;
var _alignTicks = args ? (args.alignTicks || false) : false;
var _tickSize = args ? (args.tickSize || 0) : 0;
var _numTicks = args ? (args.numTicks || 10) : 10;
var _offset = args ? (args.offset || 25) : 25;
var _zoom = args ? (args.zoom || {
s: 1,
t: 0
}) : {
s: 1,
t: 0
};
var _totalLength;
function consecutiveReduction(list, key) {
var Bin = function (val, cnt) {
return {
value: val,
count: cnt,
cumulativeCount: 0,
center: 0,
position: 0
};
};
var result = list.map(function (d) {
return key ? d[key] : d;
}).reduce(function (acc, d) {
var currentBin = acc[acc.length - 1];
if ((acc.length > 0) && d === currentBin.value) {
//add to current bin
currentBin.count++;
} else {
//create new bin
acc.push(new Bin(d, 1));
}
return acc;
}, []);
result.forEach(accumulate);
result.forEach(positionTick);
return result;
}
function positionTick(d) {
d.position = ApplyZoom(d.cumulativeCount);
d.center = _alignTicks ? d.position : ApplyZoom(d.cumulativeCount - d.count / 2);
function ApplyZoom(val) {
var translate;
if (_zoom.t.length > 1)
translate = (_direction == "x") ? _zoom.t[0] : _zoom.t[1];
else
translate = _zoom.t;
return val / _totalLength * _width * _zoom.s + translate;
}
}
function accumulate(d, i, arr) {
d.cumulativeCount = d.count;
if (i > 0) d.cumulativeCount += arr[i - 1].cumulativeCount;
}
//***************************
// PUBLIC
//***************************
var xAxis = function (selection) {
selection.each(function (data) {
// calculate
_totalLength = data.length;
var tickData = consecutiveReduction(data, key);
console.log(tickData.map(function (d) {
return d.count
}))
console.table(data,key)
//create parent axis with clip-path
var axis = d3.select(this)
.attr("id", key);
axis.selectAll("#clipAxis-" + key).data([1]).enter()
.append("clipPath")
.attr("id", "clipAxis-" + key)
.append("svg:rect")
.attr("x", 0)
.attr("y", _offset - _tickSize)
.attr("width", _width)
.attr("height", 25 + _tickSize);
// Axis line and label
var axisLine = axis.selectAll(".axisLine").data([1]).enter();
axisLine.append("line").attr({
x1: 0,
y1: _offset,
x2: _width,
y2: _offset,
class: "axisLine"
});
axisLine.append("text")
.text(key)
.attr({
x: _width + 10,
y: _offset
}).style("text-anchor", "start");
// tick on the axis
var ticks = axis.selectAll("g.tick")
.data(tickData);
// ENTER
var newticks = ticks.enter().append("g").attr("class", "tick");
newticks.append("line");
newticks.append("text");
// UPDATE
ticks.attr("clip-path", "url(#clipAxis-" + key + ")");
ticks.select(".tick line")
.attr("x1", function (d) {
return d.position
})
.attr("x2", function (d) {
return d.position
})
.attr("y1", function (d) {
return _offset - _tickSize
})
.attr("y2", function (d) {
return _offset + 5
});
ticks.select(".tick text")
.text(function (d) {
return d.value;
})
.attr("x", function (d) {
return d.center;
})
.attr("y", function (d) {
return _offset + 10;
})
.style("text-anchor", "middle")
.style("text-length", function (d) {
return (0.6 * 2 * (d.position - d.center)) + "px";
});
// EXIT
ticks.exit().remove();
})
};
var yAxis = function (selection) {
selection.each(function (data) {
// calculate
_totalLength = data.length;
var tickData = d3.extent(data, function (d) {
return d[key];
});
var tickRange = (tickData[1] - tickData[0]) / (_numTicks - 4 + 1); // -4 -> [0.85*min min ... max 1.15*max]
console.log(tickData.map(function (d) {
return d.count
}))
console.log(_tickSize)
//create parent axis with clip-path
var axis = axisLine = d3.select(this)
.attr("id", key);
axis.selectAll("#clipAxis-" + key).data([1]).enter()
.append("clipPath")
.attr("id", "clipAxis-" + key)
.append("svg:rect")
.attr("x", _offset)
.attr("y", 0)
.attr("width", _width)
.attr("height", 25 + _tickSize);
// Axis line and label
axisLine = axis.selectAll(".axisLine").data([1]).enter();
axisLine.append("line").attr({
x1: _offset,
y1: 0,
x2: _offset,
y2: _width,
class: "axisLine"
});
axisLine.append("text")
.text(key)
.attr({
x: _offset,
y: -10
}).style("text-anchor", "start");
// tick on the axis
var ticks = axis.selectAll("g.tick")
.data(tickData);
// ENTER
var newticks = ticks.enter().append("g").attr("class", "tick");
newticks.append("line");
newticks.append("text");
// UPDATE
ticks.attr("clip-path", "url(#clipAxis-" + key + ")");
ticks.select(".tick line")
.attr("x1", function (d) {
return _offset - 5
})
.attr("x2", function (d) {
return _offset + _tickSize
})
.attr("y1", function (d) {
return d.position
})
.attr("y2", function (d) {
return d.position
});
ticks.select(".tick text")
.text(function (d) {
return d.value;
})
.attr("x", function (d) {
return _offset + 10;
})
.attr("y", function (d) {
return d.center;
})
.style("text-anchor", "middle")
.style("text-length", function (d) {
return (0.6 * 2 * (d.position - d.center)) + "px";
});
// EXIT
ticks.exit().remove();
}); // end select.foreach
}; // end yAxis
xAxis.BindToZoom = function (zoomObject) {
_zoom = zoomObject;
return xAxis;
}
yAxis.BindToZoom = function (zoomObject) {
_zoom = zoomObject;
return yAxis;
}
return (_direction == "x") ? xAxis : yAxis;
}
Usage:
function chartAxis(key, args) {
//***************************
// PRIVATE
//***************************
var _direction = args ? (args.direction || "x") : "x";
var _width = args ? (args.width || 500) : 500;
var _alignTicks = args ? (args.alignTicks || false) : false;
var _tickSize = args ? (args.tickSize || 0) : 0;
var _numTicks = args ? (args.numTicks || 10) : 10;
var _offset = args ? (args.offset || 25) : 25;
var _zoom = args ? (args.zoom || {
s: 1,
t: 0
}) : {
s: 1,
t: 0
};
var _totalLength;
function consecutiveReduction(list, key) {
var Bin = function(val, cnt) {
return {
value: val,
count: cnt,
cumulativeCount: 0,
center: 0,
position: 0
};
};
var result = list.map(function(d) {
return key ? d[key] : d;
}).reduce(function(acc, d) {
var currentBin = acc[acc.length - 1];
if ((acc.length > 0) && d === currentBin.value) {
//add to current bin
currentBin.count++;
} else {
//create new bin
acc.push(new Bin(d, 1));
}
return acc;
}, []);
result.forEach(accumulate);
result.forEach(positionTick);
return result;
}
function positionTick(d) {
d.position = ApplyZoom(d.cumulativeCount);
d.center = _alignTicks ? d.position : ApplyZoom(d.cumulativeCount - d.count / 2);
function ApplyZoom(val) {
var translate;
if (_zoom.t.length > 1)
translate = (_direction == "x") ? _zoom.t[0] : _zoom.t[1];
else
translate = _zoom.t;
return val / _totalLength * _width * _zoom.s + translate;
}
}
function accumulate(d, i, arr) {
d.cumulativeCount = d.count;
if (i > 0) d.cumulativeCount += arr[i - 1].cumulativeCount;
}
//***************************
// PUBLIC
//***************************
var xAxis = function(selection) {
selection.each(function(data) {
// calculate
_totalLength = data.length;
var tickData = consecutiveReduction(data, key);
//create parent axis with clip-path
var axis = d3.select(this)
.attr("id", key);
axis.selectAll("#clipAxis-" + key).data([1]).enter()
.append("clipPath")
.attr("id", "clipAxis-" + key)
.append("svg:rect")
.attr("x", 0)
.attr("y", _offset - _tickSize)
.attr("width", _width)
.attr("height", 25 + _tickSize);
// Axis line and label
var axisLine = axis.selectAll(".axisLine").data([1]).enter();
axisLine.append("line").attr({
x1: 0,
y1: _offset,
x2: _width,
y2: _offset,
class: "axisLine"
});
axisLine.append("text")
.text(key)
.attr({
x: _width + 10,
y: _offset
}).style("text-anchor", "start");
// tick on the axis
var ticks = axis.selectAll("g.tick")
.data(tickData);
// ENTER
var newticks = ticks.enter().append("g").attr("class", "tick");
newticks.append("line");
newticks.append("text");
// UPDATE
ticks.attr("clip-path", "url(#clipAxis-" + key + ")");
ticks.select(".tick line")
.attr("x1", function(d) {
return d.position
})
.attr("x2", function(d) {
return d.position
})
.attr("y1", function(d) {
return _offset - _tickSize
})
.attr("y2", function(d) {
return _offset + 5
});
ticks.select(".tick text")
.text(function(d) {
return d.value;
})
.attr("x", function(d) {
return d.center;
})
.attr("y", function(d) {
return _offset + 10;
})
.style("text-anchor", "middle")
.style("text-length", function(d) {
return (0.6 * 2 * (d.position - d.center)) + "px";
});
// EXIT
ticks.exit().remove();
})
};
var yAxis = function(selection) {
selection.each(function(data) {
// calculate
_totalLength = data.length;
var tickData = consecutiveReduction(data, key);
//create parent axis with clip-path
var axis = axisLine = d3.select(this)
.attr("id", key);
axis.selectAll("#clipAxis-" + key).data([1]).enter()
.append("clipPath")
.attr("id", "clipAxis-" + key)
.append("svg:rect")
.attr("x", _offset)
.attr("y", 0)
.attr("width", _width)
.attr("height", 25 + _tickSize);
// Axis line and label
axisLine = axis.selectAll(".axisLine").data([1]).enter();
axisLine.append("line").attr({
x1: _offset,
y1: 0,
x2: _offset,
y2: _width,
class: "axisLine"
});
axisLine.append("text")
.text(key)
.attr({
x: _offset,
y: -10
}).style("text-anchor", "start");
// tick on the axis
var ticks = axis.selectAll("g.tick")
.data(tickData);
// ENTER
var newticks = ticks.enter().append("g").attr("class", "tick");
newticks.append("line");
newticks.append("text");
// UPDATE
ticks.attr("clip-path", "url(#clipAxis-" + key + ")");
ticks.select(".tick line")
.attr("x1", function(d) {
return _offset - 5
})
.attr("x2", function(d) {
return _offset + _tickSize
})
.attr("y1", function(d) {
return d.position
})
.attr("y2", function(d) {
return d.position
});
ticks.select(".tick text")
.text(function(d) {
return d.value;
})
.attr("x", function(d) {
return _offset + 10;
})
.attr("y", function(d) {
return d.center;
})
.style("text-anchor", "middle")
.style("text-length", function(d) {
return (0.6 * 2 * (d.position - d.center)) + "px";
});
// EXIT
ticks.exit().remove();
}); // end select.foreach
}; // end yAxis
xAxis.BindToZoom = function(zoomObject) {
_zoom = zoomObject;
return xAxis;
}
yAxis.BindToZoom = function(zoomObject) {
_zoom = zoomObject;
return yAxis;
}
return (_direction == "x") ? xAxis : yAxis;
}
var data = [{
"a": 1,
"b": 3,
c: 1
}, {
"a": 1,
"b": 3,
c: 2
}, {
"a": 1,
"b": 2,
c: 3
}, {
"a": 1,
"b": 3,
c: 4
}, {
"a": 2,
"b": 3,
c: 5
}, {
"a": 3,
"b": "a",
c: 6
}, {
"a": 1,
"b": "a",
c: 7
}];
X = ["b", "a", "c"];
var axesDOM = d3.select("svg")
.selectAll(".axis")
.data(X).enter()
.append("g").attr("class", "axis");
axesDOM.each(function(x, i) {
d3.select(this).datum(data)
.call(new chartAxis(x, {
width: 200,
offset: 25 + i * 25,
direction: "x"
}));
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg width="200px" height="200px"></svg>