Simple way to use existing object as a clipping path? - javascript

I have the following simple example, When the line extends outside the rectangle, I want to clip it. I already have the rectangle used as an outline, what is a simple way to the same rectangle as a clipping path? My current approach using id is ignored. This related question has an answer but it requires creating the clip area separately. I'd like to re-use my info rather than repeat nearly the same info.
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src = "http://d3js.org/d3.v3.min.js"> </script>
<script>
var margin = {top: 100, right: 20, bottom: 20, left: 20},
width = 600 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;
var xdata = d3.range(0, 20);
var ydata = [1, 4, 5, 9, 10, 14, 15, 15, 11, 10, 5, 5, 4, 8, 7, 5, 5, 5, 8, 10];
var xy = []; // start empty, add each element one at a time
for(var i = 0; i < xdata.length; i++ ) {
xy.push({x: xdata[i], y: ydata[i]});
}
var xscl = d3.scale.linear()
.domain(d3.extent(xy, function(d) {return d.x;})) //use just the x part
.range([margin.left, width + margin.left])
var yscl = d3.scale.linear()
.domain([1, 8]) // use just the y part
.range([height + margin.top, margin.top])
var slice = d3.svg.line()
.x(function(d) { return xscl(d.x);}) // apply the x scale to the x data
.y(function(d) { return yscl(d.y);}) // apply the y scale to the y data
var svg = d3.select("body")
.append("svg")
svg.append('rect') // outline for reference
.attr({x: margin.left, y: margin.top,
width: width,
height: height,
id: "xSliceBox",
stroke: 'black',
'stroke-width': 0.5,
fill:'white'});
svg.append("path")
.attr("class", "line")
.attr("d", slice(xy))
.attr("clip-path", "#xSliceBox")
.style("fill", "none")
.style("stroke", "red")
.style("stroke-width", 2);
</script>
</body>

You can't reference the rectangle directly in the clip-path property, you need to create a <clipPath> element. Then, inside the <clipPath> element, you can use a <use> element to reference the rectangle.
(Yes, it's round-about and more complicated that you would think it should be, but that's how the SVG specs defined it.)
Working from your code:
var svg = d3.select("body")
.append("svg")
var clip = svg.append("defs").append("clipPath")
.attr("id", "clipBox");
svg.append('rect') // outline for reference
.attr({x: margin.left, y: margin.top,
width: width,
height: height,
id: "xSliceBox",
stroke: 'black',
'stroke-width': 0.5,
fill:'white'});
clip.append("use").attr("xlink:href", "#xSliceBox");
svg.append("path")
.attr("class", "line")
.attr("d", slice(xy))
.attr("clip-path", "url(#clipBox)") //CORRECTION
.style("fill", "none")
.style("stroke", "red")
.style("stroke-width", 2);
You could also do this the other way around, defining the rectangle within the clipPath element and then using a <use> element to actually draw it to the screen. Either way, you want to only define the rectangle once, so that if you decide to change it you only have to do it in one place and the other will update to match.

Related

Mark element as clicked in d3.js v6+

