mardi 17 février 2015

Implement an SVG mask on a RECT with D3 (in Javascript)?


I'm trying to implement an SVG mask in D3, similar to this very simple jsfiddle example, but I must have lost something in translation. My implementation all takes place in a class that renders a graph. I'm trying to apply the mask to define bounds for the graph, so that when the data exceeds those bounds, the graph is neatly clipped. When I apply the mask, the bars of the graph completely disappear. As far as I can tell the mask in the right place. HELP!


Here is where I define the mask in my init() function:



// Add an SVG element with the desired dimensions and margin.
this.graph = d3.select(this.config.id).append("svg:svg")
.attr("width", this.width + this.m[1] + this.m[3])
.attr("height", this.height + this.m[0] + this.m[2])
.append("svg:g")
.attr("transform", "translate(" + this.m[3] + "," + this.m[0] + ")");

var maskWidth = 640;
var maskHeight = 321;

this.graph.append('svg:defs') <------ I START DEFINING IT HERE !
.call(function (defs) {
// Appending the mask
defs.append('svg:mask')
.attr('id', 'mask')
.attr('width', maskWidth)
.attr('height', maskHeight)
.attr('x', 0)
.attr('y', 0)
.call(function(mask) {
mask.append('svg:rect')
.attr('width', maskWidth)
.attr('height', maskHeight)
.attr('fill', '#ffffff')
});
});


Here is the Method that draws bars on the graph where I attempt to apply the mask (see the last line):



addBars: function (data){
var numberOfBars = Math.floor(this.xMaximum);
var barWidth = this.width/numberOfBars;

// Generate a histogram using twenty uniformly-spaced bins.
var histogramData = d3.layout.histogram()
.bins(this.xScale.ticks(numberOfBars))
(data);

//console.trace('typeof: '+typeof this.xScale);
var xScale = this.xScale;
var yScale = this.yScale;
var height = this.height;

this.bars = this.graph.selectAll("bar")
.data(histogramData, function(d){ return d;})
.enter()
.append("rect")
.attr("class","bar")
.attr("fill","steelblue")
.attr("transform", function(d, i) {
var yOffset = height;
return "translate(" + (i * barWidth - barWidth/2) + ","+yOffset+")";
})
.attr("y", function(d,i) {
var yPosition = yScale(d.length)- height;
return (yScale(d.length)-height);
})
.attr("height", function(d) {
return height - yScale(d.length);
})
.attr("width", barWidth - 1)
.attr('mask', 'url(#mask)'); <---- OVER HERE !!!!
},


