D3.js - changing ordinal scales dynamically - javascript

I've got the following piece of code that takes a mouse movement on an SVG container and will shrink/grow the height/width of a visualization based on a mouse movement. When the user has a mouse movement in the x direction, the bars on the chart appear jittery: the x attribute will increase by, 2 or three and then revert to what it was previously: the rects on the page will scoot right a couple pixels, and then snap back to their original position.
Is there a bug with how I'm changing the ordinal scale? Or should I use a transform instead of manipulating the X value?
'use strict';
var d3 = require("d3");
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var chartData;
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 + ")");
let render = (e,data)=>{
width += d3.event ? d3.event.movementX : 0; //changing x axis here
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
var y = d3.scale.linear()
.range([data.height, 0]);
//
x.domain(data.map(function(d) { return d.letter; }));
y.domain([0, d3.max(data, function(d) { return d.frequency; })]);
//
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(10, "%")
.tickSize(1);
var yAxisEl = svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Frequency");
var bars = svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) {
console.log(x(d.letter));
var currX = x(d.letter); //sometimes gives wrong valuse
return currX;
})
.attr("width", function(){
return x.rangeBand();
})
.attr("y", function(d) { return y(d.frequency); })
.attr("height", function(d) { return Math.abs(height - y(d.frequency)); })
}
let rerender=(data)=>{
d3.select("svg").select("g").selectAll("*").remove();
render(null,data);
}
d3.tsv("data.tsv", type, function(error, data) {
if (error) throw error;
chartData = data;
chartData.height = height;
chartData.width = width;
render(error,chartData);
});
d3.selectAll('svg').on('mousemove',function(){
if(chartData){
chartData.height += d3.event.movementY;
rerender(chartData);
}
});
data
letter frequency
A .08167
B .01492
C .02782
D .04253
E .12702
F .02288
G .02015

I made the rerender function inside time out (hope this fixes the problem you referring)
var rerender = (data) => {
if (myVar){
clearTimeout(myVar);//clear timeout if called before 1 millisecond
}
myVar = setTimeout(function(){
d3.select("svg").select("g").selectAll("*").remove();
render(null, data);
}, 1);
}
working code here

Related

r2d3: d3.js bar chart disappears on resize

I'm rendering this d3 chart in an RMarkdown document:
Javascript (test.js):
// !preview r2d3 data=readr::read_tsv("X:/D3 Practice/data.tsv"), d3_version = "3", container="div"
//
// r2d3: https://rstudio.github.io/r2d3
var margin = {top: 40, right: 20, bottom: 30, left: 40},
width = width - margin.left - margin.right,
height = height - margin.top - margin.bottom;
var formatPercent = d3.format(".0%");
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(formatPercent);
var svg = div.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
r2d3.onRender(function(data, s, w, h, options) {
x.domain(data.map(function(d) { return d.letter; }));
y.domain([0, d3.max(data, function(d) { return d.frequency; })]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Frequency");
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.letter); })
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.frequency); })
.attr("height", function(d) { return height - y(d.frequency); })
});
function type(d) {
d.frequency = +d.frequency;
return d;
}
Data (data.tsv):
letter frequency
A .08167
B .01492
C .02780
D .04253
E .12702
F .02288
G .02022
H .06094
I .06973
J .00153
K .00747
L .04025
M .02517
N .06749
O .07507
P .01929
Q .00098
R .05987
S .06333
T .09056
U .02758
V .01037
W .02465
X .00150
Y .01971
Z .00074
R Code:
library(r2d3)
r2d3(data = readr::read_tsv("X:/D3 Practice/data.tsv"),
script = "X:/D3 Practice/test.js",
d3_version = "3",
container="div")
Chart looks fine in R preview, it also looks fine in the output HMTL document. But when I resize the window up to a certain point, the chart disappears. In the console, I can see a message that says:
Node cannot be found in the current page.
Here's the initial HTML:
Here's when I resize (note the "error" div? No idea what that is).:
It's my understanding that r2d3 has already declared and set width and height on init or resize. So you could give a minimum by changing the top of your js:
var margin = {top: 40, right: 20, bottom: 30, left: 40},
min_width = 250, /// The smallest width your plot area (exluding margins) should have
min_height = 480; /// The smallest height your plot area (exluding margins) should have
width = d3.max([width, min_width]) - margin.left - margin.right;
height = d3.max([height, min_height]) - margin.top - margin.bottom;

D3 bar chart with variable width and variable length data up to 1000 data points

