D3.js Bar Chart Error [duplicate] - javascript

This question already has answers here:
d3 time scale - last bar on graph is appearing outside of the graph
(2 answers)
How to use x and width in a bar chart with scaleTime?
(1 answer)
Closed 4 years ago.
I am trying to graph a bar chart using a simple dataset by using D3.js. I managed to get most done, but I have one problem left. The bars on the very right GO OVER the y-axis and will not show correctly.
This is the code:
codepen
$(document).ready(function(){
var dataset;
var data;
var date;
var temp1 = [];
var temp2 = [];
var url = "https://raw.githubusercontent.com/FreeCodeCamp/ProjectReferenceData/master/GDP-data.json";
d3.json(url, function(json) {
dataset = json.data;
for (var j = 0; j < dataset.length; j++) {
temp1[j] = dataset[j][1]
}
data = temp1;
for (var j = 0; j < dataset.length; j++) {
temp2[j] = dataset[j][0]
}
date = temp2;
minDate = new Date(date[0]);
maxDate = new Date(date[date.length - 1]);
var svgWidth = 1000;
var svgHeight = 450;
var barWidth = (svgWidth/data.length);
var svg = d3.select("svg")
.attr("width", svgWidth)
.attr("height", svgHeight);
var xScale = d3.scaleTime()
.domain([minDate, maxDate])
.range([0, svgWidth]);
var yScale = d3.scaleLinear()
.domain([0, d3.max(data)])
.range([svgHeight, 0]);
var x_axis = d3.axisBottom()
.scale(xScale);
var y_axis = d3.axisRight()
.scale(yScale);
var xAxisTranslate = svgHeight - 20;
svg.append("g")
.attr("transform", "translate(0 " + xAxisTranslate +")")
.call(x_axis);
svg.append("g")
.attr("transform", "translate(0, -20)")
.call(y_axis);
var barChart = svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("y", function(d) {
return yScale(d);
})
.attr("height", function(d) {
return svgHeight - yScale(d);
})
.attr("width", barWidth)
.attr("transform", function (d, i) {
var translate = [barWidth * i, - 20];
return "translate("+ translate +")";
});
});
});
How do I get the bar chart to draw values in my dataset correctly/fully?

Related

D3 change order of selection/appended svgs

