Apologies for the newbie question here. I'm trying to reproduce this example https://bl.ocks.org/mbostock/3884955 with some data on online influencers (essentially drawing many lines) but with an scroll zoom in order to isolate parts of the data. Sounds simple enough, but I can't seem to get it to function!
< svg width = "960"
height = "500" > < /svg>
<script src="/ / d3js.org / d3.v4.min.js "></script>
<script>
var svg = d3.select("svg"),
margin = {top: 20, right: 80, bottom: 30, left: 100},
width = svg.attr("width") - margin.left - margin.right,
height = svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform ", "translate(" + margin.left +
", " + margin.top + ")");
var zoom = d3.zoom()
.scaleExtent([1, 32])
.translateExtent([[0, 0], [width, height]])
.extent([[0, 0], [width, height]])
.on("zoom", zoomed);
var xAxis = d3.axisBottom(x),
yAxis = d3.axisLeft(y);
var parseTime = d3.timeParse(" % d / % m / % Y ");
var x = d3.scaleTime().range([0, width]),
y = d3.scaleLinear().range([height, 0]),
z = d3.scaleOrdinal(d3.schemeCategory10);
var line = d3.line()
.curve(d3.curveBasis)
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.engagement); });
d3.csv("stack2.csv", type, function(error, data) {
if (error) throw error;
var influencers = data.columns.slice(1).map(function(id) {
return {
id: id,
values: data.map(function(d) {
return {date: d.date, engagement: d[id]};
})
};
});
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([d3.min(influencers, function(c) {
return d3.min(c.values, function(d) {
return d.engagement; }); }),
d3.max(influencers, function(c) {
return d3.max(c.values, function(d) {
return d.engagement; }); })
]);
z.domain(influencers.map(function(c) { return c.id; }));
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0, " + height + ")")
.call(d3.axisBottom(x));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y))
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 5)
.attr("dy", "0.71em")
.attr("fill", "#000")
.text("Engagement");
var influ = g.selectAll(".influ")
.data(influencers)
.enter().append("g")
.attr("class", "influ");
influ.append("path")
.attr("class", "line")
.attr("d", function(d) {
return line(d.values);
})
.style("stroke", "lightgrey")
.on("mouseover", function(d, i) {
d3.select(this).transition()
.style("stroke", function(d) {
return z(d.id);
})
.style("stroke-width", 3)
console.log(d.id);
})
.on("mouseout", function(d) {
d3.select(this).transition()
.style("stroke", "lightgrey")
.style("stroke-width", 1)
})
influ.append("text")
.datum(function(d) {
return {
id: d.id,
value: d.values[d.values.length - 1]
};
})
.attr("transform", function(d) {
return "translate(" + x(d.value.date) + "," + y(d.value.engagement) + ")";
})
.attr("x", 3)
.attr("dy", ".35em")
.style("opacity", 0.7)
.style("font", "10px sans-serif")
.text(function(d) {
return d.id;
});
svg.call(zoom)
});
function type(d, _, columns) {
d.date = parseTime(d.date);
for (var i = 1, n = columns.length, c; i < n; ++i) d[c = columns[i]] = +d[c];
return d;
}
function zoomed() {
var t = d3.event.transform,
xt = t.rescaleX(x);
g.selectAll("path.line").attr("d", function(d) {
return xt(d.date);
});
g.select(".axis--x").call(xAxis.scale(xt));
}
< /script>
I think the problem is isolated to this part:
function zoomed() {
var t = d3.event.transform,
xt = t.rescaleX(x);
g.selectAll("path.line").attr("d", function(d) {
return xt(d.date);
});
g.select(".axis--x").call(xAxis.scale(xt));
}
The Axis zoom is fine, but i don't think I'm calling the data for the line graphs correctly. I think g.selectAll definitely selects the lines, as they disappear on scroll... so I am assuming that .attr("d", function(d) {return xt(d.date); is wrong. Anyone have any tips?
I use something along these lines in an interactive line chart D3 plot that I wrote a couple of days back. I've commented extensively, so it should be pretty self explanatory. This works really well and has been bug free.
function zoomed() {
lastEventTransform = d3.event.transform;
// Rescale axes using current zoom transform
gX.call(xAxis.scale(lastEventTransform.rescaleX(x)));
gY.call(yAxis.scale(lastEventTransform.rescaleY(y)));
// Create new scales that incorporate current zoom transform on original scale
var xt = lastEventTransform.rescaleX(x),
yt = lastEventTransform.rescaleY(y);
// Apply new scale to create new definition of d3.line method for path drawing of line plots
var line = d3.line()
.x(function(d) { return xt(d.x); })
.y(function(d) { return yt(d.y); });
// Update all line-plot elements with new line method that incorporates transform
innerSvg.selectAll(".line")
.attr("d", function(d) { return line(d.values); });
// Update any scatter points if you are also plotting them
innerSvg.selectAll(".dot")
.attr("cx", function(d) {return xt(d.x); })
.attr("cy", function(d) {return yt(d.y); });
}
Note: gX and gY are just the d3 g group elements that had the axes originally called on them during setup of the plot.
This Zoom function tricks the domain to change and then redraws the lines on the new domain. Its the best i can manage for now... but hardly elegant!
function zoomed() {
var t = d3.event.transform;
// var t = d3.event.scaleBy;
var xt = t.rescaleY(y);
domain = yAxis.scale().domain();
g.select(".axis--y").call(yAxis.scale(xt));
g.selectAll("path.line").attr("d", function(d) {
if ( d3.max(d.values.map(function(dd) { return dd.engagement } )) > domain[1] ) {
return null
}
else {
return line(d.values)
}
});
if (domain[1] > 50000000) {
max = 50000000
} else if ( domain[1] < 500 ) {
max = 500
} else {
max = domain[1]
}
y.domain([0, max]);
}
It zooms alright but it is prone to de-coupling the axis from what the lines are saying. If anyone has something better let me know!
Related
I adapted a multi-line chart which has a legend and axis and displays correctly on the bl.ocks.org site (http://bl.ocks.org/Matthew-Weber/5645518). The legend reorganizes itself when you select a different type from the drop down field. On my adaptation when the legend reorganizes itself the items start to overlap each other when some types are selected. Also the axes draw on top of each other. The original code uses tipsy but I have not checked it.
// original author's code http://bl.ocks.org/Matthew-Weber/5645518;
//set the margins
var margin = {
top: 50,
right: 160,
bottom: 80,
left: 50
},
width = 900 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
//set dek and head to be as wide as SVG
d3.select('#dek')
.style('width', width + 'px');
d3.select('#headline')
.style('width', width + 'px');
//write out your source text here
var sourcetext = "xxx";
// set the type of number here, n is a number with a comma, .2% will get you a percent, .2f will get you 2 decimal points
var NumbType = d3.format(",");
// color array
var bluescale4 = ["red", "blue", "green", "orange", "purple"];
//color function pulls from array of colors stored in color.js
var color = d3.scale.ordinal().range(bluescale4);
//defines a function to be used to append the title to the tooltip.
var maketip = function(d) {
var tip = '<p class="tip3">' + d.name + '<p class="tip1">' + NumbType(d.value) + '</p> <p class="tip3">' + formatDate(d.date) + '</p>';
return tip;
}
//define your year format here, first for the x scale, then if the date is displayed in tooltips
var parseDate = d3.time.format("%Y-%m-%d").parse;
var formatDate = d3.time.format("%b %d, '%y");
//create an SVG
var svg = d3.select("#graphic").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 + ")");
//make a rectangle so there is something to click on
svg.append("svg:rect")
.attr("width", width)
.attr("height", height)
.attr("class", "plot"); //#fff
// force data to update when menu is changed
var menu = d3.select("#menu select")
.on("change", change);
//suck in the data, store it in a value called formatted, run the redraw function
d3.csv("/sites/default/d3_files/d3-provinces/statistics-april-15-2.csv", function(data) {
formatted = data;
redraw();
});
d3.select(window)
.on("keydown", function() {
altKey = d3.event.altKey;
})
.on("keyup", function() {
altKey = false;
});
var altKey;
// set terms of transition that will take place
// when a new type (Death etc.)indicator is chosen
function change() {
d3.transition()
.duration(altKey ? 7500 : 1500)
.each(redraw);
} // end change
// REDRAW all the meat goes in the redraw function
function redraw() {
// create data nests based on type indicator (series)
var nested = d3.nest()
.key(function(d) {
return d.type;
})
.map(formatted)
// get value from menu selection
// the option values are set in HTML and correspond
//to the [type] value we used to nest the data
var series = menu.property("value");
// only retrieve data from the selected series, using the nest we just created
var data = nested[series];
// for object constancy we will need to set "keys", one for each type of data (column name) exclude all others.
color.domain(d3.keys(data[0]).filter(function(key) {
return (key !== "date" && key !== "type");
}));
var linedata = color.domain().map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {
name: name,
date: parseDate(d.date),
value: parseFloat(d[name], 10)
};
})
};
});
//make an empty variable to stash the last values into so we can sort the legend // do we need to sort it?
var lastvalues = [];
//setup the x and y scales
var x = d3.time.scale()
.domain([
d3.min(linedata, function(c) {
return d3.min(c.values, function(v) {
return v.date;
});
}),
d3.max(linedata, function(c) {
return d3.max(c.values, function(v) {
return v.date;
});
})
])
.range([0, width]);
var y = d3.scale.linear()
.domain([
d3.min(linedata, function(c) {
return d3.min(c.values, function(v) {
return v.value;
});
}),
d3.max(linedata, function(c) {
return d3.max(c.values, function(v) {
return v.value;
});
})
])
.range([height, 0]);
//will draw the line
var line = d3.svg.line()
.x(function(d) {
return x(d.date);
})
.y(function(d) {
return y(d.value);
});
//create and draw the x axis - need to clear the existing axis
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickPadding(8)
.ticks(10);
//create and draw the y axis
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickSize(0 - width)
.tickPadding(8);
svg.append("svg:g")
.attr("class", "x axis");
svg.append("svg:g")
.attr("class", "y axis")
.attr("transform", "translate(" + (0) + ",0)")
.call(yAxis);
//bind the data
var thegraph = svg.selectAll(".thegraph")
.data(linedata)
//append a g tag for each line and set of tooltip circles and give it a unique ID based on the column name of the data
var thegraphEnter = thegraph.enter().append("g")
.attr("class", "thegraph")
.attr('id', function(d) {
return d.name + "-line";
})
.style("stroke-width", 2.5)
.on("mouseover", function(d) {
d3.select(this) //on mouseover of each line, give it a nice thick stroke // works
.style("stroke-width", '6px');
var selectthegraphs = $('.thegraph').not(this); //select all the rest of the lines, except the one you are hovering on and drop their opacity
d3.selectAll(selectthegraphs)
.style("opacity", 0.2);
var getname = document.getElementById(d.name); //use get element cause the ID names have spaces in them - not working
var selectlegend = $('.legend').not(getname); //grab all the legend items that match the line you are on, except the one you are hovering on
d3.selectAll(selectlegend) // drop opacity on other legend names
.style("opacity", .2);
d3.select(getname)
.attr("class", "legend-select"); //change the class on the legend name that corresponds to hovered line to be bolder
}) // end of mouseover
.on("mouseout", function(d) { //undo everything on the mouseout
d3.select(this)
.style("stroke-width", '2.5px');
var selectthegraphs = $('.thegraph').not(this);
d3.selectAll(selectthegraphs)
.style("opacity", 1);
var getname = document.getElementById(d.name);
var getname2 = $('.legend[fakeclass="fakelegend"]')
var selectlegend = $('.legend').not(getname2).not(getname);
d3.selectAll(selectlegend)
.style("opacity", 1);
d3.select(getname)
.attr("class", "legend");
});
//actually append the line to the graph
thegraphEnter.append("path")
.attr("class", "line")
.style("stroke", function(d) {
return color(d.name);
})
.attr("d", function(d) {
return line(d.values[0]);
})
.transition()
.duration(2000)
.attrTween('d', function(d) {
var interpolate = d3.scale.quantile()
.domain([0, 1])
.range(d3.range(1, d.values.length + 1));
return function(t) {
return line(d.values.slice(0, interpolate(t)));
};
});
//then append some 'nearly' invisible circles at each data point
thegraph.selectAll("circle")
.data(function(d) {
return (d.values);
})
.enter()
.append("circle")
.attr("class", "tipcircle")
.attr("cx", function(d, i) {
return x(d.date)
})
.attr("cy", function(d, i) {
return y(d.value)
})
.attr("r", 3) // was 12
.style('opacity', .2)
.attr("title", maketip);
//append the legend
var legend = svg.selectAll('.legend')
.data(linedata);
var legendEnter = legend
.enter()
.append('g')
.attr('class', 'legend')
.attr('id', function(d) {
return d.name;
})
.on('click', function(d) { //onclick function to toggle off the lines
if ($(this).css("opacity") == 1) {
//uses the opacity of the item clicked on to determine whether to turn the line on or off
var elemented = document.getElementById(this.id + "-line"); //grab the line that has the same ID as this point along w/ "-line"
//use get element cause ID has spaces
d3.select(elemented)
.transition()
.duration(1000)
.style("opacity", 0)
.style("display", 'none');
d3.select(this)
.attr('fakeclass', 'fakelegend')
.transition()
.duration(1000)
.style("opacity", .2);
} else {
var elemented = document.getElementById(this.id + "-line");
d3.select(elemented)
.style("display", "block")
.transition()
.duration(1000)
.style("opacity", 1);
d3.select(this)
.attr('fakeclass', 'legend')
.transition()
.duration(1000)
.style("opacity", 1);
}
});
//create a scale to pass the legend items through // this is broken for some types
var legendscale = d3.scale.ordinal()
.domain(lastvalues)
.range([0, 30, 60, 90, 120, 150, 180, 210]);
//actually add the circles to the created legend container
legendEnter.append('circle')
.attr('cx', width + 20) // cx=width+50 made circle overlap text
.attr('cy', function(d) {
var newScale = (legendscale(d.values[d.values.length - 1].value) + 20);
return newScale;
})
.attr('r', 7)
.style('fill', function(d) {
return color(d.name);
});
//add the legend text
legendEnter.append('text')
.attr('x', width + 35) // is this an issue?
.attr('y', function(d) {
return legendscale(d.values[d.values.length - 1].value);
})
.text(function(d) {
return d.name;
});
// set variable for updating visualization
var thegraphUpdate = d3.transition(thegraph);
// change values of path and then the circles to those of the new series
thegraphUpdate.select("path")
.attr("d", function(d, i) {
lastvalues[i] = d.values[d.values.length - 1].value;
lastvalues.sort(function(a, b) {
return b - a
});
legendscale.domain(lastvalues);
return line(d.values);
// }
});
thegraphUpdate.selectAll("circle")
.attr("title", maketip) // displays HTML but not circle
.attr("cy", function(d, i) {
return y(d.value)
})
.attr("cx", function(d, i) {
return x(d.date)
});
// and now for legend items
var legendUpdate = d3.transition(legend);
legendUpdate.select("circle")
.attr('cy', function(d, i) {
return legendscale(d.values[d.values.length - 1].value);
});
legendUpdate.select("text")
.attr('y', function(d) {
return legendscale(d.values[d.values.length - 1].value);
});
d3.transition(svg).select(".x.axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
//make my tooltips work
$('circle').tipsy({
opacity: .9,
gravity: 'n',
html: true
});
//end of the redraw function
}
svg.append("svg:text")
.attr("text-anchor", "start")
.attr("x", 0 - margin.left)
.attr("y", height + margin.bottom - 10)
.text(sourcetext)
.attr("class", "source");
My adapted code (including a lot of console.log messages) is in jsfiddle https://jsfiddle.net/pwarwick43/13fpn567/2/
I am beginning to think the problem might be with the version of d3 or jquery. Anyone got suggestions about this?
I'm working on creating a graph that updates when a button is clicked, however when clicking the button, it seems only the axis are updating, and not the data itself.
The current version is in this plunker, I've also attached the code below:
http://plnkr.co/edit/85H6i25YPbTB0MRKtpZn?p=preview
I'm still quite new to D3 and have used a a few books and a lot of reading to get me to an ok level, but am struggling to find an answer to this specific question after trawling through many pages of the internet.
It would be amazing if anyone could give me some guidance on where I'm going wrong.
<body>
<svg width="960" height="500"></svg>
<div id="option">
<input name="updateButton" type="button" value="Click here to update the chart with results after the snap election" onclick="updateData()" />
</div>
<script type="text/javascript">
//graph 1
var svg = d3.select("svg"),
margin = {
top: 20,
right: 20,
bottom: 30,
left: 40
},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x0 = d3.scaleBand()
.rangeRound([0, width])
.paddingInner(0.1);
var x1 = d3.scaleBand()
.padding(0.05);
var y = d3.scaleLinear()
.rangeRound([height, 0]);
var z = d3.scaleOrdinal()
.range(["#0087dc", "#d50000", "#FDBB30"]);
d3.csv("data.csv", function(d, i, columns) {
for (var i = 1, n = columns.length; i < n; ++i) d[columns[i]] = +d[columns[i]];
return d;
}, function(error, data) {
if (error) throw error;
var keys = data.columns.slice(1);
x0.domain(data.map(function(d) {
return d.Year;
}));
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();
g.append("g")
.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d) {
return "translate(" + x0(d.Year) + ",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 height - y(d.value);
})
.attr("fill", function(d) {
return z(d.key);
});
g.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x0));
g.append("g")
.attr("class", "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-weight", "bold")
.attr("text-anchor", "start")
.text("Seats before snap election");
var legend = g.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("text-anchor", "end")
.selectAll("g")
.data(keys.slice().reverse())
.enter().append("g")
.attr("transform", function(d, i) {
return "translate(0," + i * 20 + ")";
});
legend.append("rect")
.attr("x", width - 19)
.attr("width", 19)
.attr("height", 19)
.attr("fill", z);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9.5)
.attr("dy", "0.32em")
.text(function(d) {
return d;
});
});
// ** Update data section (Called from the onclick)
function updateData() {
//call data
d3.csv("data_copy.csv", function(d, i, columns) {
for (var i = 1, n = columns.length; i < n; ++i) d[columns[i]] = +d[columns[i]];
return d;
}, function(error, data) {
if (error) throw error;
var keys = data.columns.slice(1);
//scale range of data again
x0.domain(data.map(function(d) {
return d.Year;
}));
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();
var sel = svg.selectAll("g")
.data(data);
//remove
sel.exit().remove("g");
sel.enter().append("g")
.attr("transform", function(d) {
return "translate(" + x0(d.Year) + ",0)";
})
.selectAll("rect")
.data(function(d) {
return keys.map(function(key) {
return {
key: key,
value: d[key]
};
});
})
//remove
svg.selectAll("rect");
sel.exit().remove("rect");
sel.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 height - y(d.value);
})
.attr("fill", function(d) {
return z(d.key);
});
g.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x0));
g.append("g")
.attr("class", "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-weight", "bold")
.attr("text-anchor", "start")
.text("Seats after snap election");
});
}
#thedude's answer is right, but doesn't correct everything: it updates the bars, but the heights are wrong, for instance. Something to do with the inner .data join and the subsequent secondary formatting, I guess.
I checked at the same time and came up with the solution below. The core change that makes the update button update is this:
var sel = svg.selectAll("g.chartarea").selectAll("g.year").data(data);
sel.exit().remove();
sel.enter().append("g").classed("year", true);
// continuing with sel didn't update the just appended elements
// so I repeated the selection to get the new elements as well
sel = svg.selectAll("g.chartarea").selectAll("g.year");
sel.attr( // and so on
Complete script:
var svg = d3.select("svg"),
margin = {
top: 20,
right: 20,
bottom: 30,
left: 40
},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x0 = d3.scaleBand()
.rangeRound([0, width])
.paddingInner(0.1);
var x1 = d3.scaleBand()
.padding(0.05);
var y = d3.scaleLinear()
.rangeRound([height, 0]);
var z = d3.scaleOrdinal()
.range(["#0087dc", "#d50000", "#FDBB30"]);
// added class to enable precise selection
g.append("g").classed("chartarea", true);
// added classes to enable precise selection
g.append("g")
.classed("axis", true)
.classed("x-axis", true);
// added classes to enable precise selection
g.append("g")
.classed("axis", true)
.classed("y-axis", true);
updateGraph("data.csv");
// ** Update data section (Called from the onclick)
function updateData() {
updateGraph("data_copy.csv");
}
function updateGraph(file) {
//call data
d3.csv(file, function(d, i, columns) {
for (var i = 1, n = columns.length; i < n; ++i) d[columns[i]] = +d[columns[i]];
return d;
}, function(error, data) {
if (error) throw error;
var keys = data.columns.slice(1);
//scale range of data again
x0.domain(data.map(function(d) {
return d.Year;
}));
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();
var sel = svg.selectAll("g.chartarea").selectAll("g.year")
.data(data);
//remove
sel.exit().remove();
// added classes to enable precise selection
sel.enter().append("g").classed("year", true);
sel = svg.selectAll("g.chartarea").selectAll("g.year");
sel.attr("transform", function(d) {
return "translate(" + x0(d.Year) + ",0)";
})
.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 height - y(d.value);
})
.attr("fill", function(d) {
return z(d.key);
});
var parties =
sel.selectAll("rect.party")
.data(function(d) {
return keys.map(function(key) {
return {
key: key,
value: d[key]
};
});
});
parties.exit().remove();
// added classes to enable precise selection
parties.enter().append("rect").classed("party", true);
parties = sel.selectAll("rect.party");
parties.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 height - y(d.value);
})
.attr("fill", function(d) {
return z(d.key);
});
// select the axes instead of appending them here
g.selectAll("g.x-axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x0));
g.selectAll("g.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-weight", "bold")
.attr("text-anchor", "start")
.text("Seats after snap election");
});
}
Added some further changes that may be worth a look:
Don't differentiate between initialization and update. This is exactly what D3 excels at: doing everything with the same code. In my refactored version, the code is reduced to a single updateGraph function that does both.
Use classes or identifiers to differentiate your graphical elements. There are several places where you select too much with selectAll("g") which will select nearly all elements in your chart.
Don't add stuff multiple times. For example, the axes should be added only once. In the original code, they were added twice, overlaying each other. Instead, add them once, leave them uninitialized, then later select them and set their attributes correctly.
You need to update your rect selection in your click handler like this:
...
//remove
sel = svg.selectAll("rect");
sel.exit().remove("rect");
sel.enter().append("rect")
sel.attr("x", function(d) {
return x1(d.key);
})
.attr("y", function(d) {
return y(d.value);
})
...
You can see it in action here:
http://plnkr.co/edit/3R9lauiQQIB0IgrAk3X2?p=preview
Edit - I've updated the plunker with a working example that addresses several other issues
I am implementing a multi series line chart in d3.js following the example on this block: http://bl.ocks.org/mbostock/3884955. However, the X axis is not displaying. The X axis should display the dates. Here is my code:
<svg width="960" height="500"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
var svg = d3.select("svg"),
margin = {top: 20, right: 80, bottom: 30, left: 50},
width = svg.attr("width") - margin.left - margin.right,
height = svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var parseTime = d3.timeParse("%Y%m%d");
var x = d3.scaleBand().rangeRound([0, width]),
// xAxis = d3.svg.axis().scale(x).orient("bottom"),
y = d3.scaleLinear().range([height, 0]),
z = d3.scaleOrdinal(d3.schemeCategory10);
var line = d3.line()
.curve(d3.curveBasis)
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.price); });
d3.tsv("data.tsv", type, function(error, data) {
if (error) throw error;
var products = data.columns.slice(1).map(function(id) {
return {
id: id,
values: data.map(function(d) {
return {date: d.date, price: d[id]};
})
};
});
x.domain(data.map( function(d) { return d.date; }));
y.domain([
d3.min(products, function(c) { return d3.min(c.values, function(d) { return d.price; }); }),
d3.max(products, function(c) { return d3.max(c.values, function(d) { return d.price; }); })
]);
z.domain(products.map(function(c) { return c.id; }));
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y))
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", "0.71em")
.attr("fill", "#000")
.text("Price $");
var product = g.selectAll(".product")
.data(products)
.enter().append("g")
.attr("class", "product");
product.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", function(d) { return z(d.id); });
product.append("text")
.datum(function(d) { return {id: d.id, value: d.values[d.values.length - 1]}; })
.attr("transform", function(d) { return "translate(" + x(d.value.date) + "," + y(d.value.price) + ")"; })
.attr("x", 3)
.attr("dy", "0.35em")
.style("font", "10px sans-serif")
.text(function(d) { return d.id; });
});
function type(d, _, columns) {
d.date = parseTime(d.date);
for (var i = 1, n = columns.length, c; i < n; ++i) d[c = columns[i]] = +d[c];
return d;
}
Here is the resultant chart from my code:- http://bizvizmap.com/timeseries.html
You probably have some problem in your data, because this plunker has exactly your code, with some bogus data I created:
date,foo,bar,baz
20160101,11,23,45
20160102,21,22,35
20160103,44,76,54
20160104,31,23,45
20160105,41,63,15
20160106,12,22,25
Here is the plunker: https://plnkr.co/edit/xZY02zVTEh6o9RwNGJBw?p=preview
The ticks on the x axis are a mess because you're using the full JS date in an ordinal scale. Because of that, it makes more sense using a time scale, as in this other plunker: https://plnkr.co/edit/n5u8N9aK8YELtTCHexgR?p=preview
I am trying convert this chart I made, disregard the styles, using Highcharts, to this D3 multi-line chart.
This is the code for the d3 viz.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.axis--x path {
display: none;
}
.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
</style>
<svg width="960" height="500"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
var svg = d3.select("svg"),
margin = {top: 20, right: 80, bottom: 30, left: 50},
width = svg.attr("width") - margin.left - margin.right,
height = svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var parseTime = d3.timeParse("%Y%m%d");
var x = d3.scaleTime().range([0, width]),
y = d3.scaleLinear().range([height, 0]),
z = d3.scaleOrdinal(d3.schemeCategory10);
var line = d3.line()
.curve(d3.curveBasis)
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.temperature); });
d3.tsv("data.tsv", type, function(error, data) {
if (error) throw error;
var cities = data.columns.slice(1).map(function(id) {
return {
id: id,
values: data.map(function(d) {
return {date: d.date, temperature: d[id]};
})
};
});
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([
d3.min(cities, function(c) { return d3.min(c.values, function(d) { return d.temperature; }); }),
d3.max(cities, function(c) { return d3.max(c.values, function(d) { return d.temperature; }); })
]);
z.domain(cities.map(function(c) { return c.id; }));
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y))
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", "0.71em")
.attr("fill", "#000")
.text("Temperature, ºF");
var city = g.selectAll(".city")
.data(cities)
.enter().append("g")
.attr("class", "city");
city.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", function(d) { return z(d.id); });
city.append("text")
.datum(function(d) { return {id: d.id, value: d.values[d.values.length - 1]}; })
.attr("transform", function(d) { return "translate(" + x(d.value.date) + "," + y(d.value.temperature) + ")"; })
.attr("x", 3)
.attr("dy", "0.35em")
.style("font", "10px sans-serif")
.text(function(d) { return d.id; });
});
function type(d, _, columns) {
d.date = parseTime(d.date);
for (var i = 1, n = columns.length, c; i < n; ++i) d[c = columns[i]] = +d[c];
return d;
}
</script>
I would like to keep the data in a tsv and just convert the x-axis from using dates to using the raw numbers seen in the Highchart series. So instead of city, date, and temperature it would be batter, game number, and slugging percentage. I would also like to make it so that the lines end, like in the Highchart example, once there is no corresponding value, and not just have the line dip down to 0. It would also be nice if I could keep the hover effect from the Highchart example.
Unfortunately, I have little idea in how to achieve this. I know that I have to use a different function than the parseTime function currently in the script but that's about as far as I've gotten.
Change your scale to:
var x = d3.scaleLinear().range([0, width])
And don't parse to a date:
function type(d, _, columns) {
d.date = +d.date;
for (var i = 1, n = columns.length, c; i < n; ++i) d[c = columns[i]] = +d[c];
return d;
}
I want to display two charts in one page.The Pie chart gets displayed but the Grouped and Stacked bar chart is not displaying . I tried to change the Id name in but no luck :( . Will appreciate if anyone helps me with the code correction .
/* Display Matrix Chart ,Pie chart, Grouped and Stacked bar chart in one page */
<apex:page showHeader="false">
<apex:includeScript value="{!URLFOR($Resource.jquery1)}"/>
<apex:includeScript value="{!URLFOR($Resource.D3)}"/>
<apex:includeScript value="{!URLFOR($Resource.nvD3)}"/>
<div id="body" height="50%" width="100px" ></div> // Id for Pie chart
<div id="body1" height="30%" width="90px"></div> // Id for Stacked and Grouped bar chart
<script>
// Matrix Chart starts here
var drawChart = function(divId,matrixReportId) {
$.ajax('/services/data/v29.0/analytics/reports/'+matrixReportId,
{
beforeSend: function(xhr) {
xhr.setRequestHeader('Authorization', 'Bearer {!$Api.Session_ID}');
},
success: function(response) {
console.log(response);
var chart = nv.models.multiBarChart();
var chartData = [];
document.getElementById(divId).innerHTML = '';
$.each(response.groupingsDown.groupings, function(di, de) {
var values = [];
chartData.push({"key":de.label, "values": values});
$.each(response.groupingsAcross.groupings, function(ai, ae) {
values.push({"x": ae.label, "y": response.factMap[de.key+"!"+ae.key].aggregates[0].value});
});
});
d3.select('#'+divId).datum(chartData).transition().duration(100).call(chart);
window.setTimeout(function(){
drawChart(divId,matrixReportId);
}, 5000);
}
}
);
};
$(document).ready(function(){
drawChart('chart','00O90000005SSHv');
});
// Pie Chart Starts here
var width =250 ,
height = 450,
radius = Math.min(width, height) / 2;
var data = [{"age":"<5","population":2704659},{"age":"14-17","population":2159981},
{"age":"14-17","population":2159981},{"age":"18-24","population":3853788},
{"age":"25-44","population":14106543},{"age":"45-64","population":8819342},
{"age":">65","population":612463}];
var color = d3.scale.ordinal()
.range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]);
var arc = d3.svg.arc().outerRadius(radius - 10).innerRadius(0);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.population; });
var svg = d3.select("#body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var g = svg.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("fill", function(d) { return color(d.data.age); });
g.append("text")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return d.data.age; });
// Grouped and Stacked Bar Chart starts here
var n = 2, // number of layers
m = 10, // number of samples per layer
stack = d3.layout.stack(),
layers = stack(d3.range(n).map(function() { return bumpLayer(m, .1); })),
yGroupMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y; }); }),
yStackMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y0 + d.y; }); });
var margin = {top: 40, right: 10, bottom: 20, left: 10},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.domain(d3.range(m))
.rangeRoundBands([0, width], .08);
var y = d3.scale.linear()
.domain([0, yStackMax])
.range([height, 0]);
var color = d3.scale.linear()
.domain([0, n - 1])
.range(["#aad", "#556"]);
var xAxis = d3.svg.axis()
.scale(x)
.tickSize(0)
.tickPadding(6)
.orient("bottom");
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 layer = svg.selectAll(".layer")
.data(layers)
.enter().append("g")
.attr("class", "layer")
.style("fill", function(d, i) { return color(i); });
var rect = layer.selectAll("rect")
.data(function(d) { return d; })
.enter().append("rect")
.attr("x", function(d) { return x(d.x); })
.attr("y", height)
.attr("width", x.rangeBand())
.attr("height", 0);
rect.transition()
.delay(function(d, i) { return i * 10; })
.attr("y", function(d) { return y(d.y0 + d.y); })
.attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); });
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
d3.selectAll("input").on("change", change);
var timeout = setTimeout(function() {
d3.select("input[value=\"grouped\"]").property("checked", true).each(change);
}, 2000);
function change() {
clearTimeout(timeout);
if (this.value === "grouped") transitionGrouped();
else transitionStacked();
}
function transitionGrouped() {
y.domain([0, yGroupMax]);
rect.transition()
.duration(500)
.delay(function(d, i) { return i * 10; })
.attr("x", function(d, i, j) { return x(d.x) + x.rangeBand() / n * j; })
.attr("width", x.rangeBand() / n)
.transition()
.attr("y", function(d) { return y(d.y); })
.attr("height", function(d) { return height - y(d.y); });
}
function transitionStacked() {
y.domain([0, yStackMax]);
rect.transition()
.duration(500)
.delay(function(d, i) { return i * 10; })
.attr("y", function(d) { return y(d.y0 + d.y); })
.attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); })
.transition()
.attr("x", function(d) { return x(d.x); })
.attr("width", x.rangeBand());
}
// Inspired by Lee Byron's test data generator.
function bumpLayer(n, o) {
function bump(a) {
var x = 1/(.1 + Math.random()),
y = 2*Math.random()-.5,
z = 10/(.1 + Math.random());
for (var i = 0; i < n; i++) {
var w = (i/n- y) * z;
a[i] += x * Math.exp(-w * w);
}
}
var a=[],i;
for (i = 0; i < n; ++i) a[i] = o + o * Math.random();
for (i = 0; i < 5; ++i) bump(a);
return a.map(function(d, i) { return {x: i, y: Math.max(0, d)}; });
}
</script>
<svg id="chart" height="50%" width="500px" ></svg> // Id for Matrix chart
</apex:page>
Thanks in advance
first, as we discussed here (d3 Donut chart does not render), you should position your
<div id="body1" height="30%" width="90px"></div>
above the script.
second, you are missing the # in your second svg-declaration to correctly select the div by its id, it has to be
var svg = d3.select("#body1").append("svg")
you could also think about naming the second svg differently (eg svg2) so you don't override your first svg-variable (in case you want to do something with it later).