Here is a link to the resulting HTML in Chrome Developer Tools (I've highlighted the <defs> and one of the graph bars that should be masked):Chrome Developer Tools Dynamic HTML


As far as I can tell everything looks good. This leads me to believe that the mask is mis-aligned with the bar, causing the bar to be invisible. However, in the developer tools, when I hover over the <rect> element, it shows it as overlaying the graph bars, so it doesn't seem like an alignment issue. Any help would be appreciated.


Lastly, I've made a jsfiddle of the class being used in my application (see the comments for the link.). Below is also the entire class for drawing the graph, just in case it would be helpful to see the code in context:



// HistogramGrapher class - constructor
var HistogramGrapher = function() {

// assign default properties
this.config = {
id: "",
xAxisLabel: "xAxis",
yAxisLabel: "yAxis",
width: 1000,
height: 400,
title: "Title",
mean: 20
};

// define variables
this.m = [40, 80, 40, 80]; // margins
this.width; // width
this.height; // height
this.xAxisLabel;
this.yAxisLabel;
this.graph;
this.bars;
this.lines;
this.xScale;
this.xScaleInvert;
this.xAxis;
this.yScale;
this.yScaleInvert;
this.yAxis;
this.yMaximum = 25;
this.xMaximum = 2 * this.config.mean;
}


// methods for this class
HistogramGrapher.prototype = {

init: function (options) {
// copy properties of `options` to `config`. Will overwrite existing ones.
for(var prop in options) {
if(options.hasOwnProperty(prop)){
this.config[prop] = options[prop];
}
}

// update variables
this.updateWidth(this.config.width);
this.updateHeight(this.config.height);
this.updateXMaximum(this.config.mean);

// X scale will fit all values from datay[] within pixels 0-w
this.xScale = d3.scale.linear()
.domain([0, this.xMaximum])
.range([0, this.width]);

this.xScaleInvert = d3.scale.linear()
.range([0, this.xMaximum])
.domain([0, this.width]);

// Y scale
this.yScale = d3.scale.linear()
.domain([0, this.yMaximum])
.range([this.height,0]);

this.yScaleInvert = d3.scale.linear()
.range([0, this.yMaximum])
.domain([this.height,0]);


// Add an SVG element with the desired dimensions and margin.
this.graph = d3.select(this.config.id).append("svg:svg")
.attr("width", this.width + this.m[1] + this.m[3])
.attr("height", this.height + this.m[0] + this.m[2])
.append("svg:g")
.attr("transform", "translate(" + this.m[3] + "," + this.m[0] + ")");

var maskWidth = 640;
var maskHeight = 321;

this.graph.append('svg:defs')
.call(function (defs) {
// Appending the mask
defs.append('svg:mask')
.attr('id', 'mask')
.attr('width', maskWidth)
.attr('height', maskHeight)
.attr('x', 0)
.attr('y', 0)
.call(function(mask) {
mask.append('svg:rect')
.attr('width', maskWidth)
.attr('height', maskHeight)
.attr('fill', '#ffffff')
});
});



// create xAxis
this.xAxis = d3.svg.axis().scale(this.xScale)
.tickSize(-this.height)
.tickSubdivide(true);

// create yAxis
this.yAxis = d3.svg.axis().scale(this.yScale)
.tickSize(-this.width)
.tickSubdivide(true)
.orient("left");

// Add the x-axis label.
this.graph.append("text")
.attr("class", "x label")
.attr("text-anchor", "end")
.attr("x", this.width)
.attr("y", this.height + 25)
.text(this.config.xAxisLabel);

// Add the y-axis label.
this.graph.append("text")
.attr("class", "y label")
.attr("text-anchor", "end")
.attr("y", -30)
.attr("dy", ".75em")
.attr("transform", "rotate(-90)")
.text(this.config.yAxisLabel);

// add Title
this.graph.append("text")
.attr("x", this.width/2 )
.attr("y", -20 )
.attr("text-anchor", "middle")
.style("font-size", "12px")
.text(this.config.title);

// Add the x-axis.
this.graph.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0," + this.height + ")")
.call(this.xAxis);

// Add the y-axis.
this.graph.append("svg:g")
.attr("class", "y axis")
.call(this.yAxis);
},

updateWidth: function(width){
this.width = width - this.m[1] - this.m[3];
},

updateHeight: function(height){
this.height = height - this.m[0] - this.m[2]; // height
},

updateXMaximum: function(mean){
this.xMaximum = 2.5 * mean;
},

addBars: function (data){
var numberOfBars = Math.floor(this.xMaximum);
var barWidth = this.width/numberOfBars;

// Generate a histogram using twenty uniformly-spaced bins.
var histogramData = d3.layout.histogram()
.bins(this.xScale.ticks(numberOfBars))
(data);

//console.trace('typeof: '+typeof this.xScale);
var xScale = this.xScale;
var yScale = this.yScale;
var height = this.height;

this.bars = this.graph.selectAll("bar")
.data(histogramData, function(d){ return d;})
.enter()
.append("rect")
.attr("class","bar")
.attr("fill","steelblue")
.attr("transform", function(d, i) {
var yOffset = height;
return "translate(" + (i * barWidth - barWidth/2) + ","+yOffset+")";
})
.attr("y", function(d,i) {
var yPosition = yScale(d.length)- height;
return (yScale(d.length)-height);
})
.attr("height", function(d) {
return height - yScale(d.length);
})
.attr("width", barWidth - 1)
.attr('mask', 'url(#mask)');
},

addLine: function (data){ // the data must be in the form " [ {'x':x1, 'y':y1} , {'x':x2, 'y':y2} , {'x':x3, 'y':y3} ... ]
var xScale = this.xScale;
var yScale = this.yScale;
var height = this.height;

// create a line function that can convert data[] into x and y points
var lineFunction = d3.svg.line()
// assign the X function to plot our line as we wish
.x(function(d) { return xScale(d.x); })
.y(function(d) { return yScale(d.y); })
.interpolate("linear");

this.lines = this.graph.append("path")
.attr("d", lineFunction(data))
.attr("class", "line")
.attr("stroke", "green")
.attr("stroke-width", 2)
.attr("fill","none");

},

clear: function () {
var bars = d3.selectAll(".bar").remove();
var lines = d3.selectAll(".line").remove();
},

getxScale: function () {
return this.xScale;
},

getxScaleInvert: function () {
return this.xScaleInvert;
}
}




Aucun commentaire:

Enregistrer un commentaire