Related
I have a project where I need to display a doughnut chart. For every slice in the chart there is a corresponding icon in the legend. This icon should also been shown on the slice itself inside the chart.
I have found a working example online on how to display images on doughnut charts: Working example. I have tried to implement this solution into my own project. The images get loaded in and when I inspect the SVG each path node(slice) contains an image element with the correct image. But the images don't show up on the graph.
This is the code i am running atm. If you have some pointers on how to improve my overall code then you're welcome to do so. I am still new to D3.JS and learning a lot about it at the moment:
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="https://unpkg.com/vue"></script>
<script src="https://d3js.org/d3.v6.js"></script>
</head>
<body>
<div class="p-3 flex flex-col" id="one">
<div class="w-full flex-1">
<div id="my_dataviz"></div>
</div>
</div>
<script>
new Vue({
el: '#one',
data: {
type: Array,
required: true,
}, mounted() {
// set the dimensions and margins of the graph
var width = 450;
var height = 450;
var margin = 1;
var image_width = 32;
var image_height = 32;
var data = [
{
key: "One",
value: 20,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Two",
value: 30,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Three",
value: 10,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Four",
value: 15,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
}
]
// The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin.
// append the svg object to the div called 'my_dataviz'
var svg = d3
.select('#my_dataviz')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr(
'transform',
'translate(' + width / 2 + ',' + height / 2 + ')'
);
var radius = Math.min(width, height) / 2 - margin;
// set the color scale
var color = d3
.scaleOrdinal()
.domain(
data.map(function(d) {
return d["key"];
})
)
.range(["#206BF3"]);
// Compute the position of each group on the pie:
var pie = d3.pie().value(function(d) {
return d[1];
});
var data_ready = pie(
data.map(function(d) {
return [d["key"], d["value"], d["icon"]];
})
);
// declare an arc generator function
var arc = d3
.arc()
.outerRadius(100)
.innerRadius(50);
console.log(arc);
// Build the pie chart: Basically, each part of the pie is a path that we build using the arc function.
var paths = svg
.selectAll("whatever")
.data(data_ready)
.enter()
.append("path")
.attr("d", d => {
return arc(d);
})
.attr("fill", function(d) {
return color(d.data[0]);
})
.attr("stroke", "#2D3546")
.style("stroke-width", "2px")
.style("opacity", 0.7);
paths
.append("svg:image")
.attr("transform", function(d) {
var x = arc.centroid(d)[0] - image_width / 2;
var y = arc.centroid(d)[1] - image_height / 2;
return "translate(" + width / 2 + x + "," + height + y + ")";
})
.attr("xlink:href", function(d) {
console.log(d);
return d.data[2];
})
.attr("width", image_width)
.attr("height", image_height);
paths.on("mouseover", e => {
this.pathAnim(radius, d3.select(e.currentTarget), 1);
});
paths.on("mouseout", e => {
var thisPath = d3.select(e.currentTarget);
if (!thisPath.classed("clicked")) {
this.pathAnim(radius, thisPath, 0);
}
});
},
methods: {
pathAnim(radius, path, dir) {
switch (dir) {
case 0:
path
.transition()
.duration(500)
.ease(d3.easeBounce)
.attr(
"d",
d3
.arc()
.innerRadius(100)
.outerRadius(50)
);
path.style("fill", "#206BF3");
break;
case 1:
path.transition().attr(
"d",
d3
.arc()
.innerRadius(50)
.outerRadius(110)
);
path.style("fill", "white");
break;
}
}
}
});
</script>
</body>
</html>
A <path> element cannot contain an <image>. Instead of that, use the data to create <g> elements and append both the <path> and the <image> to them:
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="https://unpkg.com/vue"></script>
<script src="https://d3js.org/d3.v6.js"></script>
</head>
<body>
<div class="p-3 flex flex-col" id="one">
<div class="w-full flex-1">
<div id="my_dataviz"></div>
</div>
</div>
<script>
new Vue({
el: '#one',
data: {
type: Array,
required: true,
},
mounted() {
// set the dimensions and margins of the graph
var width = 450;
var height = 450;
var margin = 1;
var image_width = 32;
var image_height = 32;
var data = [{
key: "One",
value: 20,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Two",
value: 30,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Three",
value: 10,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Four",
value: 15,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
}
]
// The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin.
// append the svg object to the div called 'my_dataviz'
var svg = d3
.select('#my_dataviz')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr(
'transform',
'translate(' + width / 2 + ',' + height / 2 + ')'
);
var radius = Math.min(width, height) / 2 - margin;
// set the color scale
var color = d3
.scaleOrdinal()
.domain(
data.map(function(d) {
return d["key"];
})
)
.range(["#206BF3"]);
// Compute the position of each group on the pie:
var pie = d3.pie().value(function(d) {
return d[1];
});
var data_ready = pie(
data.map(function(d) {
return [d["key"], d["value"], d["icon"]];
})
);
// declare an arc generator function
var arc = d3
.arc()
.outerRadius(100)
.innerRadius(50);
// Build the pie chart: Basically, each part of the pie is a path that we build using the arc function.
var g = svg
.selectAll("whatever")
.data(data_ready)
.enter()
.append("g")
.attr("transform", function(d) {
var x = arc.centroid(d)[0] - image_width / 2;
var y = arc.centroid(d)[1] - image_height / 2;
return "translate(" + width / 2 + x + "," + height + y + ")";
});
g.append("path")
.attr("d", d => {
return arc(d);
})
.attr("fill", function(d) {
return color(d.data[0]);
})
.attr("stroke", "#2D3546")
.style("stroke-width", "2px")
.style("opacity", 0.7);
g.append("svg:image")
.attr("transform", function(d) {
var x = arc.centroid(d)[0] - image_width / 2;
var y = arc.centroid(d)[1] - image_height / 2;
return "translate(" + x + "," + y + ")";
})
.attr("xlink:href", function(d) {
return d.data[2];
})
.attr("width", image_width)
.attr("height", image_height);
g.on("mouseover", e => {
this.pathAnim(radius, d3.select(e.currentTarget), 1);
});
g.on("mouseout", e => {
var thisPath = d3.select(e.currentTarget);
if (!thisPath.classed("clicked")) {
this.pathAnim(radius, thisPath, 0);
}
});
},
methods: {
pathAnim(radius, path, dir) {
switch (dir) {
case 0:
path
.transition()
.duration(500)
.ease(d3.easeBounce)
.attr(
"d",
d3
.arc()
.innerRadius(100)
.outerRadius(50)
);
path.style("fill", "#206BF3");
break;
case 1:
path.transition().attr(
"d",
d3
.arc()
.innerRadius(50)
.outerRadius(110)
);
path.style("fill", "white");
break;
}
}
}
});
</script>
</body>
</html>
I've updated #GerardoFurtado's code. I just moved the events to the paths and added pointer-events: none for images. Transitions work well.
g image {
pointer-events: none;
}
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="https://unpkg.com/vue"></script>
<script src="https://d3js.org/d3.v6.js"></script>
</head>
<body>
<div class="p-3 flex flex-col" id="one">
<div class="w-full flex-1">
<div id="my_dataviz"></div>
</div>
</div>
<script>
new Vue({
el: '#one',
data: {
type: Array,
required: true,
},
mounted() {
// set the dimensions and margins of the graph
var width = 450;
var height = 450;
var margin = 1;
var image_width = 32;
var image_height = 32;
var data = [{
key: "One",
value: 20,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Two",
value: 30,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Three",
value: 10,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Four",
value: 15,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
}
]
// The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin.
// append the svg object to the div called 'my_dataviz'
var svg = d3
.select('#my_dataviz')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr(
'transform',
'translate(' + width / 2 + ',' + height / 2 + ')'
);
var radius = Math.min(width, height) / 2 - margin;
// set the color scale
var color = d3
.scaleOrdinal()
.domain(
data.map(function(d) {
return d["key"];
})
)
.range(["#206BF3"]);
// Compute the position of each group on the pie:
var pie = d3.pie().value(function(d) {
return d[1];
});
var data_ready = pie(
data.map(function(d) {
return [d["key"], d["value"], d["icon"]];
})
);
// declare an arc generator function
var arc = d3
.arc()
.outerRadius(100)
.innerRadius(50);
// Build the pie chart: Basically, each part of the pie is a path that we build using the arc function.
var g = svg
.selectAll("whatever")
.data(data_ready)
.enter()
.append("g")
/* I commented this lines and nothing changed.
.attr("transform", function(d) {
var x = arc.centroid(d)[0] - image_width / 2;
var y = arc.centroid(d)[1] - image_height / 2;
return "translate(" + width / 2 + x + "," + height + y + ")";
});
*/
g.append("path")
.attr("d", d => {
return arc(d);
})
.attr("fill", function(d) {
return color(d.data[0]);
})
.attr("stroke", "#2D3546")
.style("stroke-width", "2px")
.style("opacity", 0.7)
.on("mouseover", e => {
console.log(this)
this.pathAnim(radius, d3.select(e.currentTarget), 1);
})
.on("mouseout", e => {
var thisPath = d3.select(e.currentTarget);
if (!thisPath.classed("clicked")) {
this.pathAnim(radius, thisPath, 0);
}
});
g.append("svg:image")
.attr("transform", function(d) {
var x = arc.centroid(d)[0] - image_width / 2;
var y = arc.centroid(d)[1] - image_height / 2;
return "translate(" + x + "," + y + ")";
})
.attr("xlink:href", function(d) {
return d.data[2];
})
.attr("width", image_width)
.attr("height", image_height);
},
methods: {
pathAnim(radius, path, dir) {
switch (dir) {
case 0:
path
.transition()
.duration(500)
.ease(d3.easeBounce)
.attr(
"d",
d3
.arc()
.innerRadius(100)
.outerRadius(50)
);
path.style("fill", "#206BF3");
break;
case 1:
path.transition().attr(
"d",
d3
.arc()
.innerRadius(50)
.outerRadius(110)
);
path.style("fill", "white");
break;
}
}
}
});
</script>
</body>
</html>
I tried to have a beeswarm plot shift datapoint location to no avail. First, I made a simple scatter plot which can transition except x axis overlays on toggling.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>D3: Transitioning points to randomized values, plus rescaled axes!</title>
<script src="https://d3js.org/d3.v4.min.js"></script><style type="text/css">
/* No style rules here yet */
</style>
</head>
<body>
<label>
<input type="radio" name='market' value="a" checked/>a
<input type="radio" name='market' value="b"/>b
</label>
<p>Click on this text to update the chart with new data values as many times as you like!</p>
<script type="text/javascript">
//Width and height
var w = 500;
var h = 300;
var padding = 30;
//Dynamic, random dataset
dataset = [
{"id":1,
"value":20,
"group":"a"},
{"id":1,
"value":10,
"group":"a"},
{"id":1,
"value":30,
"group":"a"},
{"id":1,
"value":40,
"group":"a"},
{"id":1,
"value":42,
"group":"a"},
{"id":1,
"value":10,
"group":"b"},
{"id":1,
"value":12,
"group":"b"},
{"id":1,
"value":15,
"group":"b"},
{"id":1,
"value":23,
"group":"b"},
{"id":1,
"value":22,
"group":"b"},
{"id":1,
"value":54,
"group":"b"}
]
//Create scale functions
var xScale = d3.scaleLinear()
.range([padding, w - padding * 2]);
var yScale = d3.scaleLinear()
.range([h - padding, padding]);
//Define X axis
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(5);
//Define Y axis
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
draw(dataset.filter(d=> d.group=="a"));
//////////////////
//toggle//
//////////////////
d3.selectAll("input")
.on("change", function() {
var data_new = dataset.filter(d => (d.group == this.value));
draw(data_new);
});
//////////////////
//Create circles
//////////////////
function draw(dataset) {
//////////////////
//Create axis
xScale.domain([0, d3.max(dataset, function(d) { return d.value; })])
yScale.domain([0, 2])
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(5);
//Create X axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (h - padding) + ")")
.transition(2000)
.call(xAxis);
//Create Y axis
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
//////////////////
//draw circle
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d.value);
})
.attr("cy", function(d) {
return yScale(d.id);
})
.attr("r", 3)
.attr("opacity",0.2);
//update
svg.selectAll("circle")
.transition()
.duration(1000)
.attr("cx", function(d) {
return xScale(d.value);
})
}
</script>
</body>
</html>
However when apply a similar code for beeswarm plot, the points don't shift location on selection, they just layer on, like the x axis in the first example:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
</style>
<label>
<input type="radio" name='market' value="a" checked/>a
<input type="radio" name='market' value="b"/>b
</label>
<svg width="400" height="200"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var svg = d3.select("svg"),
margin = {top: 40, right: 40, bottom: 40, left: 40},
width = svg.attr("width") - margin.left - margin.right,
height = svg.attr("height") - margin.top - margin.bottom;
var formatValue = d3.format(",d");
var x = d3.scaleLog()
.rangeRound([0, width]);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
data = [
{"id":1,
"value":20,
"group":"a"},
{"id":1,
"value":21,
"group":"a"},
{"id":1,
"value":30,
"group":"a"},
{"id":1,
"value":32,
"group":"a"},
{"id":1,
"value":42,
"group":"a"},
{"id":1,
"value":10,
"group":"b"},
{"id":1,
"value":12,
"group":"b"},
{"id":1,
"value":15,
"group":"b"},
{"id":1,
"value":23,
"group":"b"},
{"id":1,
"value":22,
"group":"b"},
{"id":1,
"value":24,
"group":"b"}
]
//default
draw(data.filter(d=> d.group=="a"));
d3.selectAll("input")
.on("change", function()
{
var newdata = draw(data.filter(d=> d.group==this.value));
draw(newdata)
} )
/////////////////
//draw swarmplot
/////////////////
function draw(data) {
// transition
var t = d3.transition()
.duration(750);
x.domain(d3.extent(data, d=> d.value));
var simulation = d3.forceSimulation(data)
.force("x", d3.forceX(function(d) { return x(d.value); }).strength(1))
.force("y", d3.forceY(height / 3))
.force("collide", d3.forceCollide(18))
.stop();
for (var i = 0; i < 120; ++i) simulation.tick();
//axis
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).ticks(20, ".0s"));
//for mouse-over
var cell = g.append("g")
.attr("class", "cells")
.selectAll("g").data(d3.voronoi()
.extent([[-margin.left, -margin.top], [width + margin.right, height + margin.top]])
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.polygons(data)).enter().append("g");
//circle
cell.append("circle")
.attr("r", 3)
.attr("cx", function(d) { return d.data.x; })
.attr("cy", function(d) { return d.data.y; })
.attr("fill", d => (d.data.Food_Sub_Group))
.attr("opacity", 0.4);
//update circle
cell.selectAll("circle")
.transition()
.duration(1000)
.attr("cx", function(d) { return d.data.x; })
}
</script>
Anything amiss here? Thanks.
Here is the difference:
In draw function of the first code, you bind data to 'circle' elements. If you have 5 circle elements before and new data size is 5, then no element is added, what changed is only data. If you have 5 circles before and new data size is 6, then with 'enter()' and 'append' function, after execution, you will have 6 elements.
In draw function of the second code, you add new elements. Every call to draw function adds new points. If you have 5 circle elements before and new data size is 5, then 5 elements are added, you will have 10 elements.
What you say 'no shift location' is actually multiple points be plotted in same place(the point seems to be darker) or different places(more points).
See data binding in d3 which may help.
I modified some d3 filtering blocks with the intention to filter by year (ideally through a slider instead of having a button for each year) but encountered two issues:
upon calling filter() and exit().remove() nothing happens
how can i add a slider or autoplay across the years? I looked through some examples but they're a bit complex for beginner.
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://d3js.org/d3.v4.min.js"></script>
<meta charset="utf-8">
<title>D3: Loading data from a CSV file</title>
</head>
<body>
<p class="s12"> select 2012 </p>
<p class="s16"> select 2016 </p>
<script type="text/javascript">
var margin = {top: 20, right: 20, bottom: 30, left: 40},
w = 600 - margin.left - margin.right,
h = 300 - margin.top - margin.bottom;
var padding = 40;
var data = [{
"Food": "Apples",
"Deliciousness": 9,
"year": 2010
}, {
"Food": "Green Beans",
"Deliciousness": 5,
"year": 2012
}, {
"Food": "Egg Salad Sandwich",
"Deliciousness": 4,
"year": 2016
}, {
"Food": "Cookies",
"Deliciousness": 10,
"year": 2018
} ];
// format the data
data.forEach(function(d) {
d.Deliciousness = +d.Deliciousness;
});
var svg = d3.select("body")
.append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left+"," +
margin.top+")");
var xScale = d3.scaleBand()
.domain(d=>d.Food)
.range([margin.left,w+margin.left])
.paddingInner(0.2);
xScale.domain(data.map(function(d) { return d.Food; }));
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(5);
var yScale = d3.scaleLinear()
.domain([0, d3.max(data, d=>d.Deliciousness)])
.rangeRound([h+margin.top,margin.top]);
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x',(d,i) => margin.left + i*w/data.length)
.attr('y',d=>yScale(d.Deliciousness))
.attr('width', xScale.bandwidth())
.attr('height',d =>h-yScale(d.Deliciousness))
.attr('fill',function(d){
if (d===30) return "red";
return "rgb(0,0,"+d.Deliciousness*10+")" ;});
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
d3.select(".s12")
.on("click", function() {
svg.selectAll("rect")
.filter(function(d) {
return d.year !== 2012;
})
.remove();
d3.select(".s16")
.on("click", function() {
svg.selectAll("rect")
.filter(function(d) {
return d.year !== 2016;
})
.remove();
});
</script>
</body>
</html>
code updated
Firstly, fix the class problem explained in the comments section.
Your problem here is the logic of this block:
svg.selectAll("rect")
.data(data.filter(function(d) {
return d.year == 2012;
}))
.exit().remove();
It makes little sense: what you're doing is filtering the data array by year (2012), which will return just 1 object. Then, you're selecting all the rectangles, binding a new data array with just one object (which will leave all the other rectangles without any data) and then calling remove on the exit selection. That's not correct.
Since you don't have a new data, just filter the selection:
svg.selectAll("rect")
.filter(function(d) {
return d.year !== 2012;
})
.remove();
That way, you simply remove the rectangles that don't represent 2012.
This is the updated code:
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 40
},
w = 600 - margin.left - margin.right,
h = 300 - margin.top - margin.bottom;
var padding = 40;
var data = [{
"Food": "Apples",
"Deliciousness": 9,
"year": 2010
}, {
"Food": "Green Beans",
"Deliciousness": 5,
"year": 2012
}, {
"Food": "Egg Salad Sandwich",
"Deliciousness": 4,
"year": 2016
}, {
"Food": "Cookies",
"Deliciousness": 10,
"year": 2018
}];
// format the data
data.forEach(function(d) {
d.Deliciousness = +d.Deliciousness;
});
var svg = d3.select("body")
.append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," +
margin.top + ")");
var xScale = d3.scaleBand()
.domain(d => d.Food)
.range([margin.left, w + margin.left])
.paddingInner(0.2);
xScale.domain(data.map(function(d) {
return d.Food;
}));
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(5);
var yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.Deliciousness)])
.rangeRound([h + margin.top, margin.top]);
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', (d, i) => margin.left + i * w / data.length)
.attr('y', d => yScale(d.Deliciousness))
.attr('width', xScale.bandwidth())
.attr('height', d => h - yScale(d.Deliciousness))
.attr('fill', function(d) {
if (d === 30) return "red";
return "rgb(0,0," + d.Deliciousness * 10 + ")";
});
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
d3.select(".s12")
.on("click", function() {
svg.selectAll("rect")
.filter(function(d) {
return d.year !== 2012;
})
.remove();
});
<script src="https://d3js.org/d3.v4.min.js"></script>
<p class="s12"> select 2012 </p>
<p class="s16"> select 2016 </p>
I'm new to D3 and getting the following error in my demo script -
FirstD3.jsp:31 Uncaught TypeError: Cannot read property 'linear' of undefined
My demo code is as follow
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<title>Linear Scales</title>
<style type="text/css">
</style>
</head>
<body>
<script type="text/javascript">
var dataset = [
[ 5, 20 ],
[ 460, 90 ],
[ 250, 50 ],
[ 100, 33 ],
[ 330, 95 ],
[ 410, 12 ],
[ 468, 44 ],
[ 25, 67 ],
[ 85, 21 ],
[ 220, 88 ]
];
var w = 500;
var h = 100;
var xScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) { return d[0]; })])
.range([0, w]);
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) { return d[1]; })])
.range([0, h]);
var svg = d3.select("body")
.append("svg")
.attr("height", h)
.attr("width", w);
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function (d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r", function (d) {
return Math.sqrt(h-(d[1]));
});
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function (d) {
return d[0]+","+d[1];
})
.attr("x", function (d) {
return xScale(d[0]);
})
.attr("y", function (d) {
return yScale(d[1]);
})
.attr("font-size", "11px")
.attr("fill", "red");
</script>
</body>
</html>
what is causing this error? and how to solve it
In D3 v4 it is no longer named d3.scale.linear(). Use d3.scaleLinear() instead.
Thanks for the answer, I've solved this error for the following code (I'm studing D3.js) :
var colorScale = d3.scale.linear().domain([0, 100]).range(["#add8e6", "blue"]);
With the following error (inside the web page) :
Uncaught TypeError: Cannot read property 'linear' of undefined at index.html:22
<!DOCTYPE html>
<html lang="en">
<head>
<style>
.axis path {
fill: none;
stroke: #777;
shape-rendering: crispEdges;
}
.axis text {
font: 10px sans-serif;
}
</style>
</head>
<body>
<svg id="visualization" width="1000" height="500"></svg>
</body>
</html>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
function InitChart() {
var data = [{
"sale": "215",
"calendarDate": "20140302"
}, {
"sale": "179",
"calendarDate": "20140303"
}, {
"sale": "199",
"calendarDate": "20140304"
}, {
"sale": "134",
"calendarDate": "20140305"
}, {
"sale": "176",
"calendarDate": "20140306"
}];
var vis = d3.select("#visualization"),
WIDTH = 1000,
HEIGHT = 500,
MARGINS = {
top: 20,
right: 20,
bottom: 20,
left: 50
},
calendarDatesAsStrings = ['02.03.2014','03.03.2014','04.03.2014','05.03.2014','06.03.2014'],
xScale = d3.scale.linear().range([MARGINS.left, WIDTH - MARGINS.right]).domain([20140302, 20140306]),
yScale = d3.scale.linear().range([HEIGHT - MARGINS.top, MARGINS.bottom]).domain([124, 225]),
xAxis = d3.svg.axis().scale(xScale),
yAxis = d3.svg.axis().scale(yScale).orient("left");
vis.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (HEIGHT - MARGINS.bottom) + ")")
.call(xAxis)
.selectAll('text')
.text(function (d, i) { return calendarDatesAsStrings[i]; });
vis.append("svg:g")
.attr("class", "y axis")
.attr("transform", "translate(" + (MARGINS.left) + ",0)")
.call(yAxis);
var lineGen = d3.svg.line()
.x(function(d) {
return xScale(d.calendarDate);
})
.y(function(d) {
return yScale(d.sale);
});
vis.append('svg:path')
.attr('d', lineGen(data))
.attr('stroke', 'green')
.attr('stroke-width', 2)
.attr('fill', 'none');
}
InitChart();
</script>
http://jsbin.com/zupejizeho/1/edit
I expected the labels to be in their right positions, they aren't.
Instead, they look like they are "compressed" and placed every half-segment instead of full segment.
What am I doing wrong?
You have a decision to make here. Treat your dates as strings and use an ordinal scale or you treat your dates as dates and use a time scale. Right now you are using a linear scale which does not map nicely to date stamps.
Since your dataset is pretty limited an ordinal scale is probably easier:
// map all my dates to an ordinal
// with points separated nicely in my range
xScale = d3.scale.ordinal()
.rangePoints([MARGINS.left, WIDTH - MARGINS.right])
.domain(data.map(function(d){ return d.calendarDate })),
And format the date strings:
// then reformat the strings to what you want with some format helpers
oldFormat = d3.time.format("%Y%m%d"),
newFormat = d3.time.format("%m.%d.%Y");
xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.tickFormat(function(d){
var dateTime = oldFormat.parse(d); // parse the string to date
return newFormat(dateTime); // format it back to appropriate string
}),
Updated example here.
If you get a little more data complexity, you'll probably want to shift to really treating your dates as dates. So, first, fix your data:
data = data.map(function(d){
return {
sale: +d.sale, // this is really a numeric
calendarDate: oldFormat.parse(d.calendarDate) // this be a datetime now
}
});
Set up your scale with time:
xScale = d3.time.scale()
.range([MARGINS.left, WIDTH - MARGINS.right])
.domain(
[d3.min(data.map(function(d){ return d.calendarDate })),
d3.max(data.map(function(d){ return d.calendarDate }))]
),
And the format becomes simpler:
xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.tickFormat(d3.time.format("%m.%d.%Y")),
Here an example of this.
you don't need to add the last two rows after the call of the xAxis, here:
vis.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (HEIGHT - MARGINS.bottom) + ")")
.call(xAxis)
.selectAll('text')
.text(function (d, i) { return calendarDatesAsStrings[i]; });
you simply have to call the xAxis and that's it, working link: http://jsbin.com/yabecihihi/1/edit?html,output (the dates need to be parsed properly with a time scale tho)