d3.js categorical time series (evolustrip) - d3.js

Working in d3.js, I am looking for a good way to display categorical time series data. The data values cannot co-occur, and are not evenly spaced, so I've data exactly like:
location = [[time1: home], [time4: work], [time5: cafe], [time7: home]]
and so on. My ideal resulting graph is something like what might be called an evolustrip - one way of seeing this chart is as a time series chart with variable width bars, bar color corresponding to category (e.g. 'home').
Can anyone point me in the right direction? Thank you so much!

So I ended up crafting my own d3.js solution:
I used a d3.time.scale scale for the time dimension, and then a d3.scale.category20 scale to provide colors for the categories. I then plotted the categorical data as same-height rects on the time axis by start time, and used the d3.time.scale scale to compute the appropriate bin width for each rect.
A reusable component (following the pattern at http://bost.ocks.org/mike/chart/) example can be seen here:
function timeSeriesCategorical() {
var w = 860,
h = 70,
margin = {top: 20, right: 80, bottom: 30, left: 50},
width = w - margin.left - margin.right,
height = h - margin.top - margin.bottom;
var xValue = function(d) { return d[0]; },
yValue = function(d) { return d[1]; };
var yDomain = null;
var xScale = d3.time.scale()
.range([0, width]);
var yScale = d3.scale.category20();
var xAxis = d3.svg.axis()
.scale(xScale)
.tickSubdivide(1)
.tickSize(-height)
.orient('bottom');
var yAxis = d3.svg.axis()
.scale(yScale)
.ticks(5)
.orient('left');
var binwidth = 20;
function chart(selection) {
selection.each(function(data) {
// convert data to standard representation
data = data.map(function(d, i) {
return [xValue.call(data, d, i), yValue.call(data, d, i)];
//return d;
});
// scale the x and y domains based on the actual data
xScale.domain(d3.extent(data, function(d) { return d[0]; }));
if (!yDomain) {
yScale.domain(d3.extent(data, function(d) { return d[1]; }));
} else {
yScale.domain(yDomain);
}
// compute binwidths for TODO better comment
// d looks like {timestamp, category}
data.forEach(function(d, i) {
if (data[i+1]) {
w_current = xScale(data[i][0]);
w_next = xScale(data[i+1][0]);
binwidth = w_next - w_current;
}
d.binwidth = binwidth;
});
// create chart space as svg
// note: 'this' el should not contain svg already
var svg = d3.select(this).append('svg').data(data);
// external dimensions
svg.attr('width', w)
.attr('height', h);
// internal dimensions
svg = svg.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// x axis
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
// TODO bars legend
// bars
svg.selectAll('rect')
.data(data)
.enter().append('rect')
.attr('x', function(d, i) { return xScale(d[0]); })
.attr('width', function(d, i) { return d.binwidth; })
.attr('height', height)
.attr('fill', function(d, i) { return yScale(d[1]); })
.attr('stroke', function(d, i) { return yScale(d[1]); });
});
}
chart.x = function(_) {
if (!arguments.length) return xValue;
xValue = _;
return chart;
};
chart.y = function(_) {
if (!arguments.length) return yValue;
yValue = _;
return chart;
};
chart.yDomain = function(_) {
if (!arguments.length) return yDomain;
yDomain = _;
return chart;
};
return chart;
}
and is callable with something like:
d3.csv('./data.csv', function(data) {
var chartActivity = timeSeriesCategorical()
.x(function(d) { return d.when; })
.y(function(d) { return d.activity; })
.yDomain([0,1]);
d3.select('#chart-activity')
.datum(data)
.call(chartActivity);
});
Hopefully this is helpful to someone! The project this was made for is at https://github.com/interaction-design-lab/stress-sense-portal

Related

How do I get rid of tiny lines between canvas rects

I am very new to D3 and as you can see in the image above there are tiny lines/gaps between each rectangle that I would love to get rid of, this is drawn on a canvas element with each rectangle starting where the last one ends using D3.js following this tutorial almost exactly minus adding the gaps between each square.
I've tried
this.canvas.imageSmoothingQuality = 'low';
draw() {
const canvas = d3
.select(this.chartContainer.nativeElement)
.append('canvas')
.attr('width', this.width)
.attr('height', this.height)
.attr(
'transform',
'translate(' + this.margin.left + ',' + this.margin.top + ')'
);
this.canvas = canvas.node().getContext('2d');
this.clearCanvas();
this.canvas.imageSmoothingQuality = 'low';
const elements = this.shadowContainer.selectAll('custom.rect');
const _this = this;
elements.each(function(d, i) {
const node = d3.select(this);
// Here you retrieve the colour from the individual in-memory node and set the fillStyle for the canvas paint
_this.canvas.fillStyle = node.attr('color');
// Here you retrieve the position of the node and apply it to the fillRect context function which will fill and paint the square.
_this.canvas.fillRect(
Number(node.attr('x')),
Number(node.attr('y')),
Number(node.attr('width')),
Number(node.attr('height'))
);
});
}
private dataBind(value) {
const customBase = document.createElement('custom');
this.shadowContainer = d3.select(customBase);
const {
viewModes: {
heatMap: {
data,
chartOptions: { engagementStatus, xAxis, yAxis }
}
}
} = value;
const x = this.d3
.scaleBand()
.range([0, this.width])
.domain(xAxis.categories);
this.shadowContainer
.append('g')
.style('font-size', 11)
.attr('class', 'x-axis')
.call(this.d3.axisTop(x).tickSize(0))
.select('.domain')
.remove();
this.shadowContainer
.selectAll('.x-axis text')
.style('text-anchor', 'start')
.attr('transform', function(d) {
return `translate(8, -8)rotate(-90)`;
});
const y = this.d3
.scaleBand()
.domain(d3.reverse(yAxis.categories))
.range([this.height, 0]);
const color = this.d3
.scaleLinear()
.domain([-2, -1, 0, 1])
// #ts-ignore
.range(['#5b717d', '#ffb957', '#ee6b56', '#40a050']);
const join = this.shadowContainer
.selectAll('custom.rect')
.data(data, function(d) {
return `${d.Date}:${d.Member}`;
});
const enterSelection = join
.enter()
.append('custom')
.attr('class', 'rect')
.attr('x', d =>
this.getCorrectDatePosition(
d.Date,
x,
xAxis.categories[0].split('/').length
)
)
.attr('y', function(d) {
return y(d.Member);
})
.attr('width', 24)
.attr('height', 24);
join
.merge(enterSelection)
.attr('width', x.bandwidth())
.attr('height', y.bandwidth())
.attr('color', function(d) {
return color(d.score);
});
const exitSelection = join
.exit()
.transition()
.attr('width', 0)
.attr('height', 0)
.remove();
}
This is likely an issue stemming from your scales. It can occur with either SVG or canvas and occurs when dealing with coordinates that require plotting at fractions of a pixel.
Here's a demonstration with SVG:
var data = d3.range(20);
var x = d3.scaleBand()
.range([10,250])
.domain(data)
var svg = d3.select("body")
.append("svg")
.attr("width", 500);
var rect = svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", d=>x(d) )
.attr("y", 50)
.attr("width", x.bandwidth())
.attr("height",100)
.attr("fill","crimson")
svg.transition()
.attrTween("tween", function() {
var i = d3.interpolate(250,480)
return function(t) {
x.range([50,i(t)])
rect.attr("x",d=>x(d))
.attr("width", x.bandwidth());
return "";
}
})
.duration(10000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
And one with Canvas:
var data = d3.range(20);
var x = d3.scaleBand()
.range([10,250])
.domain(data)
var canvas = d3.select("body")
.append("canvas")
.attr("width", 500);
var rect = d3.create("div").selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", d=>x(d) )
.attr("y", 50)
.attr("width", x.bandwidth())
.attr("height",100)
.attr("fill","crimson")
canvas.transition()
.attrTween("tween", function() {
var i = d3.interpolate(250,480)
var context = canvas.node().getContext("2d");
return function(t) {
x.range([50,i(t)])
context.fillStyle = "#fff";
context.fillRect(0,0,550,300);
rect.attr("x",d=>x(d))
.attr("width", x.bandwidth())
.each(function() {
var node = d3.select(this);
context.fillStyle = "crimson"
context.fillRect(
+node.attr("x"),
+node.attr("y"),
+node.attr("width"),
+node.attr("height"))
})
return "";
}
})
.duration(10000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
The solution is to be a bit more involved in setting the scale's domain and range. Start with the desired bandwidth, a whole number in pixels, and set the range so that the difference between the minimum and maximum values is equal to the number of values in the domain * the bandwidth.
So instead of:
const x = this.d3
.scaleBand()
.range([0, this.width])
.domain(xAxis.categories);
You'd have:
const length = 10; // length of a box side
const x = this.d3
.scaleBand()
.domain(xAxis.categories)
.range([0,xAxis.categories * length])
You could also calculate length above dynamically, say by using: Math.floor(width/xAxis.categories)
Using the above approach and a slightly contrived example to accommodate the transition, we remove the aliasing/moire pattern. Because we use only full pixels, the transition jumps as each bar increases in width by a full pixel at the same time, as space becomes available in the range:
var data = d3.range(20);
var length = 30;
var x = d3.scaleBand()
.range([10,data.length*length])
.domain(data)
var canvas = d3.select("body")
.append("canvas")
.attr("width", 500);
var rect = d3.create("div").selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", d=>x(d) )
.attr("y", 50)
.attr("width", x.bandwidth())
.attr("height",100)
.attr("fill","crimson")
canvas.transition()
.attrTween("tween", function() {
var i = d3.interpolate(250,480)
var context = canvas.node().getContext("2d");
return function(t) {
length = Math.floor(i(t)/data.length)
x.range([10,length*data.length+10])
context.fillStyle = "#fff";
context.fillRect(0,0,550,300);
rect.attr("x",d=>x(d))
.attr("width", x.bandwidth())
.each(function(d,i) {
var node = d3.select(this);
context.fillStyle = d3.schemeCategory10[i%10];
context.fillRect(
+node.attr("x"),
+node.attr("y"),
+node.attr("width"),
+node.attr("height"))
})
return "";
}
})
.duration(10000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Adding axes eats up my data

I'm following the D3 tutorial but adding the axis makes half of my data disappear and I don't understand why. I thought that maybe the axis is taking up the space that's meant for the data so I added an extra 10px to the transform property, but it doesn't make any difference.
var GIST = "https://gist.githubusercontent.com/charisseysabel/f8f48fbf11b8a1b0d62cbe2d6bdc2aa6/raw/2ead1537adb822fbd59a666afd5334d525480a13/nano-2017.tsv"
var width = 1000,
height = 550,
margin = {top: 20, right: 30, bottom: 30, left: 4};
var y = d3.scaleLinear()
.range([height, 0]);
var yScale = d3.scaleLinear()
.range([height, 0]);
var xScale = d3.scaleLinear()
.range([0, width]);
var xAxis = d3.axisLeft(yScale);
var yAxis = d3.axisBottom(xScale);
var chart = d3.select(".chart")
.attr("width", width)
.attr("height", height);
chart.append("g")
.attr("transform", "translate(10, 0)")
.call(xAxis);
chart.append("g")
.attr("transform", "translate(0, 540)")
.call(yAxis);
d3.tsv(GIST, type, function(error, data) {
y.domain([0, d3.max(data, function(d) { return d.value; })]);
var barWidth = width / data.length;
var bar = chart.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d, i) {
return "translate(" + ((i * barWidth) + 10) + ",0)"; }
);
bar.append("rect")
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.attr("width", barWidth - 1);
bar.append("text")
.attr("x", (barWidth / 2) - 2)
.attr("y", function(d) { return y(d.value) + 3; })
.attr("dy", ".75em")
.text(function(d) { return d.value; });
});
function type(d) {
d.value = +d.value;
return d;
}
When you do this...
var bar = chart.selectAll("g").etc...
... you're selecting group elements that already exist in the SVG, which are the axes, and binding your data to them.
There are two easy solutions:
Move your code that creates the axes to the bottom of the d3.tsv, that is, after you have appended the bars.
Select something that doesn't exist, like
var bar = chart.selectAll(null).etc. To read more about the logic behind selectAll(null), have a look at my answer here.

D3JS makes date duplicates

I have this d3js code:
var tooltip = tooltipd3();
var svg = d3.select("svg#svg-day"),
margin = {
top: 20,
right: 30,
bottom: 30,
left: 25,
padding: 15
},
width = 700 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
// parse the periodo / time
var parseTime = d3.timeParse("%Y-%m-%d");
// set the ranges
var x = d3.scaleTime().range([0, width - margin.padding]);
var y = d3.scaleLinear().range([height, 0]);
// define the area
var area = d3.area()
.x(function(d) {
return x(d.periodo) + (margin.left + margin.padding);
})
.y0(height)
.y1(function(d) {
return y(d.guadagno);
});
// define the line
var valueline = d3.line()
.x(function(d) {
return x(d.periodo) + (margin.left + margin.padding);
})
.y(function(d) {
return y(d.guadagno);
});
var div = d3.select("svg#svg-day")
.append("div") // declare the tooltip div
.attr("class", "tooltip") // apply the 'tooltip' class
.style("opacity", 0);
// get the data
d3.csv(base_url() + 'graph/getStatementsDaily/', function(error, data) {
if (error) throw error;
$('.graph-loading').hide();
// format the data
data.forEach(function(d) {
d.periodo = parseTime(d.periodo)
d.guadagno = +d.guadagno;
});
// scale the range of the data
x.domain(d3.extent(data, function(d) {
return d.periodo;
}));
y.domain([0, d3.max(data, function(d) {
return d.guadagno + ((d.guadagno / 100) * 10); // 10% in più sulla scala numerica
})]);
// add the area
svg.append("path")
.data([data])
.attr("class", "area")
.attr("d", area);
// add the valueline path.
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
// Add the scatterplot
svg.selectAll("dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 3)
.attr("cx", function(d) {
return x(d.periodo) + (margin.left + margin.padding);
})
.attr("cy", function(d) {
return y(d.guadagno);
})
.on('mouseover', function(d) {
var html = '<h5>' + d.guadagno + ' €</h5>';
tooltip.mouseover(html); // pass html content
})
.on('mousemove', tooltip.mousemove)
.on('mouseout', tooltip.mouseout);
// add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(" + (margin.left + margin.padding) + "," + (height) + ")")
//HERE IS THE DATES CODE
.call(d3.axisBottom(x).tickFormat(d3.timeFormat("%d/%m")))
// add the Y Axis
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate (" + (margin.left + margin.padding) + " 0)")
.call(d3.axisLeft(y));
});
The dates care coming from a CSV file that has this format:
periodo,guadagno
2017-05-08,0.0
2017-05-09,0.5385
2017-05-10,0.0
2017-05-11,0.0
2017-05-12,0.0
2017-05-13,0.5680
2017-05-14,0.0
2017-05-15,0.0
The result is fine with lots of dates, but with 7 dates I get duplicates as you can see here:
Why is this?? And how do I fix it?
This is something that bothers a lot of people new to D3: the ticks in the axis, specially when using a time scale, are automatically generated. In your case, given the date interval in your domain, it coincidentally ended up creating two ticks for each day. But pay attention to this: those ticks represent different times (hours) in the same day (you can see that if you remove the tickFormat in the axis generator).
Let's see your code generating the x axis:
var svg = d3.select("svg");
var data = d3.csvParse(d3.select("#csv").text());
var parseTime = d3.timeParse("%Y-%m-%d");
data.forEach(function(d) {
d.periodo = parseTime(d.periodo)
});
var x = d3.scaleTime()
.range([20, 480])
.domain(d3.extent(data, function(d) {
return d.periodo;
}));
var axis = d3.axisBottom(x).tickFormat(d3.timeFormat("%d/%m"))(svg.append("g").attr("transform", "translate(0,50)"));
pre {
display: none;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500"></svg>
<pre id="csv">periodo,guadagno
2017-05-08,0.0
2017-05-09,0.5385
2017-05-10,0.0
2017-05-11,0.0
2017-05-12,0.0
2017-05-13,0.5680
2017-05-14,0.0
2017-05-15,0.0</pre>
As you can see, there are two ticks for each day (remember, for different hours).
Let's show that this is a coincidence: This is the same code, but changing the last date for 2017-05-20:
var svg = d3.select("svg");
var data = d3.csvParse(d3.select("#csv").text());
var parseTime = d3.timeParse("%Y-%m-%d");
data.forEach(function(d) {
d.periodo = parseTime(d.periodo)
});
var x = d3.scaleTime()
.range([20, 480])
.domain(d3.extent(data, function(d) {
return d.periodo;
}));
var axis = d3.axisBottom(x).tickFormat(d3.timeFormat("%d/%m"))(svg.append("g").attr("transform", "translate(0,50)"));
pre {
display: none;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500"></svg>
<pre id="csv">periodo,guadagno
2017-05-08,0.0
2017-05-09,0.5385
2017-05-10,0.0
2017-05-11,0.0
2017-05-12,0.0
2017-05-13,0.5680
2017-05-14,0.0
2017-05-20,0.0</pre>
Back to your code.
The solution is quite simple: using intervals. Let's set the interval for each tick:
d3.axisBottom(x).ticks(d3.timeDay)
Here is the same code with that change only:
var svg = d3.select("svg");
var data = d3.csvParse(d3.select("#csv").text());
var parseTime = d3.timeParse("%Y-%m-%d");
data.forEach(function(d) {
d.periodo = parseTime(d.periodo)
});
var x = d3.scaleTime()
.range([20, 480])
.domain(d3.extent(data, function(d) {
return d.periodo;
}));
var axis = d3.axisBottom(x).tickFormat(d3.timeFormat("%d/%m")).ticks(d3.timeDay)(svg.append("g").attr("transform", "translate(0,50)"));
pre {
display: none;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500"></svg>
<pre id="csv">periodo,guadagno
2017-05-08,0.0
2017-05-09,0.5385
2017-05-10,0.0
2017-05-11,0.0
2017-05-12,0.0
2017-05-13,0.5680
2017-05-14,0.0
2017-05-15,0.0</pre>

D3.js Radar chart line drawing

I am trying to create a radar chart similar to the link here (
http://www.larsko.org/v/euc/).
I was able to create axes (my work so far), but I am having a problem to draw lines in it.
For instance, if I have a list of values something like below, how can I draw a line in the radar chart?
var tempData = [56784, 5.898, 3417, 0, 0, 0]
Edit: I have included code. I am having a problem finding XY coordinates and I think XY value has to be derived from "scales".
var width = 1000,
height = 960,
r = (960 / 2) - 160;
var svg = d3.select("#radar")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + ", " + height / 2 + ")");
d3.csv("data/results.csv", function(data) {
var headerNames = d3.keys(data[0]);
headerNames.splice(0, 1); //Remove 'scenario'
var minList = $.map(headerNames, function(h) {
return d3.min($.map(data, function(d) {
return d[h];
}));
}),
maxList = $.map(headerNames, function(h) {
return d3.max($.map(data, function(d) {
return d[h];
}));
}),
scales = $.map(headerNames, function(h, i) {
return d3.scale.linear()
.domain([minList[i], maxList[i]])
.range([50, r]);
}),
axes = $.map(headerNames, function(h, i) {
return d3.svg.axis()
.scale(scales[i])
.tickSize(4);
});
function angle(i) {
return i * (2 * Math.PI / headerNames.length) + Math.PI / headerNames.length;
}
var line = d3.svg.line()
.interpolate("cardinal-closed")
/* computing X and Y: I am having a problem here
.x(function(d){ return scales(d); })
.y(function(d){ return scales(d); }); */
$.each(axes, function(i, a) {
svg.append("g")
.attr("transform", "rotate(" + Math.round(angle(i) * (180 / Math.PI)) + ")")
.call(a)
.selectAll("text")
.attr("text-anchor", "middle")
.attr("transform", function(d) {
return "rotate(" + -angle(i) * (180 / Math.PI) + ")";
})
//Drawing line
svg.selectAll(".layer")
.data(data)
.enter()
.append("path")
.attr("class", "layer")
.attr("d", function(d) {
return line(d);
})
}) // End CSV
Example results.csv
scenario,n_dead_oaks,percent_dead_oaks,infected_area_ha,money_spent,area_treated_ha,price_per_oak
baseline,56784,5.898,3417,0,0,0
scen2,52725,5.477,3294,382036,35,94.12071939
RS_1,58037,6.028,3407,796705,59,-635.8379888
RS_2,33571,3.487,2555,1841047,104,79.31103261
RS_3,46111,4.79,2762,1176461,61,110.227771
As Squeegy suggested, you should share some code showing your current progress and how you have achieved to create the axes.
Anyways, this is how I would go about this:
For a given list of values that you want to represent as a line, find the [x,y] coordinates of every point of the line, i.e. place your data-points on each axis. If you have a scale system in place already to draw your axes, this shouldn't be too hard.
Use d3.svg.line to draw a line that goes through all these points.
The code would end up looking like this:
var tempData = [56784, 5.898, 3417, 0, 0, 0];
/** compute tempPoints from tempData **/
var tempPoints = [[123, 30], [12, 123], [123, 123], [0,0], [0,0], [0,0]];
var line = d3.svg.line();
d3.select('svg').append('path').attr('d', line(tempPoints) + 'Z'); // the trailing Z closes the path
I think I have a solution for now and I appreciate all of your response! Here is my current solution for my posting.
function getRowValues(data) {
return $.map(data, function(d, i) {
if (i != "scenario") {
return d;
}
});
}
function getCoor(data) {
console.log(data);
var row = getRowValues(data),
x,
y,
coor = [];
for (var i = 0; i < row.length; i++) {
x = Math.round(Math.cos(angle(i)) * scales[i](row[i]));
y = Math.round(Math.sin(angle(i)) * scales[i](row[i]));
coor.push([x, y]);
}
return coor;
}
var line = d3.svg.line()
.interpolate("cardinal-closed")
.tension(0.85);
svg.selectAll(".layer")
.data(data)
.enter()
.append("path")
.attr("class", "layer")
.attr("d", function(d) { return line(getCoor(d)) + "Z"; })
.style("stroke", function(d, i){ return colors[i]; })
.style("fill", "none");

d3 redraw bar chart with new values

My barchart draws fine when the page first loads.
But choose hour 2 from the drop-down, and it doesn't want to update to hour 2 data, it just keeps displaying hour 1.
FIDDLE
This is my d3 and js:
$('#bar_chart').css('overflow-x','scroll');
var margin = {top: 20, right: 20, bottom: 40, left: 80},
width = 220 - margin.left - margin.right,
height = 233 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1, 1);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom');
var formatComma = d3.format('0,000');
var yAxis = d3.svg.axis()
.scale(y)
.orient('left')
.ticks(5)
.outerTickSize(0)
.tickFormat(formatComma);
var svg = d3.select('th.chart-here').append('svg')
.attr('viewBox', '0 0 220 233')
.attr('preserveAspectRatio','xMinYMin meet')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left/1.5 + ',' + margin.top/1.5 + ')');
var table_i = 0;
var arr1 =
[
{'hour':1,'car':[{'audi':1377},{'bmw':716},{'ford':3819},{'mazda':67},{'toyota':11580},{'tesla':0}]},
{'hour':2,'car':[{'audi':9000},{'bmw':2000},{'ford':7000},{'mazda':1000},{'toyota':5000},{'tesla':700}]},
];
var hour = arr1[table_i];
var car=hour.car;
var newobj = [];
for(var hourx1=0;hourx1<car.length;hourx1++){
var xx = car[hourx1];
for (var value in xx) {
var chartvar = newobj.push({car:value,miles:xx[value]});
var data = newobj;
}
}
x.domain(data.map(function(d) { return d.car; }));
y.domain([0, d3.max(data, function(d) { return d.miles; })]);
svg.append('g')
.attr('class', 'y axis')
.call(yAxis)
.append('text')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'start');
function changeHour(){
svg.selectAll('.bar')
.data(data)
.enter().append('rect')
.attr('class', 'bar')
.attr('transform','translate(-20)') //move rects closer to Y axis
.attr('x', function(d) { return x(d.car); })
.attr('width', x.rangeBand()*1)
.attr('y', function(d) { return y(d.miles); })
.attr('height', function(d) { return height - y(d.miles); });
xtext = svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(-20,' + height + ')') //move tick text so it aligns with rects
.call(xAxis);
xtext.selectAll('text')
.attr('transform', function(d) {
return 'translate(' + this.getBBox().height*50 + ',' + this.getBBox().height + ')rotate(0)';
});
//code to enable jqm checkbox
$('#checkbox-2a').on('change', function(e){
originalchange(e);
});
$( '#checkbox-2a' ).checkboxradio({
defaults: true
});
var sortTimeout = setTimeout(function() {
$('#checkbox-2a').prop('checked', false).checkboxradio( 'refresh' ).change();
}, 1000);
function originalchange() {
clearTimeout(sortTimeout);
var IsChecked = $('#checkbox-2a').is(':checked');
// Copy-on-write since tweens are evaluated after a delay.
var x0 = x.domain(data.sort(IsChecked
? function(a, b) { return b.miles - a.miles; }
: function(a, b) { return d3.ascending(a.car, b.car); })
.map(function(d) { return d.car; }))
.copy();
svg.selectAll('.bar')
.sort(function(a, b) { return x0(a.car) - x0(b.car); });
var transition = svg.transition().duration(950),
delay = function(d, i) { return i * 50; };
transition.selectAll('.bar')
.delay(delay)
.attr('x', function(d) { return x0(d.car); });
transition.select('.x.axis')
.call(xAxis)
.selectAll('g')
.delay(delay);
};
}
changeHour();
$('select').change(function() { //function to change hourly data
table_i = $(this).val();
var hour = arr1[table_i];
var car=hour.car;
var newobj = [];
for(var hourx1=0;hourx1<car.length;hourx1++){
var xx = car[hourx1];
for (var value in xx) {
var chartvar = newobj.push({car:value,miles:xx[value]});
var data = newobj;
}
}
x.domain(data.map(function(d) { return d.car; }));
y.domain([0, d3.max(data, function(d) { return d.miles; })]);
changeHour();
})
I thought that by updating in the function changeHour I could isolate just the rects and the text that goes with them, and redraw them based on the selected hour's data.
But it just keeps drawing the first hour.
What am I doing wrong?
2 things not working:
firstly "data" needs to be declared without 'var' in the change function at the end. Declaring it with 'var' makes it a local variable to that function, and once you leave that function it's gone. Saying "data = " without the var means you're using the data variable you've declared further up. It's all to do with scope which is something I still struggle with, but basically with 'var' it doesn't work.
var newobj = [];
for(var hourx1=0;hourx1<car.length;hourx1++){
var xx = car[hourx1];
for (var value in xx) {
var chartvar = newobj.push({car:value,miles:xx[value]});
}
}
data = newobj;
Secondly, your changeHour function only looks for new elements as it hangs all its attribute settings on an .enter() selection, changeHour should be like this:
var dataJoin = svg.selectAll('.bar')
.data(data, function(d) { return d.car; });
// possible new elements, fired first time, set non-data dependent attributes
dataJoin
.enter()
.append('rect')
.attr('class', 'bar')
.attr('transform','translate(-20)') //move rects closer to Y axis
// changes to existing elements (now including the newly appended elements from above) which depend on data values (d)
dataJoin
.attr('x', function(d) { return x(d.car); })
.attr('width', x.rangeBand()*1)
.attr('y', function(d) { return y(d.miles); })
.attr('height', function(d) { return height - y(d.miles); });
For completeness there should be a dataJoin.exit().remove() in there as well but its not something that happens in this dataset

Resources