I have created a dot matrix visual as seen below in the snippet. Colored circles represent values and gray circles represent empty/unused. For instance, in my case, the three colors represent funding in percentage form for a project from 3 countries: USA, Canada and Mexico. The gray represents funding yet to be raised, also in percentage form.
var margins = {top:20, bottom:300, left:30, right:100};
var height = 600;
var width = 900;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");
var data = [
{'country':'USA', 'value':.20},
{'country':'Canada', 'value':.15},
{'country':'Mexico', 'value':.10}
];
var circArray = new Array(50);
var circPercentage = 100/circArray.length;
var circData = new Array;
data.forEach(function(item) {
for (var i =0; i <item.value*100/circPercentage; i++) {
circData.push(item.country);
}
});
var arrayDiff = 50-circData.length;
for (var i=0; i <(arrayDiff); i++) {
circData.push("");
}
//console.log(circData)
var maxColumn = 10;
var colorMap = {
'USA':"#f6d18b",
'Canada':"#366092",
'Mexico':"#95b3d7",
"":"#a6a6a6"
};
var xScale = d3.scaleLinear()
.range([0,width])
.domain([0,1]);
var yScale = d3.scaleLinear()
.range([height,0])
.domain([0,1]);
graphGroup.selectAll('circle')
.data(circData)
.enter()
.append('circle')
.attr('cx', function(d, i) {
return (i % maxColumn) * 30
})
.attr('cy', function(d, i) {
return ~~((i / maxColumn) % maxColumn) * 30
})
.attr('r', 10)
.style('fill', function(d) {
//console.log(d)
return colorMap[d];
});
<script src="https://d3js.org/d3.v5.min.js"></script>
My use of .forEach() and my for loop work inasmuch that I created an array of the correct length to map the circles to the percentage logic, like so:
['USA','USA','USA','USA','USA','USA','USA','USA','USA','USA', ... , "", "", ""];
however I was unable to find documentation for which direction/order I want the circles to be appended. Supposed I wanted the opposite of the snippet above, where the the gray circles appear on top and the colored circles start from the bottom? Or suppose I wanted gray circles on the left and colored circles on the right?
Question
Is there a straight-forward means to toggle the direction/order of svg elements? Or is my only option to transpose the data itself? As in:
["","","",... 'USA','USA','USA','USA','USA','USA','USA','USA','USA','USA'];
This approach bothers me, I don't know why. I'm hoping there is a slightly more sophisticated way.
In your code it's the same order as circData.
But actually you control the position by cx and cy, not their order.
var margins = {top:20, bottom:300, left:30, right:100};
var height = 600;
var width = 900;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");
var data = [
{'country':'USA', 'value':.20},
{'country':'Canada', 'value':.15},
{'country':'Mexico', 'value':.10}
];
var circArray = new Array(50);
var circPercentage = 100/circArray.length;
var circData = new Array;
data.forEach(function(item) {
for (var i =0; i <item.value*100/circPercentage; i++) {
circData.push(item.country);
}
});
var arrayDiff = 50-circData.length;
for (var i=0; i <(arrayDiff); i++) {
circData.push("");
}
circData.reverse() //<---------------------------
//console.log(circData)
var maxColumn = 10;
var colorMap = {
'USA':"#f6d18b",
'Canada':"#366092",
'Mexico':"#95b3d7",
"":"#a6a6a6"
};
var xScale = d3.scaleLinear()
.range([0,width])
.domain([0,1]);
var yScale = d3.scaleLinear()
.range([height,0])
.domain([0,1]);
graphGroup.selectAll('circle')
.data(circData)
.enter()
.append('circle')
.attr('cx', function(d, i) {
return (i % maxColumn) * 30
})
.attr('cy', function(d, i) {
return ~~((i / maxColumn) % maxColumn) * 30
})
.attr('r', 10)
.style('fill', function(d) {
//console.log(d)
return colorMap[d];
});
<script src="https://d3js.org/d3.v5.min.js"></script>

D3: How to interpolate between two colors? [duplicate]

I am new to D3.js and trying to make a bar chart with GDP on Y axis and date on X axis.
I am trying to make a xScale with d3.timeScale() but for some reason the code keeps returning undefined when using it. Below is my code, what am I doing wrong?
var data = [
[
"2011-01-01",
15238.4 ]
,
["2015-07-01",
18064.7
]
];
var w = 1000;
var h = 300;
var barPadding = 1;
var svg = d3.select("#chart")
.append("svg")
.attr("height", h);
var maxDate = d3.max(data, function(d){
return d[0];
});
var minDate = d3.min(data, function(d){
return d[0];
});
var maxGDP = d3.max(data, function(d){
return d[1];
});
var minGDP = d3.min(data, function(d){
return d[1];
});
minDate = new Date(minDate);
maxDate = new Date(maxDate)
var xScale = d3.scaleTime()
.domain([minDate, maxDate])
.range(0,w)
var yScale = d3.scaleLinear()
.domain([minGDP, maxGDP])
.range(0,h)
.attr("width", w)
var bars = svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
var barAttributes = bars
.attr("x", function(d) { return xScale(d[0]);})
.attr("y", function(d) { return h - (d[1]/100);})
.attr("width", function(d,i) { return w/data.length;})
.attr("height", function(d) { return (d[1]/100) *4 ;})
.attr("fill", "#8e44ad");
In D3, both domain and range have to be an array:
If domain is specified, sets the scale’s domain to the specified array of numbers.
And also:
If range is specified, sets the scale’s range to the specified array of values.
Thus, instead of:
var xScale = d3.scaleTime()
.domain([minDate, maxDate])
.range(0,w);
It should be:
var xScale = d3.scaleTime()
.domain([minDate, maxDate])
.range([0,w]);//an array here