My Problem: I have some data points that are bound to circle elements. Now, each time the user clicks on a circle I want to change its color to red. For this, I want to use an update function which is called each time the user clicks on a circle. Since I want to change not only the clicked circle but also other elements based on which circle was clicked, I need to somehow remember which one was clicked. I saw this being done in d3.js v3 by simply saving the datum to a variable (clickedCircle) in the event listener and recalling it later. However, this doesn't seem to work in d3.js v6+ anymore. What would be the best way to do this in v6+?
Quick d3.js v3 Example (Basic idea adapted from this chart):
var data = [
{x: 10, y: 20},
{x: 30, y: 10},
{x: 20, y: 55},
];
svg = d3.select(#div1)
.append("svg")
.attr("width", 100)
.attr("height", 100)
;
var circles;
var clickedCircle;
function update() {
circles = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return d.position.x } )
.attr("cy", function(d) { return d.position.y } )
.attr("r", 10)
.on("click", function(e, d) { clickedCircle = d; update(); } )
;
circles
.style("fill", function(d) {
if (d === clickedCircle) {
return "red"
} else {
return "black"
}
})
;
}
Here's an example with D3 v7. If you click a circle, that circle becomes red and its data is shown in a text label.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
// margin convention set up
const margin = { top: 10, bottom: 10, left: 10, right: 10 };
const width = 110 - margin.left - margin.right;
const height = 110 - margin.top - margin.bottom;
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})`);
// data
const data = [
{ x: 10, y: 20 },
{ x: 30, y: 10 },
{ x: 20, y: 55 },
];
// draw cirlces
const circles = g.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', 10)
.attr('fill', 'black')
.on('click', update);
// text label
const label = g.append('text')
.attr('y', height);
// update when circle is clicked
function update(event, d) {
// reset all circles to black
circles.attr('fill', 'black');
// color the clicked circle red
d3.select(this).attr('fill', 'red');
// update the label
label.text(`x: ${d.x}, y: ${d.y}`);
}
</script>
</body>
</html>

D3.js: Why the bar chart turns around if I add this two statements

Hey I had a barChart and I'm aware in svg the x=0 and y=0 is on the top-left corner so the graphics shows downwards. If I add a couple of statements it turns around.
var w = 500;
var h = 100;
var barPadding = 1;
var dataset = [5, 4, 6, 7, 8, 10, 2, 3, 4, 5, 6, 25, 34, 54, 64, 32, 11, 32, 42, 5, 10, 15, 20, 25, 30, 35, 40];
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var rectangles = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d, i) {
return i * (w / dataset.length);
})
.attr("y", function(d) {
return h - d;
})
.attr("width", w / dataset.length - barPadding)
.attr("height", function(d) {
return d * 4;
})
.attr("fill", function(d) {
return "rgb(0, 0, " + (d * 10) + ")";
});
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
I have two questions,
a)why on the examples they use h-d for the y positioning?
If the svg height is 100 for example and a number on the dataset is 60, shouldn't that position the bar on the y=40 from the top left corner? Yet that is not the case when I render this, the bar shows on the same y position for all the bars
Why is it rendering the bars correctly with those given values? What makes the bar "grow from the bottom" in this case, cause if I do the math it makes me expect a different thing.
In this particular case, the reason why the y values are cut off at the correct place is because the svg has a height of 100. But if you carefully inspect the svg, every rectangle extends beyond the bottom of the svg, they are just not visible since there is no overflow.
h-d is correct for the starting y position, if a value was 60, then (100-60) would equal 40 as you have correctly stated. However, the actual height should be just d rather than d * 4. I do not know where the d * 4 came from.
So if your value is 60, then y would be 40, height would be 60 which would extend to the bottom of the svg.
Hope this helps.

Scaling D3 bar chart to container size

I'm wanting to scale this bar chart to the size of it's container. Ideally do something like w = '100%', h = '100%'. Any way I may do this?
http://jsfiddle.net/mo363jm7/
<div class="test" style="height:50px;width:100px;overflow:hidden"></div>
//Width and height
var w = 200,
h = 100,
barPadding = 1,
dataset = [ 5, 10, 13, 19, 21, 25, 22, 18, 15, 13,
11, 12, 15, 20, 18, 17, 16, 18, 23, 25 ];
//Create SVG element
var svg = d3.select(".test")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d, i) {
return i * (w / dataset.length);
})
.attr("y", function(d) {
return h - (d * 4);
})
.attr("width", w / dataset.length - barPadding)
.attr("height", function(d) {
return d * 4;
})
.attr("fill", function(d) {
return "rgb(0, 0, " + (d * 10) + ")";
});
You can ask for the width of your container (or any given element) with the style function, like this:
var width = parseInt(d3.select(".test").style("width"),10);
var height = parseInt(d3.select(".test").style("height"),10);
The last parameter (10) indicates that you want to use the decimal system (if I am not mistaken).
So you can set the width of your svg element to that width. I would even suggest you make a function out of it and call that function on a resize event, so your svg looks like a fluid/responsive element.
you can do that as following:
d3.select(window).on('resize', function(){/* your svg and chart code */});
EDIT: On rereading your question, it appears to me that I might have misunderstood your question. D3 has scaling functions to scale your data so it fits into your svg container. If your div element is not responsive, then you should just set your svg width and height the same as your div element, and use scales on your data so your chart fits in the svg container. More info on scales you can find here: quantitative scales
try to change the height and width of your div to its column height and width like this http://jsfiddle.net/elviz/mo363jm7/2/
<div class="test" style="height:100px;width:200px;overflow:hidden"></div>

Creating a Text Labeled x-Axis with an Ordinal Scale in D3.js

I'm building a bar chart in d3.js with an ordinal x-axis whose ticks should label the chart with text. Could anyone explain how the ordinal scale "maps" x ticks to the corresponding bar positions? Specifically, if I want to designate the x tick labels with an array of text values to the corresponding bars in a bar chart.
Currently I'm setting the domain as the following:
var labels = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t"];
var xScale = d3.scale.ordinal()
.domain(labels)
However, values of 1-19 are showing after the text labels.
As seen in this fiddle:
http://jsfiddle.net/chartguy/FbqjD/
Associated Fiddle Source Code:
//Width and height
var margin = {top: 20, right: 20, bottom: 30, left: 40};
var width = 600 - margin.left - margin.right;
var height= 500-margin.top -margin.bottom;
var w = width;
var h = height;
var dataset = [ 5, 10, 13, 19, 21, 25, 22, 18, 15, 13,
11, 12, 15, 20, 18, 17, 16, 18, 23, 25 ];
var labels = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t"];
var xScale = d3.scale.ordinal()
.domain(labels)
.rangeRoundBands([margin.left, width], 0.05);
var xAxis = d3.svg.axis().scale(xScale).orient("bottom");
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset)])
.range([h,0]);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Create bars
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return yScale(d);
})
.attr("width", xScale.rangeBand())
.attr("height", function(d) {
return h - yScale(d);
})
.attr("fill", function(d) {
return "rgb(0, 0, 0)";
});
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + 0 + ")")
.call(xAxis);
//Create labels
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d;
})
.attr("x", function(d, i) {
return xScale(i) + xScale.rangeBand() / 2;
})
.attr("y", function(d) {
return h - yScale(d) + 14;
});
You can set the tick values of an ordinal axis explicitly using d3.svg.axis().tickValues(*array*).
But this is an odd way to do it because it dangerously separates your keys and values, meaning you have to take care to manually align the scales and make sure that your data corresponds correctly. It helps to group the keys and values in a single object and then use the format:
axis.domain(array.map(function (d) { return d.value; }))
to map your axis domains.
I have reworked your data and fiddle to do it in what I see as the more d3 way. (Also note that I made some other changes just for fun, namely improved the margins and cleaned up the axis alignment, etc.)

D3.js binding an object to data and appending for each key

I'm a D3.js newbie and I'm learning how to play around with data.
Let's say I have an object with names as keys, and each key has an array of numbers like this:
var userdata = {
'John' : [0, 1, 3, 9, 8, 7],
'Harry': [0, 10, 7, 1, 1, 11],
'Steve': [3, 1, 4, 4, 4, 17],
'Adam' : [4, 77, 2, 13, 11, 13]
};
For each user, I would like to append an SVG object and then plot the line with the array of values for that user.
So here is my assumption of how that would look based on tutorials, but I know it is incorrect. This is to show my limited knowledge and give better understanding of what I'm doing:
First I should create the line
var line = d3.svg.line().interpolate('basis');
Then I want to bind the data to my body and append an svg element for each key:
d3.select('body')
.selectAll('svg')
.data(userdata)
.enter()
.append('svg')
.append(line)
.x(function(d, i) { return i; })
.y(function(d) { return d[i]; });
So am I close??
Here's another example similar to mbeasley's: http://jsfiddle.net/2N2rt/15/ which adds axis, colors, and flips the chart right side up. First, I massaged your data just a little bit. Works the best if you have an array of values and I used keys to make getting to the name and values properties easier.
var data = [
{name: 'John', values: [0,1,3,9, 8, 7]},
{name: 'Harry', values: [0, 10, 7, 1, 1, 11]},
{name: 'Steve', values: [3, 1, 4, 4, 4, 17]},
{name: 'Adam', values: [4, 77, 2, 13, 11, 13]}
];
Generally in d3 you set up the chart like this which determines the size of the actual graph and the surrounding margins.
var margin = {top: 20, right: 80, bottom: 30, left: 50},
width = 640 - margin.left - margin.right,
height = 380 - margin.top - margin.bottom;
Then you can create your scales based on your data. Though you don't have to create them, they make positioning elements on the chart much easier by converting values to points. The domain is the min and max values that your data has, while the range is the min and max values of the size of your graph. Note that the range for the y axis gets reversed which puts (0,0) in the bottom left hand corner (usually 0 for the y axis is at the top).
var x = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d.values.length - 1; })])
.range([0, width]);
var y = d3.scale.linear()
.domain([d3.min(data, function(d) { return d3.min(d.values); }),
d3.max(data, function(d) { return d3.max(d.values); })])
.range([height, 0]);
d3 has a couple of features for automatically creating color scales for you. Just set it up with the keys that you want to use and the number of colors (I think there are 10 and 20 color options).
var color = d3.scale.category10()
.domain(d3.keys(data[0]).filter(function(key) { return key === "name"; }));
Here we use our scales to setup the x and y axis. There are lots of different options for the axes. TickFormat is handy to change how the ticks look, d3.format has lots of different options so that you rarely have to create a custom formatter.
var xAxis = d3.svg.axis()
.scale(x)
.tickFormat(d3.format('d'))
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
Here we create a reusable function that knows how to draw a line. We'll pass each datapoint for each person into the line function later on. Note that d holds the current set of values that we are graphing and i is the index of the current values within our original data array.
var line = d3.svg.line()
.interpolate("basis")
.x(function(d, i) { return x(i); })
.y(function(d, i) { return y(d); });
Finally we can start adding things to our chart, first creating and positioning the main svg element.
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 + ")");
Then appending each axis.
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
We append a group (g) for each person and associate the data with the group. We'll use this data when we draw the lines. We provide a class so that we can style the chart later if we want to.
var people = svg.selectAll(".people")
.data(data)
.enter().append("g")
.attr("class", "people");
Then finally draw the lines for each person.
people.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", function(d) { return color(d.name); });
I used your data to answer another question about drawing a grid when you have negative values. You can see that graph at http://jsfiddle.net/2y3er/2/.
I'd say you're going in the right direction. Here's what I would do (but know that there isn't just one way to do this):
// your user data
var userdata = {
'John' : [0, 1, 3, 9, 8, 7],
'Harry': [0, 10, 7, 1, 1, 11],
'Steve': [3, 1, 4, 4, 4, 17],
'Adam' : [4, 77, 2, 13, 11, 13]
};
// add your main SVG block
var svg = d3.select('body')
.append('svg')
.attr('width', 400)
.attr('height', 200)
.attr('id', 'chart');
// add an SVG group element for each user
var series = svg.selectAll('g.series')
.data(d3.keys(userdata))
.enter()
.append('g')
.attr('class', 'series');
// create your line generator
var line = d3.svg.line()
.interpolate('basis')
.x(function(d, i) {
return i*40; // *40 just to increase the chart size
})
.y(function(d) {
return d*3; // *3 for the same reason
});
// add a line for each user using your SVG grouping
var lineSVG = series.selectAll('.line')
.data(d3.keys(userdata))
.enter()
.append('svg:path')
.attr('d', function(d) {
return line(userdata[d]);
})
.attr('class','line');
Here's a demo of the above code.
I also highly recommend that you read this article, as it really helps solidify the way that d3 works.

Categories