this is for holding javascript data
Alberto Pepe added file js/d3po-pf.js
over 6 years ago
Commit id: 734a00cfe976c50eafc993f44b19e438d6088ca4
deletions | additions
diff --git a/js/d3po-pf.js b/js/d3po-pf.js
new file mode 100644
index 0000000..b922054
--- /dev/null
+++ b/js/d3po-pf.js
...
// Globals
var allPlotData,
columnNames,
columnDomains,
svg,
stateMarkerStyle, stateHistogramStyle;
/*
Define some default style parameters
*/
// all size / padding parameters are specified in pixels
var defaults = {
"stateStyle" : {
"padding" : {
"top" : 20,
"left" : 50,
"right" : -20,
"bottom" : 20
},
},
"plotStyle" : {
"padding" : {
"top" : 25,
"left" : 25,
"right" : 100,
"bottom" : 50
},
"size" : {
"width" : 200,
"height" : 200
},
"brush" : {
"color" : "#000000",
"opacity" : 0.2
}
},
"markerStyle" : {
"selected" : {
"opacity" : 0.75,
"size" : 3,
"color" : "#333333"
},
"unselected" : {
"opacity" : 0.25,
"size" : 3,
"color" : "#333333"
}
},
"histogramStyle" : {
"selected" : {
"opacity" : 1.,
"color" : "#333333"
},
"unselected" : {
"opacity" : 1.,
"color" : "#cccccc"
}
},
"colorScale" : {
"map" : ["red", "blue"]
},
"bins" : 10,
"tickSize" : 16,
"nTicks" : 5
};
/*
Generalized plotting
*/
// TODO: also pass in 'data' -- list of objects w/ x, y parameters?
function scatter(state, plot, cell) {
// remove any histogram
cell.selectAll("rect.data").data([]).exit().remove();
var circ = cell.selectAll("circle.data")
.data(allPlotData);
circ.enter().append("circle")
.classed("data",true);
circ.transition().duration(500).ease("quad-in")
.attr("cx", function(d) { return state.xScaler(d[plot.xCol]); })
.attr("cy", function(d) { return state.yScaler(d[plot.yCol]); })
.attr("r", function (d,ii) {
if (state.isSelected(d,ii)) {
return plot.style['selected']['size'];
} else {
return plot.style['unselected']['size'];
}
})
.style("fill", function (d,ii) {
if (state.isSelected(d,ii)) {
return state.cScaler(d[state.colorColumn]) || plot.style['selected']["color"];
} else {
return plot.style['unselected']['color'];
}
})
.style("opacity", function (d,ii) {
if (state.isSelected(d,ii)) {
return plot.style['selected']['opacity'];
} else {
return plot.style['unselected']['opacity'];
}
})
.attr("plot-index", plot.index)
.attr("clip-path", "url(#clip)");
circ.exit().remove();
}
// TODO: how to support y histograms?
function histogram(state, plot, cell) {
var nbins = plot.bins;
var rawData = allPlotData.map(function(d) { return parseFloat(d[plot.xCol]); });
var data = d3.layout.histogram().bins(nbins)(rawData);
var barWidth = state.xScaler(2*data[0].dx) - state.xScaler(data[0].dx);
var height = state.plotStyle['size']['height'];
var barHeightScaler = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d.y; })])
.range([height, 0]);
// remove any data points
cell.selectAll("circle.data").data([]).exit().remove();
var bar = cell.selectAll("rect.data").data(data);
bar.enter().append("rect")
.classed("bar", true)
.classed("data", true);
bar.style("fill", function (d,ii) {
if (state.isSelected(d,ii)) {
return plot.style['selected']['color'];
} else {
return plot.style['unselected']['color'];
}
})
.style("opacity", function (d,ii) {
if (state.isSelected(d,ii)) {
return plot.style['selected']['opacity'];
} else {
return plot.style['unselected']['opacity'];
}
});
bar.transition().duration(500).ease("quad-in")
.attr("x", function(d) {
return state.xScaler(d.x);
})
.attr("y", function(d) {
return barHeightScaler(d.y) + state.plotStyle['padding']['top'];
})
.attr("width", barWidth)
.attr("height", function(d) {
return height - barHeightScaler(d.y);
})
.attr("clip-path", "url(#clip)");
bar.exit().remove();
}
/*
Classes for parsing JSON and defaults
*/
Plot = function(jsonPlot) {
this.gridPosition = jsonPlot["gridPosition"] || [0,0];
this.type = jsonPlot["type"] || "scatter";
var xAxis = jsonPlot["xAxis"] || {},
yAxis = jsonPlot["yAxis"] || {};
// Set column names
this.xCol = xAxis["columnName"] || undefined;
this.yCol = yAxis["columnName"] || undefined;
this.xLabel = xAxis["label"] || (this.xCol || "");
this.yLabel = yAxis["label"] || (this.yCol || "");
this.xLim = xAxis["range"];
this.yLim = yAxis["range"];
this.nXTicks = xAxis["nTicks"] || defaults["nTicks"];
this.nYTicks = yAxis["nTicks"] || defaults["nTicks"];
var defaultStyle,
style = jsonPlot["style"] || {};
if (this.type == "scatter") {
defaultStyle = stateMarkerStyle;
} else if (this.type == "histogram") {
defaultStyle = stateHistogramStyle;
this.bins = xAxis['bins'] || yAxis['bins'] || defaults["bins"];
console.debug(this.bins);
} else {
console.log("Invalid type '" + this.type + "'");
return;
}
for (var key in defaultStyle) {
var thisStyle = style[key] || {};
for (var key2 in defaultStyle[key]) {
thisStyle[key2] = thisStyle[key2] || defaultStyle[key][key2];
}
style[key] = thisStyle;
}
this.style = style;
this.translate = function(state) {
// compute the amount to translate the individual plots by
// TODO: why are indices flipped??
var xIndex = this.gridPosition[1],
yIndex = this.gridPosition[0];
xTrans = xIndex * (state.plotStyle['padding']['left'] + state.plotStyle['padding']['right'] +
state.plotStyle['size']['width']);
yTrans = yIndex *(state.plotStyle['padding']['top'] + state.plotStyle['padding']['bottom'] +
state.plotStyle['size']['height']);
return [xTrans,yTrans];
}
this.drawAxes = function(state, cell) {
// x-axis ticks and label
if (this.xCol) {
state.xScaler.domain(this.xLim || columnDomains[this.xCol]);
// set up x axis ticks
xAxisD3 = d3.svg.axis()
.scale(state.xScaler)
.orient("bottom")
.ticks(this.nXTicks)
.tickSize(defaults["tickSize"])
.tickFormat(function (d) {
if (d > 1e4) {
var x = d3.format('e');
return x(d);
} else {
var dd = state.xScaler.domain();
var hack = -parseInt(Math.round(Math.log(dd[1]-dd[0]) / 2.302585092994046)) + 1;
if (hack < 1) {
var x = d3.format('d');
} else {
var x = d3.format('.' + hack + 'f');
}
return x(d);
}
});
var xAxis = cell.selectAll(".x-axis")
.data([this]);
xAxis.enter().append("g")
.attr("class", "axis x-axis");
xAxis.attr("transform", function(p) {
return "translate(0," + (state.plotStyle['size']['height'] + state.plotStyle['padding']['top'] - 10) + ")";
}).each(function(p) {
d3.select(this).call(xAxisD3);
});
xAxis.exit().remove();
// Add axis labels
var xLabel = cell.selectAll(".x-label")
.data([this]);
xLabel.enter().append("text")
.attr("class", "axis-label x-label");
xLabel.exit().remove();
xLabel.text(function(p, i) { return p.xLabel; })
.attr("x", function(p) {
return state.plotStyle['padding']['left'] + state.plotStyle['size']['width']/2. - $(this).width()/2.;
}).attr("y", function(p) {
return state.plotStyle['padding']['top'] + state.plotStyle['size']['height'] + $(this).height() + 15;
});
} else {
var xAxis = cell.selectAll(".x-axis")
.data([]);
xAxis.exit().remove();
var xLabel = cell.selectAll(".x-label")
.data([]);
xLabel.exit().remove();
}
// y axis ticks and label
if (this.yCol) {
state.yScaler.domain(this.yLim || columnDomains[this.yCol]);
// set up y axis ticks
yAxisD3 = d3.svg.axis()
.scale(state.yScaler)
.orient("left")
.ticks(this.nYTicks)
.tickSize(defaults["tickSize"])
.tickFormat(function (d) {
if (d > 1e4) {
var x = d3.format('e');
return x(d);
} else {
var dd = state.xScaler.domain();
var hack = -parseInt(Math.round(Math.log(dd[1]-dd[0]) / 2.302585092994046)) + 1;
if (hack < 1) {
var x = d3.format('d');
} else {
var x = d3.format('.' + hack + 'f');
}
return x(d);
}
});
var yAxis = cell.selectAll(".y-axis")
.data([this]);
yAxis.enter().append("g")
.attr("class", "axis y-axis");
yAxis.transition().duration(0).ease('quad-in')
.attr("transform", function(p, i) {
return "translate(" + (state.plotStyle['padding']['left'] + 10) + ",0)";
}).each(function(p) {
d3.select(this).call(yAxisD3);
});
yAxis.exit().remove();
var yLabel = cell.selectAll(".y-label")
.data([this]);
yLabel.enter().append("text")
.attr("class", "axis-label y-label");
yLabel.exit().remove();
yLabel.text(function(p,i) { return p.yLabel; })
.transition().duration(0).ease('quad-in')
.attr("x", function(p) {
return -((state.plotStyle['padding']['top']) + state.plotStyle['size']['height']/2. + $(this).width()/2.);// deliberately backwards cause rotated
}).attr("y", function(p) {
return -(state.plotStyle['padding']['left']);
});
} else {
var yAxis = cell.selectAll(".y-axis")
.data([]);
yAxis.exit().remove();
var yLabel = cell.selectAll(".y-label")
.data([]);
yLabel.exit().remove();
}
// plot background
var rect = cell.selectAll("rect.frame").data([1]);
rect.enter().append("rect");
rect.exit().remove();
rect.transition().duration(500).ease("quad-in")
.attr("class", "frame")
.attr("x", state.plotStyle['padding']['left'])
.attr("y", state.plotStyle['padding']['top'])
.attr("width", state.plotStyle['size']['width'])
.attr("height", state.plotStyle['size']['height']);
// TODO: fix clippath
d3.select("g.state-g").append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("x", state.plotStyle['padding']['left'])
.attr("y", state.plotStyle['padding']['top'])
.attr("width", state.plotStyle['size']['width'])
.attr("height", state.plotStyle['size']['height']);
}
// TODO: store cell internally? make an 'overlay' method?
this.drawData = function(state, cell) {
if (this.type == "scatter") {
scatter(state, this, cell);
} else if (this.type == "histogram") {
histogram(state, this, cell);
} else {
alert("Invalid plot type.");
return;
}
}
}
State = function(jsonState) {
var grid = jsonState['grid'] || {};
this.nRows = grid['nRows'] || 1;
this.nCols = grid['nColumns'] || 1;
var stateStyle = jsonState["stateStyle"] || {};
var defaultPadding = defaults["stateStyle"]["padding"],
statePadding = stateStyle["padding"] || {};
this.padding = {
"top" : statePadding["top"] || defaultPadding["top"],
"left" : statePadding["left"] || defaultPadding["left"],
"right" : statePadding["right"] || defaultPadding["right"],
"bottom" : statePadding["bottom"] || defaultPadding["bottom"]
};
//// these must go before the plot definitions
// state-global plot padding / size
this.plotStyle = JSON.parse(JSON.stringify(defaults["plotStyle"]));
var plotStyle = jsonState["plotStyle"] || {}; // for this particular state
for (var key in this.plotStyle) {
var tmp = plotStyle[key] || {};
for (var key2 in tmp) {
console.debug("changing state plot " + key + " " + key2 + " from " +
this.plotStyle[key][key2] + " to " + tmp[key2]);
this.plotStyle[key][key2] = tmp[key2];
}
}
// state-global marker styling
stateMarkerStyle = JSON.parse(JSON.stringify(defaults["markerStyle"]));
var markerStyle = jsonState["markerStyle"] || {}; // for this particular state
for (var key in stateMarkerStyle) {
var tmp = markerStyle[key] || {};
for (var key2 in tmp) {
console.debug("changing state marker " + key + " " + key2 + " from " +
stateMarkerStyle[key][key2] + " to " + tmp[key2]);
stateMarkerStyle[key][key2] = tmp[key2];
}
}
// state-global histogram styling
stateHistogramStyle = JSON.parse(JSON.stringify(defaults["histogramStyle"]));
var histogramStyle = jsonState["histogramStyle"] || {}; // for this particular state
for (var key in stateHistogramStyle) {
var tmp = histogramStyle[key] || {};
for (var key2 in tmp) {
console.debug("changing state histogram " + key + " " + key2 + " from " +
stateHistogramStyle[key][key2] + " to " + tmp[key2]);
stateHistogramStyle[key][key2] = tmp[key2];
}
}
// Compute the height / width of the svg element based on the plot size, plot spacing, and figure padding.
var plotHeight = this.plotStyle['padding']['top'] + this.plotStyle['padding']['bottom'] +
this.plotStyle['size']['height'],
plotWidth = this.plotStyle['padding']['left'] + this.plotStyle['padding']['right'] +
this.plotStyle['size']['width'];
this.height = this.padding['top'] + this.padding['bottom'] + this.nRows * plotHeight;
this.width = this.padding['left'] + this.padding['right'] + this.nCols * plotWidth;
console.log(this.height);
this.plots = [];
for (var ii=0; ii < jsonState['plots'].length; ii++) {
var plot = new Plot(jsonState['plots'][ii]);
plot.index = ii;
this.plots.push(plot);
}
// Scalers for x / y axes from data space to pixel space relative to each plot cell
// -- must go after height, width
this.xScaler = d3.scale.linear()
.range([this.plotStyle['padding']['left'],
this.plotStyle['padding']['left'] + this.plotStyle['size']['width']]);
this.yScaler = d3.scale.linear()
.range([this.plotStyle['size']['height'] + this.plotStyle['padding']['top'],
this.plotStyle['padding']['top']]);
this.cScaler = d3.scale.linear();
var colorScale = jsonState['colorScale'] || {};
this.colorColumn = colorScale['columnName'] || undefined;
if (this.colorColumn) {
this.cScaler.domain(columnDomains[this.colorColumn]);
this.cScaler.range(colorScale['map'] || defaults["colorScale"]["map"]);
}
// state caption
this.caption = jsonState["caption"] || "";
/*
brushes
*/
// keep track of what cell is being brushed
// TODO: bug here with histogram...
var brushCell = undefined,
brushPlot = undefined;
state = this;
this.xyBrush = d3.svg.brush()
.x(state.xScaler)
.y(state.yScaler)
.on("brushstart", function(p) {
if (brushPlot === undefined) {
brushPlot = p;
}
if (brushCell !== this) {
if (brushPlot.type == "scatter") {
d3.select(brushCell).call(state.xyBrush.clear());
} else if (brushPlot.type == "histogram") {
d3.select(brushCell).call(state.xBrush.clear());
}
brushCell = this;
brushPlot = p;
} else {
//brushCell = this;
}
d3.select(this).selectAll(".extent")
.style("fill", state.plotStyle['brush']['color'])
.style("fill-opacity", state.plotStyle['brush']['opacity'])
.attr("clip-path", "url(#clip)");
state.xScaler.domain(p.xLim || columnDomains[p.xCol]);
state.yScaler.domain(p.yLim || columnDomains[p.yCol]);
})
.on("brush", function(p) {
var e = state.xyBrush.extent();
var xRange = [e[0][0], e[1][0]],
yRange = [e[0][1], e[1][1]];
console.log(xRange, yRange);
state.jsonSelection = {
"type" : "box",
"bounds" : [
{
"range" : xRange,
"columnName" : p.xCol
},
{
"range" : yRange,
"columnName" : p.yCol
}
]
};
// grey out any histogram bars
svg.selectAll("rect.data")
.style("fill", "#cccccc")
.style("fill-opacity", 0.5);
svg.selectAll("circle.data")
.attr("r", function (d,ii) {
if (state.isSelected(d,ii)) {
return state.plots[$(this).attr('plot-index')].style["selected"]["size"];
} else {
return state.plots[$(this).attr('plot-index')].style["unselected"]["size"];
}
})
.style("fill", function (d,ii) {
if (state.isSelected(d,ii)) {
return state.cScaler(d[state.colorColumn]) || state.plots[$(this).attr('plot-index')].style["selected"]["color"];
} else {
return state.plots[$(this).attr('plot-index')].style["unselected"]["color"];
}
})
.style("opacity", function (d,ii) {
if (state.isSelected(d,ii)) {
return state.plots[$(this).attr('plot-index')].style["selected"]["opacity"];
} else {
return state.plots[$(this).attr('plot-index')].style["unselected"]["opacity"];
}
})
})
this.xBrush = d3.svg.brush()
.x(state.xScaler).y(state.yScaler)
.on("brushstart", function(p) {
if (brushPlot === undefined) {
brushPlot = p;
}
if (brushPlot.type == "scatter") {
d3.select(brushCell).call(state.xyBrush.clear());
} else if (brushPlot.type == "histogram") {
d3.select(brushCell).call(state.xBrush.clear());
}
brushPlot = p;
brushCell = this;
d3.select(this).selectAll(".extent")
.style("fill", state.plotStyle['brush']['color'])
.style("fill-opacity", state.plotStyle['brush']['opacity']);
state.xScaler.domain(p.xLim || columnDomains[p.xCol]);
})
.on("brush", function(p) {
var _extent0 = state.xBrush.extent();
var extent1 = _extent0;
var extent0 = [_extent0[0][0], _extent0[1][0]];
var rawData = allPlotData.map(function(d) { return parseFloat(d[p.xCol]); });
var data = d3.layout.histogram().bins(p.bins)(rawData);
var min_d0, min_d1, e0, e1, dx;
for (var ii=0; ii < (data.length+1); ii++) {
if (ii == data.length) {
dx = data[ii-1].x + data[ii-1].dx
} else {
dx = data[ii].x;
}
var d0 = Math.abs(dx - extent0[0]),
d1 = Math.abs(dx - extent0[1]);
if ((!min_d0) || (d0 < min_d0)) {
min_d0 = d0;
e0 = dx;
}
if ((!min_d1) || (d1 < min_d1)) {
min_d1 = d1;
e1 = dx;
}
}
extent1[0][0] = e0;
extent1[1][0] = e1;
extent1[0][1] = 0;
extent1[1][1] = d3.max(data, function(d) { return d.y; });
d3.select(this).call(state.xBrush.extent(extent1));
d3.select(this).select(".extent")
.attr("height", state.plotStyle['size']['height'])
.attr("y", state.plotStyle['padding']['top'])
.attr("clip-path", "url(#clip)");;
state.jsonSelection = {
"type" : "box",
"bounds" : [
{
"range" : [e0,e1],
"columnName" : p.xCol
}
]
};
svg.selectAll("rect.data")
.style("fill", function(d,ii) {
if ((d.x >= e0) && ((d.x+d.dx) <= e1)) {
return p.style["selected"]["color"];
} else {
return p.style["unselected"]["color"];
}
})
.attr("fill-opacity", function(d,ii) {
if ((d.x >= e0) && ((d.x+d.dx) <= e1)) {
return p.style["selected"]["opacity"];
} else {
return p.style["unselected"]["opacity"];
}
})
svg.selectAll("circle.data")
.attr("r", function (d,ii) {
if (state.isSelected(d,ii)) {
return state.plots[$(this).attr('plot-index')].style["selected"]["size"];
} else {
return state.plots[$(this).attr('plot-index')].style["unselected"]["size"];
}
})
.style("fill", function (d,ii) {
if (state.isSelected(d,ii)) {
return state.cScaler(d[state.colorColumn]) || state.plots[$(this).attr('plot-index')].style["selected"]["color"];
} else {
return state.plots[$(this).attr('plot-index')].style["unselected"]["color"];
}
})
.style("opacity", function (d,ii) {
if (state.isSelected(d,ii)) {
return state.plots[$(this).attr('plot-index')].style["selected"]["opacity"];
} else {
return state.plots[$(this).attr('plot-index')].style["unselected"]["opacity"];
}
})
})
this.jsonSelection = jsonState['selection'];
if (!this.jsonSelection) {
console.debug("no selection provided");
this.jsonSelection = {
"type" : "none"
}
}
this.isSelected = function(d,ii) {
return selectionFunctionDispatch[this.jsonSelection['type']](this,d,ii);
}
}
selectionFunctionDispatch = {};
selectionFunctionDispatch['none'] = function(state,d,ii) {
return false;
}
selectionFunctionDispatch['all'] = function(state,d,ii) {
return true;
}
selectionFunctionDispatch['box'] = function(state,d,ii) {
var boundsList = state.jsonSelection ['bounds'];
var bools = [];
for (var jj=0; jj < boundsList.length; jj++) {
var rng = boundsList[jj]['range'],
colName = boundsList[jj]['columnName'];
if ((d[colName] >= rng[0]) && (d[colName] <= rng[1])) {
bools.push(true);
} else {
bools.push(false);
}
}
return bools.every(Boolean);
}
selectionFunctionDispatch['booleanColumn'] = function(state,d,ii) {
var boolColName = state.jsonSelection['columnName'];
if (allPlotData[ii][boolColName] == 1) {
return true;
}
}
function initialize(jsonFilename, csvFilename) {
/*
Initialize the D3PO figure given a JSON specification.
Parameters
----------
jsonFilename : string
The relative path to a D3PO JSON spec file.
*/
d3.json(jsonFilename, function(error, _tmp) {
if (error) {
console.warn(error);
alert('Unable to find file or error parsing "' + jsonFilename + '"');
}
jsonData = _tmp;
// TODO: figure out type of file, use appropriate reader
d3.csv(csvFilename, function(error, _tmp) {
if (error) {
console.warn(error);
alert('Unable to read/parse file "' + csvFilename + '"');
}
allPlotData = _tmp;
columnNames = d3.keys(allPlotData[0]);
// draw the first state
drawState(jsonData['states'][0]);
var presets = d3.select("#controls .navigation").selectAll("li")
.data(jsonData['states']);
presets.enter().append("li")
.on("click", function(e,i) {
drawState(jsonData['states'][i]);
$(".navigation li").removeClass("selected");
$(this).addClass("selected");
});
presets.text(function(d, i){ return d['name']; });
presets.exit().remove();
$(".navigation li:first-of-type").addClass("selected");
});
});
//
}
function domains(data, columns) {
/*
Get the domains (min, max) for each column in the plot data.
*/
// Define an object to contain the domains (in data space) for each column
var domainByDataColumn = {};
columnNames.forEach(function(colName) {
var domain = d3.extent(data, function(d) { return parseFloat(d[colName]); });
// if parseFloat failed, probably string values in the column
if (isNaN(domain[0]) || isNaN(domain[1])){
var this_col = [];
data.map(function(d) {
this_col.push(d[colName]);
});
domainByDataColumn[colName] = this_col.unique();
} else {
var size = domain[1]-domain[0];
domainByDataColumn[colName] = [domain[0] - size/25.,
domain[1] + size/25.];
}
});
return domainByDataColumn;
}
function drawState(jsonState) {
/*
TODO
*/
// Get the domains in data units for each column in the data file
columnDomains = domains(allPlotData, columnNames);
// define a state object which automatically sets defaults
state = new State(jsonState);
// Define top level svg tag
svg = d3.select("#svg svg");
svg.transition()
.duration(0)
.ease('quad-in')
.attr("width", state.width)
.attr("height", state.height)
.each("end", function () {
$("body").toggleClass("hack");
setTimeout(function (){ $("body").toggleClass("hack"); });
});
// move the plot container group to account for figure padding
svg.append("g")
.attr("class", "state-g")
.attr("transform", "translate(" + state.padding['left'] + "," +
state.padding['top'] + ")");
// Set up a cell group for each plot window, to then draw points and rectangle over
var cells = d3.select("g.state-g").selectAll("g.cell").data(state.plots);
cells.enter().append("g")
.attr("class", "cell");
cells.exit().remove();
cells.transition().duration(0).ease('quad-in')
.attr("transform", function(p) {
var xy = p.translate(state);
return "translate(" + xy[0] + "," + xy[1] + ")";
});
// Add axes to the plots
cells.each(function(p,ii) {
var cell = d3.select(this);
p.drawAxes(state, cell);
p.drawData(state, cell);
if (p.type == "scatter") {
state.xyBrush(cell);
} else if (p.type == "histogram") {
// TODO: y histogram?
state.xBrush(cell);
}
})
// Finally, update caption
var caption = d3.select("#caption")
.style('opacity', 0)
.text(state.caption)
.transition()
.duration(400)
.style('opacity', 1);
}