d3.js: enter-update-exit pattern - Old points not removed

I am trying to do the enter-update-exit pattern on this graph below (which was built with the tremendous help of some very kind ppl here at SO but I am now stuck again unfortunately. I cant make the pattern work but I am certain I pick up the correct object (named heatDotsGroup in the code below).
I can however check in Chrome's developer tools that this object contains the nodes (ellipses) but the pattern doesn't work, therefore clearly I am doing something wrong.
Any ideas please? Many thanks!
function heatmap(dataset) {
var svg = d3.select("#chart")
.select("svg")
var xLabels = [],
yLabels = [];
for (i = 0; i < dataset.length; i++) {
if (i==0){
xLabels.push(dataset[i].xLabel);
var j = 0;
while (dataset[j+1].xLabel == dataset[j].xLabel){
yLabels.push(dataset[j].yLabel);
j++;
}
yLabels.push(dataset[j].yLabel);
} else {
if (dataset[i-1].xLabel == dataset[i].xLabel){
//do nothing
} else {
xLabels.push(dataset[i].xLabel);
}
}
};
var margin = {top: 0, right: 25,
bottom: 60, left: 75};
var width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var dotSpacing = 0,
dotWidth = width/(2*(xLabels.length+1)),
dotHeight = height/(2*yLabels.length);
var daysRange = d3.extent(dataset, function (d) {return d.xKey}),
days = daysRange[1] - daysRange[0];
var hoursRange = d3.extent(dataset, function (d) {return d.yKey}),
hours = hoursRange[1] - hoursRange[0];
var tRange = d3.extent(dataset, function (d) {return d.val}),
tMin = tRange[0],
tMax = tRange[1];
var colors = ['#2C7BB6', '#00A6CA', '#00CCBC', '#90EB9D', '#FFFF8C', '#F9D057', '#F29E2E', '#E76818', '#D7191C'];
// the scale
var scale = {
x: d3.scaleLinear()
.range([-1, width]),
y: d3.scaleLinear()
.range([height, 0]),
};
var xBand = d3.scaleBand().domain(xLabels).range([0, width]),
yBand = d3.scaleBand().domain(yLabels).range([height, 0]);
var axis = {
x: d3.axisBottom(scale.x).tickFormat((d, e) => xLabels[d]),
y: d3.axisLeft(scale.y).tickFormat((d, e) => yLabels[d]),
};
function updateScales(data){
scale.x.domain([0, d3.max(data, d => d.xKey)]),
scale.y.domain([ 0, d3.max(data, d => d.yKey)])
}
var colorScale = d3.scaleQuantile()
.domain([0, colors.length - 1, d3.max(dataset, function (d) {return d.val;})])
.range(colors);
var zoom = d3.zoom()
.scaleExtent([1, dotHeight])
.on("zoom", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// SVG canvas
svg = d3.select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Clip path
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height+dotHeight);
// Heatmap dots
var heatDotsGroup = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("g");
//Create X axis
var renderXAxis = svg.append("g")
.attr("class", "x axis")
//.attr("transform", "translate(0," + scale.y(-0.5) + ")")
//.call(axis.x)
//Create Y axis
var renderYAxis = svg.append("g")
.attr("class", "y axis")
.call(axis.y);
function zoomed() {
d3.event.transform.y = 0;
d3.event.transform.x = Math.min(d3.event.transform.x, 5);
d3.event.transform.x = Math.max(d3.event.transform.x, (1 - d3.event.transform.k) * width);
// console.log(d3.event.transform)
// update: rescale x axis
renderXAxis.call(axis.x.scale(d3.event.transform.rescaleX(scale.x)));
// Make sure that only the x axis is zoomed
heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale\((.*?)\)/, "scale($1, 1)"));
}
svg.call(renderPlot, dataset)
function renderPlot(selection, dataset){
//Do the axes
updateScales(dataset)
selection.select('.y.axis').call(axis.y)
selection.select('.x.axis')
.attr("transform", "translate(0," + scale.y(-0.5) + ")")
.call(axis.x)
// Do the chart
const update = heatDotsGroup.selectAll("ellipse")
.data(dataset);
update
.enter()
.append("ellipse")
.attr("cx", function (d) {return scale.x(d.xKey) - xBand.bandwidth();})
.attr("cy", function (d) {return scale.y(d.yKey) + yBand.bandwidth();})
.attr("rx", dotWidth)
.attr("ry", dotHeight)
.attr("fill", function (d) {
return colorScale(d.val);}
)
.merge(update).transition().duration(800);
update.exit().remove();
}
};
#clickMe{
height:50px;
width:150px;
background-color:lavender;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Heatmap Chart</title>
<!-- Reference style.css -->
<!-- <link rel="stylesheet" type="text/css" href="style.css">-->
<!-- Reference minified version of D3 -->
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
<script src='heatmap_v4.js' type='text/javascript'></script>
</head>
<body>
<input id="clickMe" type="button" value="click me to push new data" onclick="run();" />
<div id='chart'>
<svg width="700" height="500">
<g class="focus">
<g class="xaxis"></g>
<g class="yaxis"></g>
</g>
</svg>
</div>
<script>
function run() {
var dataset = [];
for (let i = 1; i < 360; i++) { //360
for (j = 1; j < 7; j++) { //75
dataset.push({
xKey: i,
xLabel: "xMark " + i,
yKey: j,
yLabel: "yMark " + j,
val: Math.random() * 25,
})
}
};
heatmap(dataset)
}
$(document).ready(function() {});
</script>
</body>
</html>
The issue is that you are not using the same selection each time you run the enter/exit/update cycle. When the button is pushed you:
Generate new data
Run the heatmap function
The heatmap function selects the svg and appends a fresh g called heatDotsGroup
The update function is called and passed the newly created g as a selection
The enter cycle appends everything because the new g is empty.
As a result both the exit and udpate cycles are empty. Try:
console.log(update.size(),update.exit().size()) // *Without any merge*
You should see both are empty each update. This is because all elements are entered each time, which why each update increases the number of ellipses.
I've pulled out a bunch of variable declarations and append statements from the heatmap function, things that only need to be run once (I could go further, but I just did a minimum). I've also merged your update and enter selection prior to setting attributes (as we want to set the new attributes if we update).The below snippet should demonstrate this change.
In the snippet, on button push the following happens:
Generate new data
Run the heatmap function
The heatmap function selects existing selections and doesn't append anything new
The update function is called and passed the selection used by previous update/enter/exit cycles containing any existing nodes.
The update function enters/exits/updates elements as needed based on the existing nodes.
Here's a working version based on the above:
// Things to set/append once:
var svg = d3.select("#chart")
.select("svg")
var margin = {top: 0, right: 25,bottom: 60, left: 75};
var width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
svg = 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 clip = svg.append("clipPath")
.attr("id", "clip")
.append("rect")
var heatDotsGroup = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("g");
var xAxis = svg.append("g").attr("class", "x axis");
var yAxis = svg.append("g").attr("class", "y axis")
function heatmap(dataset) {
var xLabels = [],
yLabels = [];
for (i = 0; i < dataset.length; i++) {
if (i==0){
xLabels.push(dataset[i].xLabel);
var j = 0;
while (dataset[j+1].xLabel == dataset[j].xLabel){
yLabels.push(dataset[j].yLabel);
j++;
}
yLabels.push(dataset[j].yLabel);
} else {
if (dataset[i-1].xLabel == dataset[i].xLabel){
//do nothing
} else {
xLabels.push(dataset[i].xLabel);
}
}
};
var dotSpacing = 0,
dotWidth = width/(2*(xLabels.length+1)),
dotHeight = height/(2*yLabels.length);
var daysRange = d3.extent(dataset, function (d) {return d.xKey}),
days = daysRange[1] - daysRange[0];
var hoursRange = d3.extent(dataset, function (d) {return d.yKey}),
hours = hoursRange[1] - hoursRange[0];
var tRange = d3.extent(dataset, function (d) {return d.val}),
tMin = tRange[0],
tMax = tRange[1];
var colors = ['#2C7BB6', '#00A6CA', '#00CCBC', '#90EB9D', '#FFFF8C', '#F9D057', '#F29E2E', '#E76818', '#D7191C'];
// the scale
var scale = {
x: d3.scaleLinear()
.range([-1, width]),
y: d3.scaleLinear()
.range([height, 0]),
};
var xBand = d3.scaleBand().domain(xLabels).range([0, width]),
yBand = d3.scaleBand().domain(yLabels).range([height, 0]);
var axis = {
x: d3.axisBottom(scale.x).tickFormat((d, e) => xLabels[d]),
y: d3.axisLeft(scale.y).tickFormat((d, e) => yLabels[d]),
};
function updateScales(data){
scale.x.domain([0, d3.max(data, d => d.xKey)]),
scale.y.domain([ 0, d3.max(data, d => d.yKey)])
}
var colorScale = d3.scaleQuantile()
.domain([0, colors.length - 1, d3.max(dataset, function (d) {return d.val;})])
.range(colors);
var zoom = d3.zoom()
.scaleExtent([1, dotHeight])
.on("zoom", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// SVG canvas
svg.call(zoom);
// Clip path
clip.attr("width", width)
.attr("height", height+dotHeight);
//Create X axis
var renderXAxis = xAxis
//.attr("transform", "translate(0," + scale.y(-0.5) + ")")
//.call(axis.x)
//Create Y axis
var renderYAxis = yAxis.call(axis.y);
function zoomed() {
d3.event.transform.y = 0;
d3.event.transform.x = Math.min(d3.event.transform.x, 5);
d3.event.transform.x = Math.max(d3.event.transform.x, (1 - d3.event.transform.k) * width);
// console.log(d3.event.transform)
// update: rescale x axis
renderXAxis.call(axis.x.scale(d3.event.transform.rescaleX(scale.x)));
// Make sure that only the x axis is zoomed
heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale\((.*?)\)/, "scale($1, 1)"));
}
svg.call(renderPlot, dataset)
function renderPlot(selection, dataset){
//Do the axes
updateScales(dataset)
selection.select('.y.axis').call(axis.y)
selection.select('.x.axis')
.attr("transform", "translate(0," + scale.y(-0.5) + ")")
.call(axis.x)
// Do the chart
const update = heatDotsGroup.selectAll("ellipse")
.data(dataset);
update
.enter()
.append("ellipse")
.merge(update)
.attr("cx", function (d) {return scale.x(d.xKey) - xBand.bandwidth();})
.attr("cy", function (d) {return scale.y(d.yKey) + yBand.bandwidth();})
.attr("rx", dotWidth)
.attr("ry", dotHeight)
.attr("fill", function (d) {
return colorScale(d.val);}
)
update.exit().remove();
}
};
#clickMe{
height:50px;
width:150px;
background-color:lavender;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Heatmap Chart</title>
<!-- Reference style.css -->
<!-- <link rel="stylesheet" type="text/css" href="style.css">-->
<!-- Reference minified version of D3 -->
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
<script src='heatmap_v4.js' type='text/javascript'></script>
</head>
<body>
<input id="clickMe" type="button" value="click me to push new data" onclick="run();" />
<div id='chart'>
<svg width="700" height="500">
<g class="focus">
<g class="xaxis"></g>
<g class="yaxis"></g>
</g>
</svg>
</div>
<script>
function run() {
var dataset = [];
for (let i = 1; i < 360; i++) { //360
for (j = 1; j < 7; j++) { //75
dataset.push({
xKey: i,
xLabel: "xMark " + i,
yKey: j,
yLabel: "yMark " + j,
val: Math.random() * 25,
})
}
};
heatmap(dataset)
}
$(document).ready(function() {});
</script>
</body>
</html>
The exit selection here is still empty as the size of the data array is fixed. D3 assumes the new data replaces the old, but it can't know that the new data should be represented as new elements, unless of course we specify a key function as noted in a now deleted comment. This may or may not be the desired functionality that you want.
I have a slightly different approach than Andrew.
A bunch of global variables will get messy when you have multiple charts.
When you click the button:
call the renderPlot(dataset)
check if we have a #clip element in the svg
if not: call heatmap(dataset)
Construct all the static stuff and append to the svg.
append a datum object to the svg with the variables needed for the update
fetch the datum from the svg
update the content of the svg using the datum object
function heatmap(dataset) {
var svg = d3.select("#chart")
.select("svg");
var xLabels = [],
yLabels = [];
for (i = 0; i < dataset.length; i++) {
if (i==0){
xLabels.push(dataset[i].xLabel);
var j = 0;
while (dataset[j+1].xLabel == dataset[j].xLabel){
yLabels.push(dataset[j].yLabel);
j++;
}
yLabels.push(dataset[j].yLabel);
} else {
if (dataset[i-1].xLabel == dataset[i].xLabel){
//do nothing
} else {
xLabels.push(dataset[i].xLabel);
}
}
};
var margin = {top: 0, right: 25,
bottom: 60, left: 75};
var width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var dotSpacing = 0,
dotWidth = width/(2*(xLabels.length+1)),
dotHeight = height/(2*yLabels.length);
var daysRange = d3.extent(dataset, function (d) {return d.xKey}),
days = daysRange[1] - daysRange[0];
var hoursRange = d3.extent(dataset, function (d) {return d.yKey}),
hours = hoursRange[1] - hoursRange[0];
var tRange = d3.extent(dataset, function (d) {return d.val}),
tMin = tRange[0],
tMax = tRange[1];
var colors = ['#2C7BB6', '#00A6CA', '#00CCBC', '#90EB9D', '#FFFF8C', '#F9D057', '#F29E2E', '#E76818', '#D7191C'];
// the scale
var scale = {
x: d3.scaleLinear()
.range([-1, width]),
y: d3.scaleLinear()
.range([height, 0]),
};
var xBand = d3.scaleBand().domain(xLabels).range([0, width]),
yBand = d3.scaleBand().domain(yLabels).range([height, 0]);
var axis = {
x: d3.axisBottom(scale.x).tickFormat((d, e) => xLabels[d]),
y: d3.axisLeft(scale.y).tickFormat((d, e) => yLabels[d]),
};
var colorScale = d3.scaleQuantile()
.domain([0, colors.length - 1, d3.max(dataset, function (d) {return d.val;})])
.range(colors);
var zoom = d3.zoom()
.scaleExtent([1, dotHeight])
.on("zoom", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// SVG canvas
svg .attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Clip path
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height+dotHeight);
// Heatmap dots
var heatDotsGroup = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("g");
//Create X axis
var renderXAxis = svg.append("g")
.attr("class", "x axis")
//.attr("transform", "translate(0," + scale.y(-0.5) + ")")
//.call(axis.x)
//Create Y axis
var renderYAxis = svg.append("g")
.attr("class", "y axis")
.call(axis.y);
function zoomed() {
d3.event.transform.y = 0;
d3.event.transform.x = Math.min(d3.event.transform.x, 5);
d3.event.transform.x = Math.max(d3.event.transform.x, (1 - d3.event.transform.k) * width);
// console.log(d3.event.transform)
// update: rescale x axis
renderXAxis.call(axis.x.scale(d3.event.transform.rescaleX(scale.x)));
// Make sure that only the x axis is zoomed
heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale\((.*?)\)/, "scale($1, 1)"));
}
var chartData = {};
chartData.scale = scale;
chartData.axis = axis;
chartData.xBand = xBand;
chartData.yBand = yBand;
chartData.colorScale = colorScale;
chartData.heatDotsGroup = heatDotsGroup;
chartData.dotWidth = dotWidth;
chartData.dotHeight = dotHeight;
svg.datum(chartData);
//svg.call(renderPlot, dataset)
}
function updateScales(data, scale){
scale.x.domain([0, d3.max(data, d => d.xKey)]),
scale.y.domain([0, d3.max(data, d => d.yKey)])
}
function renderPlot(dataset){
var svg = d3.select("#chart")
.select("svg");
if (svg.select("#clip").empty()) { heatmap(dataset); }
chartData = svg.datum();
//Do the axes
updateScales(dataset, chartData.scale);
svg.select('.y.axis').call(chartData.axis.y)
svg.select('.x.axis')
.attr("transform", "translate(0," + chartData.scale.y(-0.5) + ")")
.call(chartData.axis.x)
// Do the chart
const update = chartData.heatDotsGroup.selectAll("ellipse")
.data(dataset);
update
.enter()
.append("ellipse")
.attr("rx", chartData.dotWidth)
.attr("ry", chartData.dotHeight)
.merge(update)
.transition().duration(800)
.attr("cx", function (d) {return chartData.scale.x(d.xKey) - chartData.xBand.bandwidth();})
.attr("cy", function (d) {return chartData.scale.y(d.yKey) + chartData.yBand.bandwidth();})
.attr("fill", function (d) { return chartData.colorScale(d.val);} );
update.exit().remove();
}
#clickMe{
height:50px;
width:150px;
background-color:lavender;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Heatmap Chart</title>
<!-- Reference style.css -->
<!-- <link rel="stylesheet" type="text/css" href="style.css">-->
<!-- Reference minified version of D3 -->
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
<script src='heatmap_v4.js' type='text/javascript'></script>
</head>
<body>
<input id="clickMe" type="button" value="click me to push new data" onclick="run();" />
<div id='chart'>
<svg width="700" height="500">
<g class="focus">
<g class="xaxis"></g>
<g class="yaxis"></g>
</g>
</svg>
</div>
<script>
function run() {
var dataset = [];
for (let i = 1; i < 360; i++) { //360
for (j = 1; j < 7; j++) { //75
dataset.push({
xKey: i,
xLabel: "xMark " + i,
yKey: j,
yLabel: "yMark " + j,
val: Math.random() * 25,
})
}
};
renderPlot(dataset)
}
$(document).ready(function() {});
</script>
</body>
</html>

