I have a challenging idea to build and couldn't think about a solution yet. The design request to have interactive/draggable graphics, as the one I send by the link below.
However, those graphics elements will be distributed in specific places on the page, with other elements around (Text, images, etc). The idea is to let the user "to play" with the graphics circles, just doing something 'cool and fun'. The user must be able to drag the circles from the graphics and change its visual all along the page.
The problem is: If I place this element in an specific place (inside a div, for example), if we drag the circles outside the 'canvas' area, the elements is no longer visible.
How could I place this canvas-div element in specific place and at the same time to allow the elements inside it to go the outside limited zone?
I thought about putting it in position relative or absolute with 100% height and width of the page, but it will be out of its place in responsive I guess, or pretty complicate to place always at a good place by just using % position. Any suggestion?
I'm using d3.js
Thanks!!
Heres the link: https://codepen.io/A8-XPs/pen/ePWRxZ?editors=0010
HTML
<svg width="500" height="350"></svg>
JS
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 30, left: 50},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
let points = d3.range(1, 10).map(function(i) {
return [i * width / 10, 50 + Math.random() * (height - 100)];
});
var x = d3.scaleLinear()
.rangeRound([0, width]);
var y = d3.scaleLinear()
.rangeRound([height, 0]);
var xAxis = d3.axisBottom(x),
yAxis = d3.axisLeft(y);
var line = d3.line()
.x(function(d) { return x(d[0]); })
.y(function(d) { return y(d[1]); })
.curve(d3.curveCatmullRom.alpha(0.5))
let drag = d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended);
svg.append('rect')
.attr('class', 'zoom')
.attr('cursor', 'move')
.attr('fill', 'none')
.attr('pointer-events', 'all')
.attr('width', width)
.attr('height', height)
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
var focus = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
x.domain(d3.extent(points, function(d) { return d[0]; }));
y.domain(d3.extent(points, function(d) { return d[1]; }));
focus.append("path")
.datum(points)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 1.5)
.attr("d", line);
focus.selectAll('circle')
.data(points)
.enter()
.append('circle')
.attr('r', 5.0)
.attr('cx', function(d) { return x(d[0]); })
.attr('cy', function(d) { return y(d[1]); })
.style('cursor', 'pointer')
.style('fill', 'steelblue');
focus.selectAll('circle')
.call(drag);
focus.append('g')
.attr('class', 'axis axis--x')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
focus.append('g')
.attr('class', 'axis axis--y')
.call(yAxis);
function dragstarted(d) {
d3.select(this).raise().classed('active', true);
}
function dragged(d) {
d[0] = x.invert(d3.event.x);
d[1] = y.invert(d3.event.y);
d3.select(this)
.attr('cx', x(d[0]))
.attr('cy', y(d[1]))
focus.select('path').attr('d', line);
}
function dragended(d) {
d3.select(this).classed('active', false);
}
PS: I got to solve the problem by just applying simple CSS to the SVG:
Overflow: visible;
Hopefully it will work in a real page scenario as well.
Related
This question already has answers here:
Adding text to d3 circle
(2 answers)
Closed 3 years ago.
I am writing a text element (x axis measure value) for each circle but even after showing text element in inspect in browser its not showing
I have appended the text under circle given same x and y for the circle but its coming through
!DOCTYPE html>
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
<script>
// set the dimensions and margins of the graph
var margin = {top: 10, right: 30, bottom: 40, left: 100},
width = 460 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.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 + ")");
// Parse the Data
d3.csv("https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/7_OneCatOneNum_header.csv", function(data) {
// sort data
data.sort(function(b, a) {
return a.Value - b.Value;
});
// Add X axis
var x = d3.scaleLinear()
.domain([0, 13000])
.range([ 0, width]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x))
.selectAll("text")
.attr("transform", "translate(-10,0)rotate(-45)")
.style("text-anchor", "end");
// Y axis
var y = d3.scaleBand()
.range([ 0, height ])
.domain(data.map(function(d) { return d.Country; }))
.padding(1);
svg.append("g")
.call(d3.axisLeft(y))
// Lines
svg.selectAll("myline")
.data(data)
.enter()
.append("line")
.attr("x1", x(0))
.attr("x2", x(0))
.attr("y1", function(d) { return y(d.Country); })
.attr("y2", function(d) { return y(d.Country); })
.attr("stroke", "grey")
// Circles -> start at X=0
svg.selectAll("mycircle")
.data(data)
.enter()
.append("circle")
.attr("cx", x(0) )
.attr("cy", function(d) { return y(d.Country); })
.attr("r", "7")
.style("fill", "#69b3a2")
.attr("stroke", "black")
// Change the X coordinates of line and circle
svg.selectAll("circle")
.transition()
.duration(2000)
.attr("cx", function(d) { return x(d.Value); })
svg.selectAll("line")
.transition()
.duration(2000)
.attr("x1", function(d) { return x(d.Value); })
// this is the line i have added at my end and it showing as well while i do the inspect element.
svg.selectAll("circle")
.append(Text)
.attr("x", function (d) { return x(d.Value); })
.attr("y", function (d) { return y(d.Country); })
.text(function (d) { return d.Value})
.attr("font-family", "sans-serif")
.attr("font-size", "6px")
.attr("fill", "black")
.style("text-anchor", "middle")
})
</script>
Would like to show measure value under circle so user dont have to guess the x axis. circle is at 13000 so it should show as 13 in circle divided by 1000
From what I can see there's a couple of things going on.
Firstly, instead of:
...
.append(Text)
which is trying to pass in a variable called Text to the append function, it should be:
...
.append('text')
i.e. append an svg text element.
However, this is still appending text elements to circle elements. If you look at the elements via Chrome Devtools, you can see that there will be a text element inside each circle element, which doesn't actually display anything.
Instead, the label text needs to be rendered separately from the circles using something like.
svg.selectAll("mytext")
.data(data)
.enter()
.append('text')
.attr("x", function (d) { return x(d.Value) + 10; })
.attr("y", function (d) { return y(d.Country) + 4; })
.text(function (d) { return d.Value})
.attr("font-family", "sans-serif")
.attr("font-size", "10px")
.attr("fill", "black")
.style("text-anchor", "start")
.style('opacity', 0)
.transition()
.delay(1500)
.duration(500)
.style('opacity', 1);
I've made the font a bit bigger, and adjusted the x and y values and used text-anchor: start so that now the text appears just off the right of the circles. I've also put in a transition based on opacity with a delay so that the text only appears at the end of the circles' animation.
I am trying to make the angles on this chart smoother, D3.js is being used and I already tried to apply a few ideas as solution, like adding .interpolate("basis") on the code, but for some reason the chart disappear when I do it.
Do you have any clue on what am I doing wrong? The dots are draggable and this is the intended behavior.
Here's a sample to the code: https://codepen.io/A8-XPs/pen/ePWRxZ?editors=1010
HTML:
<svg width="500" height="350"></svg>
JS:
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 30, left: 50},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
let points = d3.range(1, 10).map(function(i) {
return [i * width / 10, 50 + Math.random() * (height - 100)];
});
var x = d3.scaleLinear()
.rangeRound([0, width]);
var y = d3.scaleLinear()
.rangeRound([height, 0]);
var xAxis = d3.axisBottom(x),
yAxis = d3.axisLeft(y);
var line = d3.line()
.x(function(d) { return x(d[0]); })
.y(function(d) { return y(d[1]); });
let drag = d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended);
svg.append('rect')
.attr('class', 'zoom')
.attr('cursor', 'move')
.attr('fill', 'none')
.attr('pointer-events', 'all')
.attr('width', width)
.attr('height', height)
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
var focus = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
x.domain(d3.extent(points, function(d) { return d[0]; }));
y.domain(d3.extent(points, function(d) { return d[1]; }));
focus.append("path")
.datum(points)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 1.5)
.attr("d", line);
focus.selectAll('circle')
.data(points)
.enter()
.append('circle')
.attr('r', 5.0)
.attr('cx', function(d) { return x(d[0]); })
.attr('cy', function(d) { return y(d[1]); })
.style('cursor', 'pointer')
.style('fill', 'steelblue');
focus.selectAll('circle')
.call(drag);
focus.append('g')
.attr('class', 'axis axis--x')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
focus.append('g')
.attr('class', 'axis axis--y')
.call(yAxis);
function dragstarted(d) {
d3.select(this).raise().classed('active', true);
}
function dragged(d) {
d[0] = x.invert(d3.event.x);
d[1] = y.invert(d3.event.y);
d3.select(this)
.attr('cx', x(d[0]))
.attr('cy', y(d[1]))
focus.select('path').attr('d', line);
}
function dragended(d) {
d3.select(this).classed('active', false);
}
Thank you!
To get basic interpolation use
var line = d3.line()
.x(function(d) { return x(d[0]); })
.y(function(d) { return y(d[1]); })
.curve(d3.curveBasis);
or
.curve(d3.curveCatmullRom.alpha(0.5))
I create a line chart like that.
However, I added the possibility to update data. The "darker line" is updated correctly, the area no.
Here is a piece of code:
var margin = {top: 10, right: 10, bottom: 35, left: 30};
var width = 500 - margin.left - margin.right;
var height = 220 - margin.top - margin.bottom;
var x = d3.scalePoint().range([width, 0]);
var y = d3.scaleLinear().range([height, 0]);
x.domain([...new Set(dataFilter.map(function(d) {
return d.year;
}))]);
y.domain([minX, 100]);
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
var valueline = d3.line()
.x(function(d) {
return x(d.year);
})
.y(function(d) {
return y(d.euro);
})
.defined(function(d) {
return (d.euro !== 0 && !isNaN(d.euro));
});
var area = d3.area()
.x(function(d) {
return x(d.year);
})
.y0(function(d) {
return y(d.euro);
})
.y1(height)
.defined(function(d) {
return (d.euro !== 0 && !isNaN(d.euro));
});
var svg = d3.select("#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 + ")");
svg.append("path")
.attr("class", "linePath")
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 1)
.attr("d", valueline(dataFilter));
svg.append('path')
.datum(dataFilter)
.attr('d', area)
.attr('fill', 'steelblue')
.attr('stroke', 'none')
.attr('opacity', '0.1')
.attr('class', 'areaLines');
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0, " + height + ")")
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
function updateData() {
dataFilter = ...; // other data
// Scale the range of the data again minX is now different
x.domain([...new Set(dataFilter.map(function(d) {
return d.year;
}))]);
y.domain([minX, 100]);
var svg = d3.select("#chart").transition();
svg.select(".linePath")
.duration(1000)
.attr("d", valueline(dataFilter));
svg.select(".x.axis")
.duration(1000)
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
// update area (doesn't work): TypeError: svg.select(...).datum is not a function
svg.select('.areaLines')
.datum(dataFilter)
.attr('d', area)
.duration(1000);
svg.select(".y.axis")
.duration(1000)
.call(yAxis);
}
When I run it I get: TypeError: svg.select(...).datum is not a function.
Why?
I found this question but I'cant able to solve the problem. Thanks a lot!!
You cannot transition the datum, it makes no sense. That's why datum() is not a transformation method.
Instead of that, transition the d attribute:
svg.select('.areaLines')
.attr('d', area(dataFilter))
.duration(1000);
I want to create a bar chart like this:
There are two chart bars one below the other, the first one grows upwards while the second one grows downwards.
They have different scales and data.
This is what I created:
var doublebarSvg1 = d3.select('#doublebar')
.append('svg')
.attr('class', 'doublebarSvg1')
.attr('width', 700)
.attr('height', 400);
var doublebarSvg2 = d3.select('#doublebar')
.append('svg')
.attr('class', 'doublebarSvg2')
.attr('width', 700)
.attr('height', 400);
var margin = {top: 0, right: 0, bottom: 0, left: 50};
var width = doublebarSvg1.attr('width') - margin.left - margin.right;
var height = doublebarSvg1.attr('height') - margin.top - margin.bottom;
var x = d3.scaleBand()
.rangeRound([0, width])
.padding(0.1)
.domain(years);
var y1 = d3.scaleLinear()
.rangeRound([height, 0])
.domain([0, 100]);
var y2 = d3.scaleSqrt()
.rangeRound([height, 0])
.domain([813, 0.1]); // max value 812.05 but domain is [0, 100000]
var doublebarSvgG1 = doublebarSvg1.append('g').attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
var doublebarSvgG2 = doublebarSvg2.append('g').attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
////////////////////////////////////////////////////////////////////////
// Tooltip.
////////////////////////////////////////////////////////////////////////
var svgTip = doublebarSvg1.append('svg').attr('id', 'tooltip');
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-5, 0])
.html(function(d) {
return '<div><span>Country:</span> <span style=\'color:white\'>' + d.country + '</span></div>' +
'<div><span>Perc:</span> <span style=\'color:white\'>' + d.perc + '%</span></div>' +
'<div><span>Rate:</span> <span style=\'color:white\'>' + d.rate + '%</span></div>';
});
svgTip.call(tip);
////////////////////////////////////////////////////////////////////////
// Draw a single double bar
////////////////////////////////////////////////////////////////////////
makeDoublebar1();
function makeDoublebar1() {
// define the axes
var xAxis = d3.axisBottom(x);
var yAxis1 = d3.axisLeft(y1);
// create x axis
doublebarSvgG1.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
// create y axis
doublebarSvgG1.append('g')
.attr('class', 'y axis')
.call(yAxis1)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end');
// create bar rect
doublebarSvgG1.selectAll('.bar')
.data(testData1) //.data(covFiltered)
.enter().append('rect')
.attr('fill', 'steelblue')
.attr('class', 'bar')
.attr('x', function(d) {
return x(d.year);
})
.attr('y', function(d) {
if(isNaN(d.perc)) {
d.perc = 0;
}
return y1(d.perc);
})
.attr('width', x.bandwidth())
.attr('height', function(d) {
if(isNaN(d.perc)) {
d.perc = 0;
}
return height - y1(d.perc);
})
.on('mouseover', function(d) {
d3.select(this).attr('fill', 'darkblue');
tip.show(d);
})
.on('mouseout', function(d) {
d3.select(this).attr('fill', 'steelblue');
tip.hide(d);
});
}
////////////////////////////////////////////////////////////////////////
// Draw a single double bar
////////////////////////////////////////////////////////////////////////
makeDoublebar2();
function makeDoublebar2() {
// define the axes
var xAxis = d3.axisBottom(x);
var yAxis2 = d3.axisLeft(y2);
// create x axis
doublebarSvgG2.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, 0)')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
// create y axis
doublebarSvgG2.append('g')
.attr('class', 'y axis')
.call(yAxis2)
.append('text')
.style('text-anchor', 'end');
// create bar rect
doublebarSvgG2.selectAll('.bar')
.data(testData2)
.enter().append('rect')
.attr('fill', 'tomato')
.attr('class', 'bar')
.attr('x', function(d) { // left start point
return x(d.year);
})
.attr('y', function(d) { // top start point
if(isNaN(d.rate)) {
d.rate = 0;
}
return 0;
})
.attr('width', x.bandwidth())
.attr('height', function(d) {
if(isNaN(d.rate)) {
d.perc = 0;
}
return y2(d.rate);
})
.on('mouseover', function(d) {
d3.select(this).attr('fill', 'red');
tip.show(d);
})
.on('mouseout', function(d) {
d3.select(this).attr('fill', 'tomato');
tip.hide(d);
});
}
PLUNKER here.
There are some problem:
if I replace .axis {display: initial;} with .axis {display: none;}, all the axis disappear but I want the horizontal line between the two chart
I would like there to be only one tooltip, which when the user hovers over any bar, comes out with a tooltip that shows both perc and rate value.
And, more importantly, is this the smartest way to create a chart like that?
Regarding the axis, since you want to keep the horizontal line, just hide the ticks and the texts:
.x.axis text,.x.axis line {
opacity: 0;
}
The tooltip problem is a bit more complex. The issue is that you're binding different data arrays to each set of bars.
Because of that, the best idea is finding the desired object in each array when you hover over a given year and getting the respective properties:
var thisPerc = testData1.find(function(e){return e.year === d.year}).perc;
var thisRate = testData2.find(function(e){return e.year === d.year}).rate;
Then you use those properties for setting the tooltip's text.
Here is the updated Plunker: http://plnkr.co/edit/tfB4TpkETgzp5GF1677p?p=preview
Finally, for your last question ("And, more importantly, is this the smartest way to create a chart like that?"), the answer is no. There are a lot of things that can (and must) be changed here, but this involves a lot of refactoring and it's arguably off topic at Stack Overflow. However, this is an adequate question for Code Review. But please read their docs first for keeping your question on topic, asking there is not the same as asking here.
I have 5 years of data for a grouped bar chart that includes two variables per year. When I mouseover each bar, I want it to show the specific value for that bar. But I'm not sure how to style the tooltip at the very bottom of my code to show the actually amount in my CSV when I mouse over the bar.
I want the specific dollar amount in my CSV to show up where I have "amount here" written below. I'm able to get the dollar sign in that text to show up, just not pull any data from my CSV.
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 600 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x0 = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
var x1 = d3.scale.ordinal();
var y = d3.scale.linear()
.range([height, 0]);
var color = d3.scale.ordinal()
.range(["#98abc5", "#7b6888"]);
var xAxis = d3.svg.axis()
.scale(x0)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(d3.format(".2s"));
var svg = d3.select(".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 + ")");
$(document).ready(function() {
});
d3.csv("data/data.csv", function(error, data) {
var restAmt = d3.keys(data[0]).filter(function(key) { return key !== "year"; });
data.forEach(function(d) {
d.totalrest = restAmt.map(function(name) { return {name: name, value: +d[name]}; });
});
x0.domain(data.map(function(d) { return d.year; }));
x1.domain(restAmt).rangeRoundBands([0, x0.rangeBand()]);
y.domain([0, d3.max(data, function(d) { return d3.max(d.totalrest, function(d) { return d.value; }); })]);
var year = svg.selectAll(".year")
.data(data)
.enter().append("g")
.attr("class", "g")
.attr("transform", function(d) { return "translate(" + x0(d.year) + ",0)"; });
//draw X axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
//draw Y axis
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("Millions of Dollars");
year.selectAll("rest")
.data(function(d) { return d.totalrest; })
.enter().append("rect")
.attr("width", x1.rangeBand())
.attr("x", function(d) { return x1(d.name); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.style("fill", function(d) { return color(d.name); })
.on("mouseover", function(){return tooltip.style("visibility", "visible"); })
.on("mousemove", function(){return tooltip.style("top", (event.pageY-120)+"px").style("left",(event.pageX-120)+"px"); })
.on("mouseout", function(){return tooltip.style("visibility", "hidden");} )
var tooltip = d3.select(".chart")
.append("g")
.style("position", "absolute")
.style("z-index", "0")
.style("visibility", "hidden")
.text(function(){
return '$'+"amount here"
})
For one thing, see if you can create a minimal complete verifiable example. I couldn't manage to get you code running so I'm guessing here.
Just by looking though, when you set the text on the tooltip, see if data is bound such that you can just pass it in.
.text(function(d){
return '$'+d // or d.whatever
})
If that fails, you should be able to pull the same trick with .on just above, and pass that data to a function that creates the tooltip.
That being said, I get the sense that you may be hiding and showing every tooltip. If that's the case, and the data are bound to the tooltips, you can create a tooltip function that takes in indx and then call .style("visibility", function(d,i){return i === indx ? null : "hidden") which will unhide only the tooltip of indx. You can run the function with -1 at the start of execution to hide all tooltips. Alternatively, just create the tooltip when you need it rather than toggle its visibility.
here just guess your amount field in csv is called amount, use d.amount as an example below:
You can add parameter d in 'mouseover' event, and bind the amout value into tooltip element as html element:
var div = d3.select('body').append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
year.selectAll("rest")
...
.on('mouseover', function(d) {
div.transition()
.duration(200)
.style('opacity', .9);
div.html('<h3>' + d.amount + '</h3>' + '<p>' + d.amount + '</p>')
.style('left', (d3.event.pageX) + 'px')
.style('top', (d3.event.pageY - 28) + 'px');
})