I am having a lot of trouble with D3 bar chart where the length of the data I get from search is variable. I used the D3 bar chart example and built this. However as data length goes up, the graph gets less and less legible. The problem seems to be the range gets confused when it is beyond 100 points or so.
Code is below:
function makegraph(data,ctype) {
//var data=xdata.splice(-900)
var gwidth=data.length*2;
if(gwidth < 800) gwidth=800;
//gwidth=800
var margin = {top: 9, right: 20, bottom: 30, left: 90},
width = gwidth - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
//x=d3.scale.linear().domain([0,1]).range(0,width)
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickFormat(function(d) { if (d % 10) return ""; else return d;})
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
// .ticks(10, "%");
d3.select("svg").remove()
var svg = d3.select("#graphchart").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 line = d3.svg.line()
.x(function(d) { return x(d.rowid); })
.y(function(d) { return y(d[ctype]); });
x.domain(data.map(function(d) { return d.rowid; }));
var ymax=d3.max(data, function(d) { return d[ctype]; });
var ymin=d3.min(data, function(d) { return d[ctype]; });
y.domain([ymin, ymax]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", Math.log10(ymax))
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Bytes");
// console.log(x.rangeBand())
var bar = svg.selectAll(".bar")
.data(data);
bar.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.rowid); })
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d[ctype]); })
.attr("height", function(d) { var xya= height - y(d[ctype]); /* console.log(xya+":"+d[ctype]); */ return height - y(d[ctype]); })
/* .attr("title", function(d) { return d['bytes']+" Bytes at "+d.stime; }) */
.on("mouseover", function(d) { $("#graphinfo").html(d3.format('0,000')(d['bytes'])+" Bytes at "+d.stime) })
.on("mouseout",function(d) { $("#graphinfo").html(' ')})
.on("click",function(d) { tablegraph(d)});
$('.bar').tooltip()
}
Do you have suggestions how I can make this graph so that the bar chart will grow horizontally for larger data sets? Thanks for any help and suggestions!
Vijay

How to make SVG/D3js clickable and get values?

Here is my entire JS code, I'm really just getting started based on some example.
I would like to be able to catch a click event, wether on the whole drawing, wether on the chart line and get y value for it.
onMouseOver and onMouseOut work as intended for now.
Thank you.
//Example may be interesting: http://bl.ocks.org/byrongibson/5232838
var parseDate = d3.time.format("%d-%b-%y").parse;
var svg, d3, x, y, valueline, xAxis, yAxis, width, height;
function CreateSvg()
{
// Set the dimensions of the canvas / graph
var margin = {top: 30, right: 20, bottom: 30, left: 50};
width = 600 - margin.left - margin.right;
height = 270 - margin.top - margin.bottom;
// Set the ranges
x = d3.time.scale().range([0, width]);
y = d3.scale.linear().range([height, 0]);
// Define the axes
xAxis = d3.svg.axis().scale(x)
.orient("bottom").ticks(5);
yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(5);
// Define the line
valueline = d3.svg.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.close); });
//var rect = d3.selectAll("rect");
// Adds the svg canvas
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 + ")").on("click", click)
;
}
function GetData(data)
{
data.forEach(function(d)
{
d.date = parseDate(d.date);
d.close = +d.close;
});
data.reverse();
data.push({date:parseDate('3-May-12'), close:Math.random() * 1200});
data.push({date:parseDate('4-May-12'), close:Math.random() * 1200});
data.reverse();
return data;
}
function Draw()
{
d3.csv("data.csv", function(error, data) {
data = GetData(data);
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([0, d3.max(data, function(d) { return d.close; })]);
svg.text("");
// Add the valueline path.
svg.append("path")
.attr("class", "line")
.attr("d", valueline(data))
.on("mouseover", onMouseOver)
.on("mouseout", onMouseOut)
.on("click", click)
;
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
});
}
function click() {
alert("onclick");
}
function onMouseOver(){
alert("mouseOver");
};
function onMouseOut(){
alert("mouseOut");
};
CreateSvg();
Draw();
Draw();

Bar chart scale

