I am so close this is killing me. I've generated a simple brush for one column and it's generating the limits it's set to perfectly. The thing is I'd like multiple brushes for multiple columns ['A', 'B', 'C', 'D']. I could write this out four times, but I've put it in a function that doesn't appear to work. Please see the working code below, I've commented out the part that doesn't work. I know I need to somehow bind the data and loop through, but how do I do this efficiently?
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 50
},
width = 960 - margin.left - margin.right,
height = 180 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.domain(["A", "B", "C", "D"])
.rangeBands([0, 200])
var y = d3.scale.linear()
.range([height, 0])
.domain([0, 100])
var xAxis = d3.svg.axis()
.scale(x)
var svg = d3.select("#timeline").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 + ")");
// define brush control element and its events
var brush = d3.svg.brush().y(y)
.on("brushend", () => console.log('A Extent: ', brush.extent()))
// create svg group with class brush and call brush on it
var brushg = svg.append("g")
.attr("class", "brush")
.call(brush);
// set brush extent to rect and define objects height
brushg.selectAll("rect")
.attr("x", x("A"))
.attr("width", 20);
/*
var brush = (d) => {
var brush = d3.svg.brush().y(y)
.on("brushend", () => console.log(d, ' Extent: ', brush.extent()))
var brushg = svg.append("g")
.attr("class", "brush")
.call(brush1);
brushg.selectAll("rect")
.attr("x", x("A"))
.attr("width", 20);
}
*/
.brush {
fill: lightgray;
fill-opacity: .75;
shape-rendering: crispEdges;
}
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.6/d3.min.js'></script>
<div class="container">
<div id="timeline"></div>
</div>
Treat the brushes as data, as they map to each ordinal value on the x axis. Create the brushes with .enter and append all the necessary functionality. The .each function is similar to call, but runs on each element separately. This is very useful to contain the generation of the brushes.
xData = ["A", "B", "C", "D"];
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 50
},
width = 960 - margin.left - margin.right,
height = 180 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.domain(xData)
.rangeBands([0, 200])
var y = d3.scale.linear()
.range([height, 0])
.domain([0, 100])
var xAxis = d3.svg.axis()
.scale(x)
var svg = d3.select("#timeline").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 + ")");
const brushes = svg.selectAll('g.brush')
.data(xData)
.enter()
.append('g')
.attr('class', 'brush')
.each(function(d) {
const el = d3.select(this);
const brush = d3.svg.brush().y(y).on("brushend", () => console.log(d, ' Extent: ', brush.extent()));
el.call(brush);
el.selectAll("rect")
.attr("x", x(d))
.attr("width", 20);
});
.brush {
fill: lightgray;
fill-opacity: .75;
shape-rendering: crispEdges;
}
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.6/d3.min.js'></script>
<div class="container">
<div id="timeline"></div>
</div>
Related
I have created a time series animation. Essentially, I generate a random datum every 20ms and the time axis slides continuously leftwards on a one second interval. D3.interval() creates what I want, but the graph seems "choppy". I assume it has something to do with the web browser or D3? Here is my code (expand the snippet window for a better view):
////////////////////////////////////////////////////////////
////////////////////// Set-up /////////////////////////////
////////////////////////////////////////////////////////////
const margin = { left: 80, right: 80, top: 30, bottom: 165},
TIME_MEASUREMENT = 20,
TIME_INTERVAL = 1000,
TIME_FRAME = 10000;
let width = $("#chart").width() - margin.left - margin.right,
height = $("#chart").height() - margin.top - margin.bottom;
let date1 = 0,
date2 = TIME_FRAME;
let accuracy = 20,
precision = 20,
aF = 2, //accuracyFactor
pF = 1; //precisionFactor
let random = d3.randomUniform(-(pF * precision), pF * precision),
count = TIME_FRAME/TIME_MEASUREMENT + 1;
let data = d3.range(count).map((d,i) => random() + accuracy );
const yDomain = [0, 50],
yTickValues = [0, 10, 20, 30, 40, 50];
////////////////////////////////////////////////////////////
///////////////////////// SVG //////////////////////////////
////////////////////////////////////////////////////////////
const svg = d3.select("#chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
const g = svg.append("g")
.attr("transform", "translate(" + margin.left + ", " + margin.top + ")");
////////////////////////////////////////////////////////////
///////////////////// Axes & Scales ////////////////////////
////////////////////////////////////////////////////////////
//X axis - dynamic
let xAxisGroup = g.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0," + (height) + ")");
let xScale = d3.scaleTime();
const xAxis = d3.axisBottom(xScale)
.ticks(d3.timeSecond.every(1))
.tickSizeInner(15)
.tickFormat(d3.timeFormat("%M:%S"));
//Y axis - static
let yAxisGroup = g.append("g")
.attr("class", "y-axis");
let yScale = d3.scaleLinear()
.domain(yDomain)
.range([height, 0]);
const yAxis = d3.axisLeft()
.scale(yScale)
.tickValues(yTickValues)
.tickFormat(d3.format(".0f"));
yAxisGroup.call(yAxis);
//Data points - static
let dataScale = d3.scaleTime()
.domain([date1,date2])
.range([0,width]);
////////////////////////////////////////////////////////////
/////////////////////// Functions //////////////////////////
////////////////////////////////////////////////////////////
function drawData(){
xScale.domain([date1, date2])
.range([0, width]);
xAxisGroup
.transition()
.duration(TIME_MEASUREMENT)
.ease(d3.easeLinear)
.call(xAxis);
g.selectAll("circle")
.data(data)
.join(
function(enter){
enter.append("circle")
.attr("cx", (d,i) => dataScale(i*TIME_MEASUREMENT))
.attr("cy", (d,i) => yScale(d))
.attr("fill", "black")
.attr("r","3");
},
function(update){
update
.attr("cx", (d,i) => dataScale(i*TIME_MEASUREMENT))
.attr("cy", (d,i) => yScale(d))
.transition()
.ease(d3.easeLinear)
.attr("transform", "translate(" + dataScale(-TIME_MEASUREMENT) + ", 0)");
}
);
data.push(random() + accuracy);
data.shift();
date1 += TIME_INTERVAL/50;
date2 += TIME_INTERVAL/50;
};
////////////////////////////////////////////////////////////
///////////////////////// Main /////////////////////////////
////////////////////////////////////////////////////////////
d3.interval(drawData, TIME_MEASUREMENT);
x-axis,
.y-axis {
font-size: 0.8em;
stroke-width: 0.06em;
}
#chart {
width: 600px;
height: 500px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="//d3js.org/d3.v6.min.js"></script>
<div id="chart"></div>
I don't get this "choppy effect" when I use d3.timer or setInterval, but each second slides as if it were ~850ms. Thank you for your input!
I want to add custom tick labels on the x axis,like 1,2,3,4,3,2,1 in this pattern. But the code that I am using doesn't show the decreasing numbers.
var margin = {
top: 100,
right: 100,
bottom: 100,
left: 100
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.domain([1, 2, 3, 4, 5, 4, 3, 2, 1])
.rangePoints([0, width]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("g")
.attr("class", "x axis")
.call(xAxis);
Any kind of help would be appreciated. Thanks.
Little hack it needed to solve this issue.
Cause
d3.scale.ordinal(), domain calculation in purely mathematical logic. Each
domain associated with a range. ( f(x)=y ).
Duplication not allowed because it will make ambiguity
Solution
Create linear scale with total item in the axis as domain [0, itemLength]
While creating axis use tickFormat to find out the index of the element
var margin = {
top: 100,
right: 100,
bottom: 100,
left: 100
},
width = 560 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var xdata = ["1", "2", "3", "4", "5", "4", "3", "2", "1", "0"];
var linear = d3.scale.linear()
.domain([0, 10])
.range([0, width]);
var xAxis = d3.svg.axis()
.scale(linear)
.orient("bottom");
var axis = d3.svg.axis()
.scale(linear)
.orient("top")
.ticks(10)
.tickFormat(function (d) {
return xdata[d];
});
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 + ")");
svg.append("g")
.attr("class", "x axis")
.call(axis);
.axis path, line {
fill: none;
stroke: #000;
stroke-linecap: round;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I have been following the block that uses hexagonal binning of random points with the normal distribution but instead trying to tailor it to the exponential distribution.
The code runs, but the output seems to show a mirror along the x-axis. That is, the points are all clustered along the upper-left instead of lower-left. I've been playing with the transform function but can't quite get it. What am I missing? JSFiddle
<!DOCTYPE html>
<style>
.hexagon {
stroke: #000;
stroke-width: 0.5px;
}
</style>
<svg width="500" height="200"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-hexbin.v0.2.min.js"></script>
<script>
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 30, left: 40},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var randomX = d3.randomExponential(1 / 30),
randomY = d3.randomExponential(1 / 30),
points = d3.range(2000).map(function() { return [randomX(), randomY()]; });
var color = d3.scaleSequential(d3.interpolateLab("white", "steelblue"))
.domain([0, 20]);
var hexbin = d3.hexbin()
.radius(5)
.extent([[0, 0], [width, height]]);
var x = d3.scaleLinear()
.domain([0, width])
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, height])
.range([height, 0]);
g.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
g.append("g")
.attr("class", "hexagon")
.attr("clip-path", "url(#clip)")
.selectAll("path")
.data(hexbin(points))
.enter().append("path")
.attr("d", hexbin.hexagon())
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.attr("fill", function(d) { return color(d.length); });
</script>
You set your scales, but you never use them:
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
Solution: use your scales:
.attr("transform", function(d) {
return "translate(" + x(d.x) + "," + y(d.y) + ")";
//scales here --------^--------------^
})
Here is your code with that change:
var svg = d3.select("svg"),
margin = {
top: 20,
right: 20,
bottom: 30,
left: 40
},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var randomX = d3.randomExponential(1 / 30),
randomY = d3.randomExponential(1 / 30),
points = d3.range(2000).map(function() {
return [randomX(), randomY()];
});
var color = d3.scaleSequential(d3.interpolateLab("white", "steelblue"))
.domain([0, 20]);
var hexbin = d3.hexbin()
.radius(5)
.extent([
[0, 0],
[width, height]
]);
var x = d3.scaleLinear()
.domain([0, width])
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, height])
.range([height, 0]);
g.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
g.append("g")
.attr("class", "hexagon")
.attr("clip-path", "url(#clip)")
.selectAll("path")
.data(hexbin(points))
.enter().append("path")
.attr("d", hexbin.hexagon())
.attr("transform", function(d) {
return "translate(" + x(d.x) + "," + y(d.y) + ")";
})
.attr("fill", function(d) {
return color(d.length);
});
.hexagon {
stroke: #000;
stroke-width: 0.5px;
}
<svg width="500" height="200"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-hexbin.v0.2.min.js"></script>
I'm using Polymer to render some d3 charts. When the Polymer is initially rendered I only draw a graph with axes and no data, since the data comes later once the API calls succeed. However, when I get around to adding the 'rect' elements in the svg, despite them showing up in the Chrome devtools element inspector, they don't show up in the chart itself.
dataChanged: function() {
var data = this.data;
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = this.width - margin.left - margin.right,
height = this.height - margin.top - margin.bottom;
// format the data
data.forEach(function(d) {
d.date = d3.isoParse(d.date);
});
// set the ranges
var x = d3.scaleTime()
.domain(d3.extent(data, function(d) { return d.date; }))
.rangeRound([0, width]);
var y = d3.scaleLinear()
.range([height, 0]);
var svg = d3.select(this.$.chart);
var histogram = d3.histogram()
.value(function(d) { return d.date; })
.domain(x.domain())
.thresholds(x.ticks(d3.timeMonth));
var bins = histogram(data);
y.domain([0, d3.max(bins, function(d) { return d.length; })]);
var t = d3.transition()
.duration(750)
.ease(d3.easeLinear);
svg.selectAll("rect")
.data(bins)
.enter().append("rect")
.attr("class", "bar")
.attr("x", 1)
.attr("transform", function(d) {
return "translate(" + x(d.x0) + "," + y(d.length) + ")";
})
.attr("width", function(d) { return x(d.x1) - x(d.x0) -1 ; })
.attr("height", function(d) { return height - y(d.length); });
svg.select(".xAxis")
.transition(t)
.call(d3.axisBottom(x));
svg.select(".yAxis")
.transition(t)
.call(d3.axisLeft(y));
},
ready: function() {
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = this.width - margin.left - margin.right,
height = this.height - margin.top - margin.bottom;
// set the ranges
var x = d3.scaleTime()
.domain([new Date(2010, 6, 3), new Date(2012, 0, 1)])
.rangeRound([0, width]);
var y = d3.scaleLinear()
.range([height, 0]);
// Add the SVG to my 'chart' div.
var svg = d3.select(this.$.chart).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 + ")");
// Add the X Axis
svg.append("g")
.attr("class","xAxis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add the Y Axis
svg.append("g")
.attr("class","yAxis")
.call(d3.axisLeft(y));
}
ready() gets called upon rendering, dataChanged() when the parent component passes a chunk of data down.
The axes get rendered correctly, with the right transitions and the right dimensions, but the rects don't. They show up in the chrome element inspector with a 0x17 size, even though this is what they look like: <rect class="bar" x="1" transform="translate(0,24.06417112299465)" width="101" height="275.93582887700535"></rect>
In your ready function, you are grabbing your div creating an svg element adding a g element and then appending your axis to that g.
In your dataChanged function, you are grabbing your div and appending rects to it.
See the disconnect? You can't parent svg to HTML.
In ready do this:
var svg = d3.select(this.$.chart).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("id", "canvas")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
In dataChanged:
var svg = d3.select("#canvas");
This will allow you to "find" the appropriate g to append your rects to.
I'm attempting to create a bar chart in D3 that replicates this design. The idea is that values can range from -100 to 100 and are displayed alongside each other. The scale must stay as 0-100, with colours being used to indicate whether the number is above or below 0.
I've managed to create a simple bar chart that displays positive numbers but as soon as a negative number is added, the chart breaks. The following code is used to create the x and y axis. Negative values are displayed if the x domain is changed to [-100, 100], but doing so renders the chart in a way that is too different from the original design.
var y = d3.scaleBand()
.range([height, 0])
.padding(0.1);
var x = d3.scaleLinear()
.range([0, width]);
x.domain([0, 100])
y.domain(data.map(function(d) { return d.sentiment; }));
Can anyone provide some tips/guidance on producing a graph that looks similar to the provided design, if it's even possible? Link to my current graph can be found in the JSFiddle below:
JSFiddle
Many thanks.
Just use Math.abs():
.attr("width", function(d){
return x(Math.abs(d.value));
})
Here is the demo:
var data = [{"sentiment":"Result","value":28},{"sentiment":"Result2","value":-56}]
var margin = {top: 20, right: 20, bottom: 50, left: 70},
width = 850 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
var y = d3.scaleBand()
.range([height, 0])
.padding(0.1);
var x = d3.scaleLinear()
.range([0, width]);
var svg = d3.select("#graph").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.value = +d.value;
});
x.domain([0, 100])
y.domain(data.map(function(d) { return d.sentiment; }));
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("width", function(d) {return x(Math.abs(d.value)); } )
.attr("y", function(d) { return y(d.sentiment) + 15; })
.attr("height", y.bandwidth())
.attr("fill", function(d) {
if (d.value <= 0) {
return "#FC4E5C";
} else {
return "#34A232";
}
});
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
svg.append("g")
.call(d3.axisLeft(y));
svg.append("text")
.attr("class", "x label")
.attr("text-anchor", "end")
.attr("x", width - 300)
.attr("y", height + 40)
.text("Sentiment (%)");
.bar {
margin-top: 50px;
height: 30px;
}
text {
fill: black;
font-size: 14px;
}
path {
stroke: black;
}
line {
stroke: black;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
<div id="graph">
</div>