How can I implement an invert function for a point scale?

I am trying to add a tooltip for my dual line chart graph.
However, instead of using timeScale or scaleLinear, I used scalePoint to graph my chart.
I am trying to achieve the following effect:
https://bl.ocks.org/mbostock/3902569
this.x = d3.scalePoint().range([ this.margin.left, this.width - this.margin.right ]);
this.xAxis = d3.axisBottom(this.x);
this.x.domain(
this.dataArray.map(d => {
return this.format(d[ 'year' ]);
}));
Here is my mouseover function,
function mousemove() {
//d3.mouse(this)[ 0 ]
//x.invert
var x0 = d3.mouse(this)[ 0 ],
i = bisectDate(data, x0, 1),
d0 = data[ i - 1 ],
d1 = data[ i ],
d = x0 - d0.year > d1.year - x0 ? d1 : d0;
console.log(x0);
// focus.attr("transform", "translate(" + x(format(d.year)) + "," + y(d.housing_index_change) + ")");
// focus.select("text").text(d.housing_index_change);
}
Since I am using scalePoint, there is obviously no invert function to map the coordinates to my data. and I am only retrieving the first element in the array and it is the only one that is being display regardless of the position of the mouse.
So my question is, how can I implement the invert functionality here while still using scalePoint?
Thank you :)
You are right, there is no invert for a point scale. But you can create your own function to get the corresponding domain of a given x position:
function scalePointPosition() {
var xPos = d3.mouse(this)[0];
var domain = xScale.domain();
var range = xScale.range();
var rangePoints = d3.range(range[0], range[1], xScale.step())
var yPos = domain[d3.bisect(rangePoints, xPos) -1];
console.log(yPos);
}
Step by step explanation
First, we get the x mouse position.
var xPos = d3.mouse(this)[0];
Then, based on your scale's range and domain...
var domain = xScale.domain();
var range = xScale.range();
...we create an array with all the steps in the point scale using d3.range:
var rangePoints = d3.range(range[0], range[1], xScale.step())
Finally, we get the corresponding domain using bisect:
var yPos = domain[d3.bisect(rangePoints, xPos) -1];
Check the console.log in this demo:
var data = [{
A: "groupA",
B: 10
}, {
A: "groupB",
B: 20
}, {
A: "groupC",
B: 30
}, {
A: "groupD",
B: 10
}, {
A: "groupE",
B: 17
}]
var width = 500,
height = 200;
var svg = d3.selectAll("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var color = d3.scaleOrdinal(d3.schemeCategory10)
.domain(data.map(function(d) {
return d.A
}));
var xScale = d3.scalePoint()
.domain(data.map(function(d) {
return d.A
}))
.range([50, width - 50])
.padding(0.5);
var yScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {
return d.B
}) * 1.1])
.range([height - 50, 10]);
var circles = svg.selectAll(".circles")
.data(data)
.enter()
.append("circle")
.attr("r", 8)
.attr("cx", function(d) {
return xScale(d.A)
})
.attr("cy", function(d) {
return yScale(d.B)
})
.attr("fill", function(d) {
return color(d.A)
});
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale);
svg.append("g").attr("transform", "translate(0,150)")
.attr("class", "xAxis")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(50,0)")
.attr("class", "yAxis")
.call(yAxis);
svg.append("rect")
.attr("opacity", 0)
.attr("x", 50)
.attr("width", width - 50)
.attr("height", height)
.on("mousemove", scalePointPosition);
function scalePointPosition() {
var xPos = d3.mouse(this)[0];
var domain = xScale.domain();
var range = xScale.range();
var rangePoints = d3.range(range[0], range[1], xScale.step())
var yPos = domain[d3.bisect(rangePoints, xPos) - 1];
console.log(yPos);
}
.as-console-wrapper { max-height: 20% !important;}
<script src="https://d3js.org/d3.v4.min.js"></script>

