I am trying to add text to a working Marimekko chart. I understand that adding text to a rect requires that the rect and text need to be in a group. But the code I used as a base already uses a group. That is my main question, but I'd also like to make the x axes display the month rather than a %value.
Is this mission impossible for a marimekko chart?
<div id="chart"> </div>
<div id="legend"> </div>
<script>
var width = 800,
height = 500,
margin = 20;
var color = d3.scale.category10();
var x = d3.scale.linear()
.range([0, width - 3 * margin]);
var y = d3.scale.linear()
.range([0, height - 2 * margin]);
var n = d3.format(",d"),
p = d3.format("%");
var svg = d3.select("#chart") .append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + 2 * margin + "," + margin + ")");
d3.json("/mydrupal/sites/default/d3_files/json/marimekko6.json", function(error,data) {
var offset = 0;
// Nest values by month. We assume each month + cause is unique.
var months = d3.nest()
.key(function(d) {
return d.month;
})
.entries(data);
// Compute the total sum, the per-month sum, and the per-cause offset.
// You can use reduce rather than reduceRight to reverse the ordering.
// We also record a reference to the parent cause for each month.
var sum = months.reduce(function(v, p) {
return (p.offset = v) + (p.sum = p.values.reduceRight(function(v, d) {
d.parent = p;
return (d.offset = v) + d.deaths;
}, 0));
}, 0);
// Add x-axis ticks.
var xtick = svg.selectAll(".x")
.data(x.ticks(10))
.enter().append("g")
.attr("class", "x")
.attr("transform", function(d) {
return "translate(" + x(d) + "," + y(1) + ")";
});
xtick.append("line")
.attr("y2", 6)
.style("stroke", "#000");
xtick.append("text")
.attr("y", 8)
.attr("text-anchor", "middle")
.attr("dy", ".71em")
.text(p);
// Add y-axis ticks.
var ytick = svg.selectAll(".y")
.data(y.ticks(10))
.enter().append("g")
.attr("class", "y")
.attr("transform", function(d) {
return "translate(0," + y(1 - d) + ")";
});
ytick.append("line")
.attr("x1", -6)
.style("stroke", "#000");
ytick.append("text")
.attr("x", -8)
.attr("text-anchor", "end")
.attr("dy", ".35em")
.text(p);
// Add a group for each cause.
var months = svg.selectAll(".month")
.data(months)
.enter().append("g")
.attr("class", "month")
.attr("xlink:title", function(d) { return d.key; })
.attr("transform", function(d) {
return "translate(" + x(d.offset / sum) + ")";
});
// Add a rect for each month.
var causes = months.selectAll (".cause")
.data(function(d) { return d.values; })
.enter().append("a")
.attr("class", "month")
.attr("xlink:title", function(d) { return d.cause + " " + d.parent.key + ": " + n(d.deaths); })
.append("rect")
.attr("y", function(d) {
return y(d.offset / d.parent.sum); })
.attr("height", function(d) {
return y(d.deaths / d.parent.sum); })
.attr("width", function(d) {
return x(d.parent.sum / sum); })
.style("fill", function(d) {
return color(d.cause);
});
});
</script>
As noted above:
<!--HTML-->
<p>Marimekko Chart see http://bl.ocks.org/mbostock/1005090</p>
<div id="chart"> </div>
<!--CSS-->
<style type="text/css">body {
font: 10px sans-serif;
}
rect {
stroke: #000;
}
.label {
font-size: 12px;
fill: white;
}
.label2 {
font-size: 14px;
fill: black;
}
svg {
shape-rendering: crispEdges;
}
#chart {
margin-bottom: 20px;
}
</style>
<!--JavaScript-->
<script>
var width = 700,
height = 500,
margin = 20;
var color = d3.scale.category20();
var x = d3.scale.linear()
.range([0, width - 3 * margin]);
var y = d3.scale.linear()
.range([0, height - 2 * margin]);
var n = d3.format(",d"),
p = d3.format("%");
var svg = d3.select("#chart") .append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + 2 * margin + "," + margin + ")");
d3.json("/mydrupal/sites/default/d3_files/json/marimekko6.json", function(error,data) {
var offset = 0;
// Nest values by month. We assume each month + cause is unique.
var months = d3.nest()
.key(function(d) {
return d.month;
})
.entries(data);
// Compute the total sum, the per-month sum, and the per-cause offset.
// You can use reduce rather than reduceRight to reverse the ordering.
// We also record a reference to the parent cause for each month.
var sum = months.reduce(function(v, p) {
return (p.offset = v) + (p.sum = p.values.reduceRight(function(v, d) {
d.parent = p;
return (d.offset = v) + d.deaths;
}, 0));
}, 0);
// Add a group for each cause.
var months = svg.selectAll(".month")
.data(months)
.enter().append("g")
.attr("class", "month")
.attr("xlink:title", function(d) {
return d.key; })
.attr("transform", function(d) {
return "translate(" + x(d.offset / sum) + ")";
});
// Add a rect for each month.
var causes = months.selectAll (".cause")
.data(function(d) {
return d.values; })
.enter().append("a")
.attr("class", "month")
.attr("xlink:title", function(d) {
return d.cause + " " + d.parent.key + ": " + n(d.deaths); });
causes.append("rect")
.attr("y", function(d) {
return y(d.offset / d.parent.sum); })
.attr("height", function(d) {
return y(d.deaths / d.parent.sum); })
.attr("width", function(d) {
return x(d.parent.sum / sum); })
.style("fill", function(d) {
return color(d.cause);
});
// http://stackoverflow.com/questions/17574621/text-on-each-bar-of-a-stacked-bar-chart-d3-js
causes.append("text")
.text(function(d) {
return d.cause + " " + n(d.deaths);})
.attr("x", 5)
.attr("y", function(d) {
return (y(d.offset / d.parent.sum)+20); })
.attr("class", "label")
;
causes.append("text")
.text(function(d) {
return (" Total: " + d.parent.sum);}) // total
.attr("x", 5)
.attr("y", function(d) {
return 450 })
.attr("class", "label2")
;
causes.append("text")
.text(function(d) {
return d.parent.key;}) // month
.attr("x", 5)
.attr("y", function(d) {
return 480; })
.attr("class", "label2")
;
});
</script>
Related
I've got a line chart that needs circles on every other point for column A and C (not for column B). I've struggled to figure out how to do it. This is my line chart without the circles:
date,A=count,A=rank,B=count,B=rank,C=count,C=rank
2016-11-01,60588,213,51915,46,41200,10
2016-12-01,73344,216,58536,47,41230,10
2017-01-01,64164,219,53203,50,51220,12
2017-02-01,85295,224,34047,52,61000,15
2017-03-01,86089,226,44636,54,71200,16
2017-04-01,96871,230,55281,55,71000,10
2017-05-01,97622,234,85879,55,67900,10
I've tried dozens of solutions and I'm very stuck! Here is one of the things I've tried:
linesAndDots.selectAll("line-circle")
.data(data)
.enter().append("circle")
.attr("class", "data-circle")
.attr("r", 5)
.attr("cx", function(d) { return xScale(d.date); })
.attr("cy", function(d) { return yScale(d.measurement); });
But that is giving back NaN for the cx and cy values.
My full code looks like this:
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<style>
path.line-0 {
fill: none;
stroke: #1F77B4;
}
path.line-1 {
fill: none;
stroke: #FF7F0E;
}
</style>
</head>
<!-- Body tag is where we will append our SVG and SVG objects-->
<body>
</body>
<!-- Load in the d3 library -->
<script src="lib/d3.v5.min.js"></script>
<div id="svgdiv"></div>
<script>
//------------------------1. PREPARATION------------------------//
//-----------------------------SVG------------------------------//
var columns=['A=count','B=count'];
var columnsB=['A=rank','B=rank'];
var width = 960;
var height = 500;
var margin = 5;
var padding = 5;
var adj = 75;
// we are appending SVG first
var svg = d3.select("body").append("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "-"
+ adj + " -"
+ adj + " "
+ (width + adj *3) + " "
+ (height + adj*3))
.style("padding", padding)
.style("margin", margin)
.classed("svg-content", true);
//-----------------------------DATA-----------------------------//
var timeConv = d3.timeParse("%Y-%m-%d");
var formatTime = d3.timeFormat("%b %y")
var dataset = d3.csv("toShare.csv");
dataset.then(function(data) {
console.log(data.columns.slice(1))
var slices = columns.map(function(id) {
return {
id: id,
values: data.map(function(d){
return {
date: timeConv(d.date),
measurement: +d[id]
};
})
};
})
//----------------------------SCALES----------------------------//
var xScale = d3.scaleTime().range([0,width]);
var yScale = d3.scaleLinear().rangeRound([height, 0]);
xScale.domain(d3.extent(data, function(d){
return timeConv(d.date)}));
yScale.domain([(0), d3.max(slices, function(c) {
return d3.max(c.values, function(d) {
return d.measurement + 4; });
})
]);
//-----------------------------AXES-----------------------------//
var yaxis = d3.axisLeft()
.ticks(9)
.scale(yScale);
var xaxis = d3.axisBottom()
.ticks(7)
.scale(xScale);
//----------------------------LINES-----------------------------//
var line = d3.line()
.x(function(d) { return xScale(d.date); })
.y(function(d) { return yScale(d.measurement); });
let id = 0;
var ids = function () {
return "line-"+id++;
}
//-------------------------2. DRAWING---------------------------//
//-----------------------------AXES-----------------------------//
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(xaxis)
.append("text")
.attr("transform",
"translate(" + (width/2) + " ," +
50 + ")")
.style("text-anchor", "middle")
.text("Month");
svg.append("g")
.attr("class", "axis")
.call(yaxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - adj)
.attr("x",0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Running Total");
//----------------------------LINES-----------------------------//
var linesAndDots = svg.selectAll("lines")
.data(slices)
.enter()
.append("g");
linesAndDots.append("path")
.attr("class", ids)
.attr("d", function(d) { return line(d.values); });
linesAndDots.selectAll("line-circle")
.data(data)
.enter().append("circle")
.attr("class", "data-circle")
.attr("r", 5)
.attr("cx", function(d) {
console.log("id", id)
return 5; })
.attr("cy", function(d) { return 40; });
//Add the label on the right
linesAndDots.append("text")
.attr("class", ids)
.datum(function(d) {
return {
id: d.id,
value: d.values[d.values.length - 1]}; })
.attr("transform", function(d) {
return "translate(" + (xScale(d.value.date) + 10)
+ "," + (yScale(d.value.measurement) + 5 ) + ")"; })
.attr("x", 5)
.text(function(d) { return d.id.replace("=count", ""); });
});
</script>
</body>
Thanks for the help!
The problem can be inspected by seeing what d really is in the cx and cy function.
cx's problem: you didnt parse the date string into a Date object like you did for the slices data; cy's problem: the data item has no measurement key.
You have used both data and slices. To make the code more consistent, I fix the code of circle drawing using slices too.
linesAndDots
.selectAll(".data-circle")
.data(d=>d.values) // `d` now is the one of the two entries of `slices`,
//and we want to use the `values` array of each entry.
.enter()
.append("circle")
.attr("class", "data-circle")
.attr("r", 5)
.attr("cx", function(d) {
return xScale(d.date);
})
.attr("cy", function(d) {
return yScale(d.measurement)
});
A demo here
I have designed a chart using d3.js and have embedded it in a div within a UIkit framework, however I can't get it to centre within the div.
I have tried putting the svg in a container and adjusting the CSS by putting the svg in a flexbox, removing the margins, aligning it centre, but none of it works. I have tried this response, this one, this one and this one and none of them work. I'm sure it must be something simple - how can I get it to centre? Code below.
Chart
<div id="age_chart" class="uk-container uk-width-medium-1-2 uk-container-center uk-margin-top">
<script>
// chart based on this example: https://bl.ocks.org/63anp3ca/6bafeb64181d87750dbdba78f8678715
// var svg1 = d3.select("svg1"),
var margin = {top: 20, right: 20, bottom: 100, left: 20},
width1 = d3.select("#age_chart").node().getBoundingClientRect().width,
height1 = 400 - margin.top - margin.bottom;
// g = svg1.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// The scale spacing the groups:
var x0 = d3.scaleBand()
.rangeRound([0, width1])
.paddingInner(0.05);
// The scale for spacing each group's bar:
var x1 = d3.scaleBand()
.padding(0.05);
var y = d3.scaleLinear()
.rangeRound([height1, 0]);
var z = d3.scaleOrdinal()
.range(["#2c7fb8", "#7fcdbb"]);
var svg1 = d3.select('body').append("svg")
.attr("width", width1 + margin.left + margin.right)
.attr("height", height1 + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("data/age_distribution.csv", function(d, i, columns) {
for (var i = 1, n = columns.length; i < n; ++i) d[columns[i]] = +d[columns[i]];
return d;
}).then(function(data) {
console.log(data);
var keys = data.columns.slice(1);
console.log('keys');
console.log(keys);
x0.domain(data.map(function(d) { return d.age; }));
x1.domain(keys).rangeRound([0, x0.bandwidth()]);
y.domain([0, d3.max(data, function(d) { return d3.max(keys, function(key) { return d[key]; }); })]).nice();
svg1.append("g")
.selectAll("g")
.data(data)
.enter().append("g")
.attr("class","bar")
.attr("transform", function(d) { return "translate(" + x0(d.age) + ",0)"; })
.selectAll("rect")
.data(function(d) { return keys.map(function(key) { return {key: key, value: d[key]}; }); })
.enter().append("rect")
.attr("x", function(d) { return x1(d.key); })
.attr("y", function(d) { return y(d.value); })
.attr("width", x1.bandwidth())
.attr("height", function(d) { return height1 - y(d.value); })
.attr("fill", function(d) { return z(d.key); });
svg1.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height1 + ")")
.call(d3.axisBottom(x0));
svg1.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(y).ticks(null, "s"))
.append("text")
.attr("x", 2)
.attr("y", y(y.ticks().pop()) + 0.5)
.attr("dy", "0.32em")
.attr("fill", "#000")
.attr("font-size", 11)
.attr("text-anchor", "start")
.text("Percentage of population")
.attr("font-family", "Archivo");
var legend1 = svg1.append("g")
.attr("font-family", "inherit")
.attr("font-size", 11)
.attr("text-anchor", "end")
.selectAll("g")
.data(keys.slice().reverse())
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend1.append("rect")
.attr("x", width1 * .9)
.attr("width", 15)
.attr("height", 15)
.attr("fill", z)
.attr("stroke", z)
.attr("stroke-width",2)
.on("click",function(d) { update(d) });
legend1.append("text")
.attr("x", width1 * .85)
.attr("y", 9.5)
.attr("dy", "0.32em")
.text(function(d) { return d; });
var filtered = [];
////
//// Update and transition on click:
////
function update(d) {
//
// Update the array to filter the chart by:
//
// add the clicked key if not included:
if (filtered.indexOf(d) == -1) {
filtered.push(d);
// if all bars are un-checked, reset:
if(filtered.length == keys.length) filtered = [];
}
// otherwise remove it:
else {
filtered.splice(filtered.indexOf(d), 1);
}
//
// Update the scales for each group's items:
//
var newKeys = [];
keys.forEach(function(d) {
if (filtered.indexOf(d) == -1 ) {
newKeys.push(d);
}
})
x1.domain(newKeys).rangeRound([0, x0.bandwidth()]);
y.domain([0, d3.max(data, function(d) { return d3.max(keys, function(key) { if (filtered.indexOf(key) == -1) return d[key]; }); })]).nice();
// update the y axis:
svg1.select(".y")
.transition()
.call(d3.axisLeft(y).ticks(null, "s"))
.duration(500);
//
// Filter out the bands that need to be hidden:
//
var bars = svg1.selectAll(".bar").selectAll("rect")
.data(function(d) { return keys.map(function(key) { return {key: key, value: d[key]}; }); })
bars.filter(function(d) {
return filtered.indexOf(d.key) > -1;
})
.transition()
.attr("x", function(d) {
return (+d3.select(this).attr("x")) + (+d3.select(this).attr("width"))/2;
})
.attr("height",0)
.attr("width",0)
.attr("y", function(d) { return height1; })
.duration(500);
//
// Adjust the remaining bars:
//
bars.filter(function(d) {
return filtered.indexOf(d.key) == -1;
})
.transition()
.attr("x", function(d) { return x1(d.key); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height1 - y(d.value); })
.attr("width", x1.bandwidth())
.attr("fill", function(d) { return z(d.key); })
.duration(500);
// update legend:
legend1.selectAll("rect")
.transition()
.attr("fill",function(d) {
if (filtered.length) {
if (filtered.indexOf(d) == -1) {
return z(d);
}
else {
return "white";
}
}
else {
return z(d);
}
})
.duration(100);
}
});
</script>
</div>
The problem is that the SVG isn't in your div.
The position of a script element doesn't decide where the SVG will get appended on the page. If you put it at the bottom of the page, you'll get the same results - you need to do so to wait for the DOM to be fully loaded.
d3.select('body').append("svg") will append the svg directly on the body element. To append it to the element you've created, since it has id age_chart, use:
d3.select('#age_chart').append("svg")
I'm able to generate the following graph using D3 areas:
I want to create the following animation. When the webpage loads, you see the first figure. Then, each of the areas morphs into a bar. Finally, I would like to allow users to toggle between the two figures by clicking "B" or "D".
I was able to add the buttons and the corresponding bars to my figure, but I'm having troubles figuring out how to do the animation. This is the code that I have right now:
HTMLWidgets.widget({
name: 'IMposterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
console.log("threshold: ", opts.threshold);
console.log("bars: ", opts.bars);
var margin = {left:50,right:50,top:40,bottom:125};
xMax = d3.max(opts.data, function(d) { return d.x ; });
yMax = d3.max(opts.data, function(d) { return d.y ; });
xMin = d3.min(opts.data, function(d) { return d.x ; });
yMin = d3.min(opts.data, function(d) { return d.y ; });
var y = d3.scaleLinear()
.domain([0,yMax])
.range([height-margin.bottom,0]);
var x = d3.scaleLinear()
.domain([xMin,xMax])
.range([0,width]);
var yAxis = d3.axisLeft(y);
var xAxis = d3.axisBottom(x);
var area = d3.area()
.x(function(d){ return x(d.x) ;})
.y0(height-margin.bottom)
.y1(function(d){ return y(d.y); });
var svg = d3.select(el).append('svg').attr("height","100%").attr("width","100%");
var chartGroup = svg.append("g").attr("transform","translate("+margin.left+","+margin.top+")");
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return d.x< -opts.MME ;})))
.style("fill", opts.colors[0]);
if(opts.MME !==0){
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return (d.x < opts.MME & d.x > -opts.MME) ;})))
.style("fill", opts.colors[1]);
}
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return d.x > opts.MME ;})))
.style("fill", opts.colors[2]);
chartGroup.append("g")
.attr("class","axis x")
.attr("transform","translate(0,"+(height-margin.bottom)+")")
.call(xAxis);
var tooltip = d3.tip()
.attr('class', 'd3-tip chart-data-tip')
.offset([30, 0])
.direction('s')
.html(function(d, i) {
return "<strong>" + d + "</strong> <span style='color:" + "white" + "'>"+ "</span>";
});
svg.call(tooltip);
chartGroup.selectAll("path").data(opts.text).on('mouseover', tooltip.show).on('mouseout', tooltip.hide);
// Bars
var yBar = d3.scaleLinear()
.domain([0,1])
.range([height-margin.bottom,0]);
var xBar = d3.scaleBand()
.domain(opts.bars.map(function(d) { return d.x; }))
.rangeRound([0, width]).padding(0.1);
var yAxisBar = d3.axisLeft(yBar);
var xAxisBar = d3.axisBottom(xBar);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
g.append("g")
.attr("class", "axis axis--x")
.attr("transform","translate(0,"+(height-margin.bottom)+")")
.call(d3.axisBottom(xBar));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(yBar).ticks(10, "%"))
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", "0.71em")
.attr("text-anchor", "end")
.text("Probability");
g.selectAll(".bar")
.data(opts.bars)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return xBar(d.x); })
.attr("y", function(d) { return yBar(d.y); })
.attr("width", xBar.bandwidth())
.style("fill", function(d) { return d.color; })
.attr("height", function(d) { return height - margin.bottom - yBar(d.y); });
// Add buttons
//container for all buttons
var allButtons= svg.append("g")
.attr("id","allButtons");
//fontawesome button labels
var labels= ["B", "D"];
//colors for different button states
var defaultColor= "#E0E0E0";
var hoverColor= "#808080";
var pressedColor= "#000000";
//groups for each button (which will hold a rect and text)
var buttonGroups= allButtons.selectAll("g.button")
.data(labels)
.enter()
.append("g")
.attr("class","button")
.style("cursor","pointer")
.on("click",function(d,i) {
updateButtonColors(d3.select(this), d3.select(this.parentNode));
d3.select("#numberToggle").text(i+1);
})
.on("mouseover", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill",hoverColor);
}
})
.on("mouseout", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill",defaultColor);
}
});
var bWidth= 40; //button width
var bHeight= 25; //button height
var bSpace= 10; //space between buttons
var x0= 20; //x offset
var y0= 10; //y offset
//adding a rect to each toggle button group
//rx and ry give the rect rounded corner
buttonGroups.append("rect")
.attr("class","buttonRect")
.attr("width",bWidth)
.attr("height",bHeight)
.attr("x",function(d,i) {return x0+(bWidth+bSpace)*i;})
.attr("y",y0)
.attr("rx",5) //rx and ry give the buttons rounded corners
.attr("ry",5)
.attr("fill",defaultColor);
//adding text to each toggle button group, centered
//within the toggle button rect
buttonGroups.append("text")
.attr("class","buttonText")
.attr("x",function(d,i) {
return x0 + (bWidth+bSpace)*i + bWidth/2;
})
.attr("y",y0+bHeight/2)
.attr("text-anchor","middle")
.attr("dominant-baseline","central")
.attr("fill","white")
.text(function(d) {return d;});
function updateButtonColors(button, parent) {
parent.selectAll("rect")
.attr("fill",defaultColor);
button.select("rect")
.attr("fill",pressedColor);
}
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});
And this is the figure that that code produces:
This does the trick:
HTMLWidgets.widget({
name: 'IMPosterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
//transition
var transDuration = 1000;
var dataDiscrete = opts.bars.map((b, i) => {
b.y = Number(b.y);
b.desc = opts.text[i];
return b;
});
var distParams = {
min: d3.min(opts.data, d => d.x),
max: d3.max(opts.data, d => d.x)
};
distParams.cuts = [-opts.MME, opts.MME, distParams.max];
opts.data = opts.data.sort((a,b) => a.x - b.x);
var dataContinuousGroups = [];
distParams.cuts.forEach((c, i) => {
let data = opts.data.filter(d => {
if (i === 0) {
return d.x < c;
} else if (i === distParams.cuts.length - 1) {
return d.x > distParams.cuts[i - 1];
} else {
return d.x < c && d.x > distParams.cuts[i - 1];
}
});
data.unshift({x:data[0].x, y:0});
data.push({x:data[data.length - 1].x, y:0});
dataContinuousGroups.push({
color: opts.colors[i],
data: data
});
});
var margin = {
top: 50,
right: 20,
bottom: 80,
left: 70
},
dims = {
width: width - margin.left - margin.right,
height: height - margin.top - margin.bottom
};
var xContinuous = d3.scaleLinear()
.domain([distParams.min - 1, distParams.max + 1])
.range([0, dims.width]);
var xDiscrete = d3.scaleBand()
.domain(dataDiscrete.map(function(d) { return d.x; }))
.rangeRound([0, dims.width]).padding(0.1);
var y = d3.scaleLinear()
.domain([0, 1])
.range([dims.height, 0]);
var svg = d3.select(el).append("svg")
.attr("width", dims.width + margin.left + margin.right)
.attr("height", dims.height + margin.top + margin.bottom);
var g = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var xAxis = d3.axisBottom()
.scale(xDiscrete);
var yAxis = d3.axisLeft()
.scale(y)
.ticks(10)
.tickFormat(d3.format(".0%"));
var yLabel = g.append("text")
.attr("class", "y-axis-label")
.attr("transform", "rotate(-90)")
.attr("y", -52)
.attr("x", -160)
.attr("dy", ".71em")
.style("text-anchor", "end")
.style("font-size", 14 + "px")
.text("Probability");
g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + dims.height + ")")
.call(xAxis);
g.append("g")
.attr("class", "y axis")
.call(yAxis);
var areas = g.selectAll(".area")
.data(dataDiscrete)
.enter().append("path")
.attr("class", "area")
.style("fill", function(d) { return d.color; })
.attr("d", function(d, i) {
let numPts = dataContinuousGroups[i].data.length - 2;
var path = d3.path()
path.moveTo(xDiscrete(d.x), y(0));
for (j=0; j<numPts; j++) {
path.lineTo(xDiscrete(d.x) + j*xDiscrete.bandwidth()/(numPts-1), y(d.y))
}
path.lineTo(xDiscrete(d.x) + xDiscrete.bandwidth(), y(0));
return path.toString();
});
var tooltip = d3.tip()
.attr('class', 'd3-tip chart-data-tip')
.offset([30, 0])
.direction('s')
.html(function(d, i) {
return "<span>" + dataDiscrete[i].desc + "</span>";
});
g.call(tooltip);
areas
.on('mouseover', tooltip.show)
.on('mouseout', tooltip.hide);
var thresholdLine = g.append("line")
.attr("stroke", "black")
.style("stroke-width", "1.5px")
.style("stroke-dasharray", "5,5")
.style("opacity", 1)
.attr("x1", 0)
.attr("y1", y(opts.threshold))
.attr("x2", dims.width)
.attr("y2", y(opts.threshold));
var updateXAxis = function(type, duration) {
if (type === "continuous") {
xAxis.scale(xContinuous);
} else {
xAxis.scale(xDiscrete);
}
d3.select(".x").transition().duration(duration).call(xAxis);
};
var updateYAxis = function(data, duration) {
var extent = d3.extent(data, function(d) {
return d.y;
});
extent[0] = 0;
extent[1] = extent[1] + 0.2*(extent[1] - extent[0]);
y.domain(extent);
d3.select(".y").transition().duration(duration).call(yAxis);
};
var toggle = function(to, duration) {
if (to === "distribution") {
updateYAxis(dataContinuousGroups[0].data.concat(dataContinuousGroups[1].data).concat(dataContinuousGroups[2].data), 0);
updateXAxis("continuous", duration);
areas
.data(dataContinuousGroups)
.transition()
.duration(duration)
.attr("d", function(d) {
var gen = d3.line()
.x(function(p) {
return xContinuous(p.x);
})
.y(function(p) {
return y(p.y);
});
return gen(d.data);
});
thresholdLine
.style("opacity", 0);
g.select(".y.axis")
.style("opacity", 0);
g.select(".y-axis-label")
.style("opacity", 0);
} else {
y.domain([0, 1]);
d3.select(".y").transition().duration(duration).call(yAxis);
updateXAxis("discrete", duration);
areas
.data(dataDiscrete)
.transition()
.duration(duration)
.attr("d", function(d, i) {
let numPts = dataContinuousGroups[i].data.length - 2;
var path = d3.path()
path.moveTo(xDiscrete(d.x), y(0));
for (j=0; j<numPts; j++) {
path.lineTo(xDiscrete(d.x) + j*xDiscrete.bandwidth()/(numPts-1), y(d.y))
}
path.lineTo(xDiscrete(d.x) + xDiscrete.bandwidth(), y(0));
return path.toString();
});
thresholdLine
.transition()
.duration(0)
.delay(duration)
.style("opacity", 1)
.attr("y1", y(opts.threshold))
.attr("y2", y(opts.threshold));
g.select(".y.axis")
.transition()
.duration(0)
.delay(duration)
.style("opacity", 1);
g.select(".y-axis-label")
.transition()
.duration(0)
.delay(duration)
.style("opacity", 1);
}
};
// Add buttons
//container for all buttons
var allButtons = svg.append("g")
.attr("id", "allButtons");
//fontawesome button labels
var labels = ["B", "D"];
//colors for different button states
var defaultColor = "#E0E0E0";
var hoverColor = "#808080";
var pressedColor = "#000000";
//groups for each button (which will hold a rect and text)
var buttonGroups = allButtons.selectAll("g.button")
.data(labels)
.enter()
.append("g")
.attr("class", "button")
.style("cursor", "pointer")
.on("click", function(d, i) {
updateButtonColors(d3.select(this), d3.select(this.parentNode));
d3.select("#numberToggle").text(i + 1);
if (d === "D") {
toggle("distribution", transDuration);
} else {
toggle("discrete", transDuration);
}
})
.on("mouseover", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill", hoverColor);
}
})
.on("mouseout", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill", defaultColor);
}
});
var bWidth = 40; //button width
var bHeight = 25; //button height
var bSpace = 10; //space between buttons
var x0 = 20; //x offset
var y0 = 10; //y offset
//adding a rect to each toggle button group
//rx and ry give the rect rounded corner
buttonGroups.append("rect")
.attr("class", "buttonRect")
.attr("width", bWidth)
.attr("height", bHeight)
.attr("x", function(d, i) {
return x0 + (bWidth + bSpace) * i;
})
.attr("y", y0)
.attr("rx", 5) //rx and ry give the buttons rounded corners
.attr("ry", 5)
.attr("fill", defaultColor);
//adding text to each toggle button group, centered
//within the toggle button rect
buttonGroups.append("text")
.attr("class", "buttonText")
.attr("x", function(d, i) {
return x0 + (bWidth + bSpace) * i + bWidth / 2;
})
.attr("y", y0 + bHeight / 2)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "central")
.attr("fill", "white")
.text(function(d) {
return d;
});
function updateButtonColors(button, parent) {
parent.selectAll("rect")
.attr("fill", defaultColor);
button.select("rect")
.attr("fill", pressedColor);
}
toggle("distribution", 0);
setTimeout(() => {
toggle("discrete", transDuration);
}, 1000);
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});
The code below displays marker-ends on arrows/paths/lines as intended, but the color of the marker-end does not vary by line (i.e., it is always the same orange color, not the color of its respective line). I think the code is defaulting to the color assigned to the first field of my data(?). Any advice would be appreciated.
<script src="http://www.protobi.com/javascripts/d3.v3.min.js"></script>
<script src="http://labratrevenge.com/d3-tip/javascripts/d3.tip.v0.6.3.js"></script>
<script src="http://www.protobi.com/examples/pca/pca.js"></script>
<script type="text/javascript">
var margin = {top: 20, right: 20, bottom: 20, left: 20};
var width = 1500 - margin.left - margin.right;
var height = 1500 - margin.top - margin.bottom;
var angle = Math.PI * 0;
var color = d3.scale.category10();
var x = d3.scale.linear().range([width, 0]); // switch to match how R biplot shows it
var y = d3.scale.linear().range([height, 0]);
x.domain([-3.5,3.5]).nice()
y.domain([-3.5,3.5]).nice()
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("/brand.csv", function(error, data) {
var matrix = data.map(function(d){
return d3.values(d).slice(1,d.length).map(parseFloat);
});
var pca = new PCA();
matrix = pca.scale(matrix,true,true);
pc = pca.pca(matrix,2)
var A = pc[0]; // this is the U matrix from SVD
var B = pc[1]; // this is the dV matrix from SVD
var brand_names = Object.keys(data[0]); // first row of data file ["ATTRIBUTE", "BRAND A", "BRAND B", "BRAND C", ...]
brand_names.shift(); // drop the first column label, e.g. "ATTRIBUTE"
data.map(function(d,i){
label: d.ATTRIBUTE,
d.pc1 = A[i][0];
d.pc2 = A[i][1];
});
var label_offset = {
"Early/First line": 20,
"Unfamiliar":10,
"Convenient": -5
}
var brands = brand_names
.map(function(key, i) {
return {
brand: key,
pc1: B[i][0]*4,
pc2: B[i][1]*4
}
});
function rotate(x,y, dtheta) {
var r = Math.sqrt(x*x + y*y);
var theta = Math.atan(y/x);
if (x<0) theta += Math.PI;
return {
x: r * Math.cos(theta + dtheta),
y: r * Math.sin(theta + dtheta)
}
}
data.map(function(d) {
var xy = rotate(d.pc1, d.pc2, angle);
d.pc1 = xy.x;
d.pc2 = xy.y;
});
brands.map(function(d) {
var xy = rotate(d.pc1, d.pc2, angle);
d.pc1 = xy.x;
d.pc2 = xy.y;
});
var showAxis = false; // normally we don't want to see the axis in PCA, it's meaningless
if (showAxis) {
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text("PC1");
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("PC2");
}
var legend = svg.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 10.5)
.attr("cx", function(d) { return x(d.pc1); })
.attr("cy", function(d) { return y(d.pc2); })
.style("fill", function(d) { return color(d['species']); })
.on('mouseover', onMouseOverAttribute)
.on('mouseleave', onMouseLeave);
svg.selectAll("text.brand")
.data(brands)
.enter().append("text")
.attr("class", "label-brand")
.attr("x", function(d) { return x(d.pc1) + 10; })
.attr("y", function(d) { return y(d.pc2) + 0; })
.text(function(d) { return d['brand']})
svg.selectAll("marker.brand")
.data(brands)
.enter().append("svg:marker")
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 6)
.attr('markerWidth', 10)
.attr('markerHeight', 10)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.style("fill", function(d) { return color(d['brand']); });
svg.selectAll(".line")
.data(brands)
.enter().append("line")
.attr("class", "square")
.attr('x1', function(d) { return x(-d.pc1);})
.attr('y1', function(d) { return y(-d.pc2); })
.attr("x2", function(d) { return x(d.pc1); })
.attr("y2", function(d) { return y(d.pc2); })
.style("stroke", function(d) { return color(d['brand']); })
.style('marker-end', "url(#end-arrow)")
.on('mouseover', onMouseOverBrand)
.on('mouseleave', onMouseLeave);
svg.selectAll("text.attr")
.data(data)
.enter().append("text")
.attr("class", "label-attr")
.attr("x", function(d,i ) { return x(d.pc1)+4 ; })
.attr("y", function(d ,i) { return y(d.pc2) + (label_offset[d.ATTRIBUTE]||0); })
.text(function(d,i) { return d.ATTRIBUTE})
var pctFmt = d3.format('0%')
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([10, 20])
.direction('e')
.html(function(values,title) {
var str =''
str += '<h3>' + (title.length==1 ? 'Brand ' : '' )+ title + '</h3>'
str += "<table>";
for (var i=0; i<values.length; i++) {
if (values[i].key != 'ATTRIBUTE' && values[i].key != 'pc1' && values[i].key != 'pc2') {
str += "<tr>";
str += "<td>" + values[i].key + "</td>";
str += "<td class=pct>" + pctFmt(values[i].value) + "</td>";
str + "</tr>";
}
}
str += "</table>";
return str;
});
svg.call(tip);
function getSpPoint(A,B,C){
var x1=A.x, y1=A.y, x2=B.x, y2=B.y, x3=C.x, y3=C.y;
var px = x2-x1, py = y2-y1, dAB = px*px + py*py;
var u = ((x3 - x1) * px + (y3 - y1) * py) / dAB;
var x = x1 + u * px, y = y1 + u * py;
return {x:x, y:y}; //this is D
}
// draw line from the attribute a perpendicular to each brand b
function onMouseOverAttribute(a,j) {
brands.forEach(function(b, idx) {
var A = { x: 0, y:0 };
var B = { x: b.pc1, y: b.pc2 };
var C = { x: a.pc1, y: a.pc2 };
b.D = getSpPoint(A,B,C);
});
svg.selectAll('.tracer')
.data(brands)
.enter()
.append('line')
.attr('class', 'tracer')
.attr('x1', function(b,i) { return x(a.pc1); return x1; })
.attr('y1', function(b,i) { return y(a.pc2); return y1; })
.attr('x2', function(b,i) { return x(b.D.x); return x2; })
.attr('y2', function(b,i) { return y(b.D.y); return y2; })
.style("stroke", function(d) { return "#aaa"});
delete a.D;
var tipText = d3.entries(a);
tip.show(tipText, a.ATTRIBUTE);
};
// draw line from the brand axis a perpendicular to each attribute b
function onMouseOverBrand(b,j) {
data.forEach(function(a, idx) {
var A = { x: 0, y:0 };
var B = { x: b.pc1, y: b.pc2 };
var C = { x: a.pc1, y: a.pc2 };
a.D = getSpPoint(A,B,C);
});
svg.selectAll('.tracer')
.data(data)
.enter()
.append('line')
.attr('class', 'tracer')
.attr('x1', function(a,i) { return x(a.D.x); })
.attr('y1', function(a,i) { return y(a.D.y); })
.attr('x2', function(a,i) { return x(a.pc1); })
.attr('y2', function(a,i) { return y(a.pc2); })
.style("stroke", function(d) { return "#aaa"});
var tipText = data.map(function(d) {
return {key: d.ATTRIBUTE, value: d[b['brand']] }
})
tip.show(tipText, b.brand);
};
function onMouseLeave(b,j) {
svg.selectAll('.tracer').remove()
tip.hide();
}
});
While you are creating an svg:marker for each line, you give them all the same id. When they are then used on your line elements, since they all have the same id you are only using one of them.
Simple fix, give them unique ids:
svg.selectAll("marker.brand")
.data(brands)
.enter().append("svg:marker")
.attr('id', function(d,i){
return 'end-arrow' + i; //<-- append index postion
})
...
svg.selectAll(".line")
.data(brands)
.enter().append("line")
.attr("class", "square")
.attr('x1', function(d) {
return x(-d.pc1);
})
.attr('y1', function(d) {
return y(-d.pc2);
})
.attr("x2", function(d) {
return x(d.pc1);
})
.attr("y2", function(d) {
return y(d.pc2);
})
.style("stroke", function(d) {
return color(d['brand']);
})
.style('marker-end', function(d,i){
return "url(#end-arrow"+i+")"; //<--use the one with the right id
})
....
Example here.
I have a Marimekko chart where the bars are aligned vertically (with a limited number of bars).
However, the eventual chart will have so many bars that it would be better with a horizontal layout which would support a larger number of values.
I've tried to modify the chart by reversing x and y values but the result does not work properly. I want the first month in the data to appear at the top of the chart.The working code with vertical bars (no data) is below and here.
var width = 700,
height = 500,
margin = 20;
var color = d3.scale.category20();
var x = d3.scale.linear()
.range([0, width - 3 * margin]);
var y = d3.scale.linear()
.range([0, height - 2 * margin]);
var n = d3.format(",d"),
p = d3.format("%");
var svg = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + 2 * margin + "," + margin + ")");
d3.json("/mydrupal/sites/default/d3_files/json/marimekko6.json",
function(error,data) {
var offset = 0;
// Nest values by month. We assume each month + cause is unique.
var months = d3.nest()
.key(function(d) {
return d.month;
})
.entries(data);
// Compute the total sum, the per-month sum, and the per-cause offset.
// You can use reduce rather than reduceRight to reverse the ordering.
// We also record a reference to the parent cause for each month.
var sum = months.reduce(function(v, p) {
return (p.offset=v) + (p.sum=p.values.reduceRight(function(v, d) {
d.parent = p;
return (d.offset = v) + d.deaths;
}, 0));
}, 0);
// Add a group for each cause.
var months = svg.selectAll(".month")
.data(months)
.enter()
.append("g")
.attr("class", "month")
.attr("xlink:title", function(d) {
return d.key;
})
.attr("transform", function(d) {
return "translate(" + x(d.offset / sum) + ")";
});
// Add a rect for each month.
var causes = months.selectAll (".cause")
.data(function(d) {
return d.values;
})
.enter()
.append("a")
.attr("class", "month")
.attr("xlink:title", function(d) {
return d.cause + " " + d.parent.key + ": " + n(d.deaths);
});
causes.append("rect")
.attr("y", function(d) {
return y(d.offset / d.parent.sum);
})
.attr("height", function(d) {
return y(d.deaths / d.parent.sum);
})
.attr("width", function(d) {
return x(d.parent.sum / sum);
})
.style("fill", function(d) {
return color(d.cause);
});
// see http://stackoverflow.com/questions/17574621/
// text-on-each-bar-of-a-stacked-bar-chart-d3-js
causes.append("text")
.text(function(d) {
return d.cause + " " + n(d.deaths);
})
.attr("x", 5)
.attr("y", function(d) {
return (y(d.offset / d.parent.sum)+20);
})
.attr("class", "label");
causes.append("text")
.text(function(d) {
return (" Total: " + d.parent.sum);
}) // total
.attr("x", 5)
.attr("y", function(d) {
return 450;
})
.attr("class", "label2");
causes.append("text")
.text(function(d) {
return d.parent.key;
}) // month
.attr("x", 5)
.attr("y", function(d) {
return 480;
})
.attr("class", "label2");
});
Here's the fix. Basically you need to change x and y as well as width and the height.
// Add a group for each cause.
var months = svg.selectAll(".month")
.data(months)
.enter().append("g")
.attr("class", "month")
.attr("xlink:title", function(d) {
return d.key; })
.attr("transform", function(d) {
return "translate(0," + y(d.offset / sum) + ")";
});
// Add a rect for each month.
var causes = months.selectAll (".cause")
.data(function(d) {
return d.values; })
.enter().append("a")
.attr("class", "month")
.attr("xlink:title", function(d) {
return d.cause + " " + d.parent.key + ": " + n(d.deaths); });
causes.append("rect")
.attr("x", function(d) {
return x(d.offset / d.parent.sum); })
.attr("width", function(d) {
return x(d.deaths / d.parent.sum); })
.attr("height", function(d) {
return y(d.parent.sum / sum); })
.style("fill", function(d) {
return color(d.cause);
});