This demo inspired by z-m-k, where I have make it as a reuseable modular and fix a trival statistics problem.
You guys can generate many effects with the interpolation attribut, while I think step-after is most appropriate interpolation from the statistics perspective.
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v3.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
</style>
</head>
<body>
<div id="container"></div>
<!-- Loading data -->
<script src="data.js"></script>
<script>
d3violin('container', {data: violin});
function d3violin(ele, content){
var o = {
title: "Genes expression distribution in samples",
margin: {top: 50, right: 20, bottom: 40, left: 40},
padding: 0.2,
size: {width: 600, height: 400},
bins: 20,
// linear, step, step-before, step-after, basis,
// basis-open, cardinal, cardinal-open, monotone
// only "step-after" is appropriate from the statistics perspective.
interpolate: 'step-after',
data: [],
};
Object.keys(content).map(function(e){
if(e in o){
o[e] = content[e];
}
})
if(!o.data.length){
throw new Error('d3violin: data is EMPTY!');
}
var plotWidth = o.size.width - o.margin.left - o.margin.right,
plotHeight = o.size.height - o.margin.top - o.margin.bottom;
// data proccessing
var groupQty = o.data.length,
maxValue = Math.ceil(
d3.max(o.data, function(e, i){
return d3.max(e.data);
})
),
minValue = Math.floor(
d3.min(o.data, function(e, i){
return d3.min(e.data);
})
);
// define scale and axis
var xScale = d3.scale.ordinal()
.domain(o.data.map(function(e){return e.name;}))
.rangeBands([0, plotWidth], o.padding, o.padding),
yScale = d3.scale.linear()
.domain([minValue, maxValue])
.range([0, plotHeight]),
xAxis = d3.svg.axis()
.orient('bottom')
.scale(xScale),
yAxis = d3.svg.axis()
.orient('left')
.scale(yScale.copy().domain([maxValue, minValue]));
// create svg and divided into three parts
var svg = d3.select("#" + ele)
.append('svg')
.attr({
width : o.size.width,
height : o.size.height,
class : "svg-violin",
"font-size" : 12,
"font-family" : "Arial",
}),
plotMain = svg.append('g')
.attr('class', 'plot-main')
.attr('transform', 'translate(' +
[o.margin.left, o.margin.top] + ')');
svg.append('g')
.attr({
transform: 'translate(' +[o.margin.left, o.margin.top] + ')',
class: 'axis y-axis',
})
.call(yAxis)
.selectAll('path, line')
.attr({
fill: 'none',
stroke: '#000',
'stroke-width': 1
});
svg.append('g')
.attr({
transform: 'translate(' +
[o.margin.left, o.margin.top + plotHeight] + ')',
class: 'axis x-axis',
})
.call(xAxis)
.selectAll('path, line')
.attr({
fill: 'none',
stroke: 'transparent'
});
// prepare for drawing
var boxWidth = xScale.rangeBand();
var violinGroups = plotMain.selectAll('g.violin-group')
.data(o.data)
.enter()
.append('g')
.each(function(d,i){
d3.select(this).attr({
class: "violin-group",
transform: 'translate(' + [
xScale.range()[i] + 0.5 * boxWidth,
plotHeight
] + ')',
})
});
function addViolin(selection, hData, hArea, hline){
var halfViolin = selection.append('g')
.attr('fill', function(d){return d.color;});
halfViolin.append('path')
.datum(hData)
.attr('class', 'path-area')
.attr('d', hArea);
halfViolin.append('path')
.datum(hData)
.attr('d', hline)
.attr('class', 'path-line')
.attr('fill', 'none');
var cloneNode = halfViolin.node().cloneNode(true);
cloneNode = d3.select(selection.node().insertBefore(cloneNode, null));
cloneNode.attr('transform', 'rotate(-90) scale(1,-1)')
halfViolin.attr('transform', 'rotate(-90)')
}
function addPlotBox(selection, hData, boxWidth) {
var plotBox = selection.append('g')
.attr('class', 'violin-plotbox');
// Quantile Probility
// https://en.wikipedia.org/wiki/Quantile
var probs = [0.05, 0.25, 0.5, 0.75, 0.95];
probs = probs.map(function(e){
return yScale(d3.quantile(hData, e));
});
var mean = yScale(d3.mean(hData));
var width = Math.max(10, boxWidth * 0.25);
// add two whisters and a center line
plotBox.append('line')
.attr({
x1 : -0.5 * width,
x2 : 0.5 * width,
y1 : -probs[4],
y2 : -probs[4],
class : 'plot-box-line'
});
plotBox.append('line')
.attr({
x1 : -0.5 * width,
x2 : 0.5 * width,
y1 : -probs[0],
y2 : -probs[0],
class : 'plot-box-line'
});
plotBox.append('line')
.attr({
x1 : 0,
x2 : 0,
y1 : -probs[0],
y2 : -probs[4],
class : 'plot-box-line'
});
// median line, box and mean circles
plotBox.append('rect')
.attr({
x : -0.5 * width,
y : -probs[3],
width : width,
height : probs[3] - probs[1],
stroke : '#000',
'stroke-width': 1,
fill : '#000'
});
plotBox.append('line')
.attr({
x1 : -0.5 * width,
x2 : 0.5 * width,
y1 : -probs[2],
y2 : -probs[2],
'stroke-width': 2,
stroke : '#fff'
});
plotBox.append('circle')
.attr({
cx : 0,
cy : -mean,
r : Math.max(4, 0.4 * width),
fill: '#fff'
});
plotBox.append('circle')
.attr({
cx : 0,
cy : -mean,
r : Math.max(4, 0.4 * width) - 2,
fill: '#000'
});
// line attributs settings
plotBox.selectAll('line.plot-box-line').attr({
fill: 'none',
stroke: '#000',
'stroke-width': 1
});
}
for(var i = 0; i < o.data.length; i++){
var histogram = d3.layout.histogram()
.bins(o.bins);
o.data[i].data = o.data[i].data.sort(d3.ascending);
var histData = histogram(o.data[i].data),
xBoxScale = d3.scale.linear()
.domain([0, d3.max(histData.map(function(e){return e.y}))])
.range([0, 0.5 * boxWidth]),
area = d3.svg.area()
.x(function(d){return yScale(d.x)})
.y(function(d){return xBoxScale(d.y)})
.interpolate(o.interpolate)
.y0(0),
line = d3.svg.line()
.x(function(d){return yScale(d.x)})
.y(function(d){return xBoxScale(d.y)})
.interpolate(o.interpolate);
addViolin(d3.select(violinGroups[0][i]), histData, area, line);
addPlotBox(d3.select(violinGroups[0][i]), o.data[i].data, boxWidth);
}
if(o.title){
svg.append('text')
.attr({
x: 0.5 * o.size.width,
y: 25,
'text-anchor': 'middle',
})
.style('font-size', '20px')
.text(o.title)
}
}
</script>
</body>
https://d3js.org/d3.v3.min.js