I have this bar chart on D3.js... It works fine..
But I'm having problems with the scale... When a data series has a value much grater than the others, the <rect> does not fit into the scale.
Any idea how to solve this matter?
Here is the code:
var data = [
{"Anio":"1999","CONTRAVENCIONAL":"78484","PENAL":"0","FALTAS":"0","MULTAS":"0","OTROS":"0","TOTAL":"78484"},
{"Anio":"2000","CONTRAVENCIONAL":"92879","PENAL":"0","FALTAS":"0","MULTAS":"0","OTROS":"0","TOTAL":"92879"},
{"Anio":"2001","CONTRAVENCIONAL":"100018","PENAL":"0","FALTAS":"1818","MULTAS":"0","OTROS":"0","TOTAL":"101836"},
{"Anio":"2002","CONTRAVENCIONAL":"101380","PENAL":"0","FALTAS":"3692","MULTAS":"0","OTROS":"0","TOTAL":"105072 "},
{"Anio":"2003","CONTRAVENCIONAL":"86791","PENAL":"0","FALTAS":"7417","MULTAS":"0","OTROS":"0","TOTAL":"94208"},
{"Anio":"2004","CONTRAVENCIONAL":"47870","PENAL":"255","FALTAS":"1105","MULTAS":"1811","OTROS":"0","TOTAL":"51041"},
{"Anio":"2005","CONTRAVENCIONAL":"33013","PENAL":"348","FALTAS":"1473","MULTAS":"634","OTROS":"0","TOTAL":"35468"},
];
var margin = {top: 20, right: 30, bottom: 30, left: 40},
width = 860 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var arr_data = [];
for (var i = 0; i < data.length; i++) {
var o = {'name':data[i].Anio,'value':data[i].TOTAL};
arr_data.push(o);
};
var chart = d3.select(".chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom )
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
x.domain(arr_data.map(function(d) { return d.name; }));
y.domain([0, d3.max(arr_data, function(d) { return d.value; })]);
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
chart.append("g")
.attr("class", "y axis")
.call(yAxis);
var bar = chart.selectAll(".bar")
.data(arr_data)
.enter().append("g");
bar.append("rect")
.attr("class", "rect")
.attr("x", function(d) { return x(d.name); })
.attr("y", function(d) { return y(d.value) ; })
.attr("height", function(d) { return height - y(d.value); })
.attr("width", x.rangeBand())
.attr("fill","#632423")
.on('mouseover',function(d){
var a = d3.select(this)
.attr("fill","#733A39");
var a = d3.select("#tooltip")
.style("left","100px")
.style("top","20px")
.select("#value")
.text(d.value);
}).on('mouseout',function(d){
var a = d3.select(this)
.attr("fill","#632423"); //old color: #790018
});
bar.append("text")
.attr("class", "label")
.text(function(d) { return d.value; })
.attr("x", function(d) { return x(d.name)+4; })
.attr("y", function(d) { return y(d.value)+20 ; });
Your scale will actually handle series with larger values for you because you're dynamically setting it to go from 0 to the max value in your data:
y.domain([0, d3.max(arr_data, function(d) { return d.value; })]);
However, you're not converting your data to numbers from strings, so right now the scale is from [0, 94208] instead of [0, 105072]. This is because the character "9" is greater than "1". You can fix this by converting the values to numbers when you construct your arr_data, like this:
var o = {'name':data[i].Anio,'value':+data[i].TOTAL}; // <---- notice the '+'
This produces a better looking graph:

D3: How do you highlight a range of dates on D3 a line chart?

I’m building a simple line chart using D3. The chart will contain a range of dates on the X axis and a range of values on the Y axis.
I’d like to draw a couple of rectangles behind the line. These rect’s would span the height of the chart and span the width of the dates that I would define. Let’s say for example rect 1 being between Thu 18th to Wed 24th and rect 2 being between Wed 24th and Tue 30th.
These rect’s would also have a click event attached to them. So that if clicked, they would trigger a function that I would define. This function would need to know which rect was clicked.
Has anybody achieved something like this before? Can anybody help steer me in the right direction? Thanks so much.
I've created this Fiddle to get started (based on an mbostock example):
http://jsfiddle.net/Critter/TJqE6/2/
var data = [
{"date":"1-May-13","close":58.13},
{"date":"30-Apr-13","close":53.98},
{"date":"27-Apr-13","close":67.00},
{"date":"26-Apr-13","close":89.70},
{"date":"25-Apr-13","close":99.00},
{"date":"24-Apr-13","close":130.28},
{"date":"23-Apr-13","close":166.70},
{"date":"20-Apr-13","close":234.98},
{"date":"19-Apr-13","close":345.44},
{"date":"18-Apr-13","close":443.34},
];
var margin = {top: 20, right: 50, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var parseDate = d3.time.format("%d-%b-%y").parse,
bisectDate = d3.bisector(function(d) { return d.date; }).left,
formatValue = d3.format(",.2f"),
formatCurrency = function(d) { return "$" + formatValue(d); };
var x = d3.time.scale()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var line = d3.svg.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.close); });
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 + ")");
data.forEach(function(d) {
d.date = parseDate(d.date);
d.close = +d.close;
data.sort(function(a, b) {
return a.date - b.date;
});
x.domain([data[0].date, data[data.length - 1].date]);
y.domain(d3.extent(data, function(d) { return d.close; }));
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Price ($)");
svg.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
var focus = svg.append("g")
.attr("class", "focus")
.style("display", "none");
focus.append("circle")
.attr("r", 4.5);
focus.append("text")
.attr("x", 9)
.attr("dy", ".35em");
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.on("mouseover", function() { focus.style("display", null); })
.on("mouseout", function() { focus.style("display", "none"); })
.on("mousemove", mousemove);
function mousemove() {
var x0 = x.invert(d3.mouse(this)[0]),
i = bisectDate(data, x0, 1),
d0 = data[i - 1],
d1 = data[i],
d = x0 - d0.date > d1.date - x0 ? d1 : d0;
focus.attr("transform", "translate(" + x(d.date) + "," + y(d.close) + ")");
focus.select("text").text(formatCurrency(d.close));
}
});
The main issue is putting the rectangles behind the lines. Given a date you can calculate its position on the corresponding axis like this - say you want to put a bar starting on 04/19/2013 and ending on 04/21/2013.
//here x is already the x axis time scale you have defined in your code
var left = x(new Date("Apr 19 2013"));
var right = x(new Date("Apr 22 2013")); //one more day
var wid = right - left;
svg.append("rect")
.attr("x", left)
.attr("width", wid)
.attr("height", height)
Now you can also add a style class to fill or stroke or set opacity etc. Adding click events should be easy but since there is no z-index for SVG which only relies on DOM order you can see if you can insert the rectangles before you insert the chart lines.

Categories