d3 stack bar chart in version 4

I read "Interactive Data Visualization for the web" by Scott Murray, but this book use d3 version3. I've tried to fix it, but some problems happen, and my code is below. I got errors about "y: Expected length, "NaN".", and maybe my stack function doesn't work. However, I don't know how to solve it. I need someone to help me.
// declare variable
var svgWidth = 500,
svgHeight = 300,
svgData = [],
maxValue = 16;
svgData = getData(svgData);
// set stack color
var color = d3.scaleOrdinal(d3.schemeCategory10);
// create stack layout
var stack = d3.stack();
stack(svgData);
// define x,y scale
var xScale = d3.scaleBand()
.domain(d3.range(svgData[0].length))
.rangeRound([0, svgWidth])
.paddingInner(0.05),
yScale = d3.scaleLinear()
.domain([0, d3.max(svgData, function(d){
return d3.max(d, function(d){
d.y0 + d.y;
});
})])
.range([0, svgHeight])
.clamp(true);
// create svg
var svg = d3.select("body")
.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight);
// add group and fill color for each row of data
var group = svg.selectAll("g")
.data(svgData)
.enter()
.append("g")
.style("fill", function(d, i){
return color(i);
});
// add a rect for each data value
var rect = group.selectAll("rect")
.data(function(d){
return d;
})
.enter()
.append("rect")
.attr("x", function(d, i){
return xScale(i);
})
.attr("y", function(d){
return yScale(d.y0);
})
.attr("width", xScale.bandwidth())
.attr("height", function(d){
return yScale(d.y);
});
// get data
function getData(data){
var temp =0,
tempArr = [];
data = [];
for(var i=0; i<3; i++){
tempArr = [];
for(var j=0; j<5; j++){
temp = Math.round(Math.random() *maxValue);
tempArr.push( { x: j, y: temp });
}
data.push(tempArr);
}
return data;
}

Categories