How to remove lines in between cartogram Maps in d3.js - javascript

I am new to d3.js. Trying to understand the cartogram example give in http://prag.ma/code/d3-cartogram/ . Here they gave example for USA map. I am trying the same for World Map to see how things works. My cartogram map has lines in between. My data has values for only few countries so I am setting the rest of the country's value as low or 0.
<!DOCTYPE html>
<html>
<head>
<title>Cartograms with d3 & TopoJSON</title>
<meta charset="utf-8">
<meta property="og:image" content="placeholder.png">
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="lib/colorbrewer.js"></script>
<script src="lib/topojson.js"></script>
<script src="cartogram.js"></script>
<style type="text/css">
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.4em;
padding: 0;
margin: 0;
}
#container {
width: 960px;
margin: 20px auto;
}
h1 {
font-size: 200%;
margin: 0 0 15px 0;
}
h2 {
font-size: 160%;
margin: 0 0 10px 0;
}
p {
margin: 0 0 10px;
}
form, form > * {
margin: 0;
}
#status {
color: #999;
}
#map-container {
height: 700px;
text-align: center;
position: relative;
margin: 20px 0;
}
#map {
display: block;
position: absolute;
background: #fff;
width: 100%;
height: 100%;
margin: 0;
}
path.state {
stroke: #666;
stroke-width: .5;
}
path.state:hover {
stroke: #000;
}
form {
font-size: 120%;
}
select {
font-size: inherit;
}
#placeholder {
position: absolute;
z-index: -1;
display: block;
left: 0;
top: 0;
}
</style>
</head>
<body>
<div id="container">
<h1>Cartograms with d3 & TopoJSON</h1>
<form>
<p>
<label>Scale by <select id="field"></select></label>
<span id="status"></span>
</p>
</form>
<div id="map-container">
<svg id="map"></svg>
</div>
</div>
<script>
var margin = 1,
width = 970 - margin,
height = 700 - margin;
if (!document.createElementNS) {
document.getElementsByTagName("form")[0].style.display = "none";
}
var percent = (function() {
var fmt = d3.format(".2f");
return function(n) { return fmt(n) + "%"; };
})(),
fields = [
{name: "(no scale)", id: "none"},
{name: "Internet_Users", id: "internet", key: "Internet_Users", format : percent},
{name: "GDP", id: "gdp", key: "GDP"},
{name: "Literacy_rates", id: "literacy", key: "Literacy_rates", format : percent},
{name: "female_male", id: "fm", key: "female_male"},
{name: "Population", id: "pop", key: "Population"},
],
fieldsById = d3.nest()
.key(function(d) { return d.id; })
.rollup(function(d) { return d[0]; })
.map(fields),
field = fields[0],
colors = colorbrewer.RdYlBu[3]
.reverse()
.map(function(rgb) { return d3.hsl(rgb); });
var body = d3.select("body"),
stat = d3.select("#status");
var fieldSelect = d3.select("#field")
.on("change", function(e) {
field = fields[this.selectedIndex];
location.hash = "#" + [field.id]
});
fieldSelect.selectAll("option")
.data(fields)
.enter()
.append("option")
.attr("value", function(d) { return d.id; })
.text(function(d) { return d.name; });
var map = d3.select("#map").attr("width", width + margin)
.attr("height", height + margin),
zoom = d3.behavior.zoom()
.translate([-38, 32])
.scale(.95)
.scaleExtent([0.5, 10.0])
.on("zoom", updateZoom),
layer = map.append("g")
.attr("id", "layer"),
states = layer.append("g")
.attr("id", "states")
.selectAll("path");
updateZoom();
function updateZoom() {
var scale = zoom.scale();
layer.attr("transform",
"translate(" + zoom.translate() + ") " +
"scale(" + [scale, scale] + ")");
}
var proj = d3.geo.mercator().scale(145).translate([width / 2, height / 1.5]),
topology,
geometries,
rawData,
dataById = {},
carto = d3.cartogram()
.projection(proj)
.properties(function(d) {
return dataById[d.id];
})
.value(function(d) {
return +d.properties[field];
});
window.onhashchange = function() {
parseHash();
};
d3.json("data/world_countries_topo.json", function(topo) {
topology = topo;
// console.log("T",topology)
geometries = topology.objects.countries.geometries;
d3.csv("data/parallel_score.csv", function(data) {
rawData = data;
dataById = d3.nest()
.key(function(d) { return d.Id; })
.rollup(function(d) { return d[0]; })
.map(data);
init();
});
});
function init() {
var features = carto.features(topology, geometries),
path = d3.geo.path()
.projection(proj);
states = states.data(features)
.enter()
.append("path")
.attr("class", "state")
.attr("id", function(d) {
return d.Id;
})
.attr("fill", "#000")
.attr("d", path);
states.append("title");
parseHash();
}
function reset() {
stat.text("");
body.classed("updating", false);
var features = carto.features(topology, geometries),
path = d3.geo.path()
.projection(proj);
states.data(features)
.transition()
.duration(750)
.ease("linear")
.attr("fill", "#fafafa")
.attr("d", path);
states.select("title")
.text(function(d) {
return d.Id;
});
}
function update() {
var start = Date.now();
body.classed("updating", true);
var key = field.key
var fmt = (typeof field.format === "function")
? field.format
: d3.format(field.format || ","),
value = function(d) {
if(d.properties == undefined){}
else {
return +d.properties[key];
}
},
values = states.data()
.map(value)
.filter(function(n) {
return !isNaN(n);
})
.sort(d3.ascending),
lo = values[0],
hi = values[values.length - 1];
console.log("L",lo)
console.log("H",hi)
var color = d3.scale.linear()
.range(colors)
.domain(lo < 0
? [lo, 0, hi]
: [lo, d3.mean(values), hi]);
// normalize the scale to positive numbers
var scale = d3.scale.linear()
.domain([lo, hi])
.range([1, 1000]);
// tell the cartogram to use the scaled values
carto.value(function(d) {
if( value(d) == undefined) {
return lo
}
else {
console.log("SCale", (value(d)))
return scale(value(d));
}
});
// generate the new features, pre-projected
var features = carto(topology, geometries).features;
// update the data
states.data(features)
.select("title")
/*.text(function(d) {
return [d.properties.Id, fmt(value(d))].join(": ");
});*/
states.transition()
.duration(750)
.ease("linear")
.attr("fill", function(d) {
if(d.properties == undefined){
return color(lo)
}
else {
return color(value(d));
}
})
.attr("d", carto.path);
var delta = (Date.now() - start) / 1000;
stat.text(["calculated in", delta.toFixed(1), "seconds"].join(" "));
body.classed("updating", false);
}
var deferredUpdate = (function() {
var timeout;
return function() {
var args = arguments;
clearTimeout(timeout);
stat.text("calculating...");
return timeout = setTimeout(function() {
update.apply(null, arguments);
}, 10);
};
})();
var hashish = d3.selectAll("a.hashish")
.datum(function() {
return this.href;
});
function parseHash() {
var parts = location.hash.substr(1).split("/"),
desiredFieldId = parts[0],
field = fieldsById[desiredFieldId] || fields[0];
fieldSelect.property("selectedIndex", fields.indexOf(field));
if (field.id === "none") {
reset();
} else {
deferredUpdate();
location.replace("#" + [field.id].join("/"));
hashish.attr("href", function(href) {
return href + location.hash;
});
}
}
</script>
</body>
</html>
Here is the link of my map: My Map
Can Someone please explain me why I am getting this line.
Thanks.

We had the same problem a year or so back and it is due to the arcs in the topojson file moving from 180 or 360 back to 0, basically wrapping at the ends of the map.
We needed to manually go into the map file and edit it using QGIS.
This resolved the issue of the lines.
You will also find if you are using the Cartogram code that the map of the world is far to detailed than you will need given you will be distorting the map anyway. If you are generating the cartogram in real time then you will face delays in the code.
You should probably reduce the complexity of the map too.
Here is an example of the JSON we used to create real time hexagonal cartograms in the browser.
Simplified World Map JSON

Related

Connecting points by mouse click - flickering behavior on hover

This code works but has an issue:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.3/d3.min.js"></script>
<title>Document</title>
<style>
svg {
border: 1px solid red;
}
line {
stroke: black;
stroke-width: 2px;
stroke-linecap: square;
}
circle {
fill: red;
stroke: black;
stroke-width: 0;
}
circle:hover {
stroke-width: 3px;
}
</style>
</head>
<body>
<script>
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var marbles = [
{ name: "m1" },
{ name: "m2" },
{ name: "m3" },
{ name: "m4" },
{ name: "m5" },
{ name: "m6" },
{ name: "m7" },
{ name: "m8" },
{ name: "m9" },
{ name: "m10" },
{ name: "m11" },
{ name: "m12" },
];
var coords = [];
for (i = 0; i < marbles.length; i++) {
coords.push({
x: getRandomInt(10, 590),
y: getRandomInt(10, 390),
r: 10,
});
}
var clickables = Array.from({ length: marbles.length }, (v, i) => i);
function arrayRemove(arr, value) {
return arr.filter(function (ele) {
return ele != value;
});
}
var line;
var mx = 0;
var my = 0;
var pt0 = [-1, -1];
var pt1 = [-1, -1];
var vis = d3
.select("body")
.append("svg")
.attr("width", 600)
.attr("height", 400);
function mousemove() {
var m = d3.mouse(this);
line.attr("x2", m[0]).attr("y2", m[1]);
}
function addLine(i_, x_, y_) {
if (clickables.includes(i_)) {
if (pt0[0] == -1) {
clickables = arrayRemove(clickables, i_);
pt0 = [x_, y_];
line = vis
.append("line")
.attr("x1", x_)
.attr("y1", y_)
.attr("x2", x_)
.attr("y2", y_);
vis.on("mousemove", mousemove);
} else if (pt1[0] == -1) {
clickables = arrayRemove(clickables, i_);
console.log("clicked on target");
line.attr("x2", x_).attr("y2", y_);
vis.on("mousemove", null);
pt0 = [-1, -1];
}
}
}
vis
.selectAll("circle")
.data(coords)
.enter()
.append("circle")
.attr("r", function (d, i) {
return d.r;
})
.attr("cx", function (d, i) {
return d.x;
})
.attr("cy", function (d, i) {
return d.y;
})
.on("click", function (d, i) {
addLine(i, d.x, d.y);
});
</script>
</body>
</html>
Random points can be connected with a line through mouse clicks (only in pairs). A first click on a point creates a line that starts at the clicked point and ends at the cursor position, moving around with it. So far so good.
A second click on a target point fixes the line between first and second point.
Problem: a lot of wiggling is required to get the second point highlighted (black rim) and finally fix the connection with a click.
Note that when choosing the first point, highlight on hover is quite responsive; but it's rather unpredictable when choosing the second point.
the line element is getting in the way. add the css rule pointer-events: none; to the line's css block

Is there a way alter the color lines on D3.js line chart?

fairly new to D3.js but keen to update the color scheme of my currently working version, the color: steelblue is used in the .css files but seems to be getting overridden from somewhere else, any idea how to best go about this? (as you can see i've tried updating to pink)
.chart-wrapper .line {
fill: none;
stroke: pink;
stroke-width: 5px;
}
// Render the chart
chartObj.render = function () {
//Create SVG element
chartObj.svg = chartObj.chartDiv
.append('svg')
.attr('class', 'chart-area')
.attr(
'width',
chartObj.width + (chartObj.margin.left + chartObj.margin.right)
)
.attr(
'height',
chartObj.height + (chartObj.margin.top + chartObj.margin.bottom)
)
.append('g')
.attr(
'transform',
'translate(' + chartObj.margin.left + ',' + chartObj.margin.top + ')'
);
// Draw Lines
for (var y in yObjs) {
yObjs[y].path = chartObj.svg
.append('path')
.datum(chartObj.data)
.attr('class', 'line')
.attr('d', yObjs[y].line)
.style('stroke', color(y))
.attr('data-series', y)
.on('mouseover', function () {
focus.style('display', null);
})
.on('mouseout', function () {
focus.transition().delay(700).style('display', 'none');
})
.on('mousemove', mousemove);
}
return chartObj;
}
It looks like you are basing your code on this example here which is quite an in-depth place to start if you are new to d3.
I have modified the code to utilize the colour specified in the configuration object which appears to have been proposed but not implemented by the author of the example.
Previously the colours were generated according to the built-in d3.scale.category10 colours but this has been removed.
This example code also uses d3 version 3 which probably means it is not a great starting point as the newer versions provide many API improvements. If you are interested in learning more about d3 colour schemes then please see this article here
function makeLineChart(dataset, xName, yObjs, axisLables) {
var chartObj = {};
var color = d3.scale.category10();
chartObj.xAxisLable = axisLables.xAxis;
chartObj.yAxisLable = axisLables.yAxis;
/*
yObjsects format:
{y1:{column:'',name:'name',color:'color'},y2}
*/
chartObj.data = dataset;
chartObj.margin = {top: 15, right: 60, bottom: 30, left: 50};
chartObj.width = 650 - chartObj.margin.left - chartObj.margin.right;
chartObj.height = 480 - chartObj.margin.top - chartObj.margin.bottom;
// So we can pass the x and y as strings when creating the function
chartObj.xFunct = function(d){return d[xName]};
// For each yObjs argument, create a yFunction
function getYFn(column) {
return function (d) {
return d[column];
};
}
// Object instead of array
chartObj.yFuncts = [];
for (var y in yObjs) {
yObjs[y].name = y;
yObjs[y].yFunct = getYFn(yObjs[y].column); //Need this list for the ymax function
chartObj.yFuncts.push(yObjs[y].yFunct);
yObjs[y].color = d3.rgb(yObjs[y].color);
}
//Formatter functions for the axes
chartObj.formatAsNumber = d3.format(".0f");
chartObj.formatAsDecimal = d3.format(".2f");
chartObj.formatAsCurrency = d3.format("$.2f");
chartObj.formatAsFloat = function (d) {
if (d % 1 !== 0) {
return d3.format(".2f")(d);
} else {
return d3.format(".0f")(d);
}
};
chartObj.xFormatter = chartObj.formatAsNumber;
chartObj.yFormatter = chartObj.formatAsFloat;
chartObj.bisectYear = d3.bisector(chartObj.xFunct).left; //< Can be overridden in definition
//Create scale functions
chartObj.xScale = d3.scale.linear().range([0, chartObj.width]).domain(d3.extent(chartObj.data, chartObj.xFunct)); //< Can be overridden in definition
// Get the max of every yFunct
chartObj.max = function (fn) {
return d3.max(chartObj.data, fn);
};
chartObj.yScale = d3.scale.linear().range([chartObj.height, 0]).domain([0, d3.max(chartObj.yFuncts.map(chartObj.max))]);
chartObj.formatAsYear = d3.format("");
//Create axis
chartObj.xAxis = d3.svg.axis().scale(chartObj.xScale).orient("bottom").tickFormat(chartObj.xFormatter); //< Can be overridden in definition
chartObj.yAxis = d3.svg.axis().scale(chartObj.yScale).orient("left").tickFormat(chartObj.yFormatter); //< Can be overridden in definition
// Build line building functions
function getYScaleFn(yObj) {
return function (d) {
return chartObj.yScale(yObjs[yObj].yFunct(d));
};
}
for (var yObj in yObjs) {
yObjs[yObj].line = d3.svg.line().interpolate("cardinal").x(function (d) {
return chartObj.xScale(chartObj.xFunct(d));
}).y(getYScaleFn(yObj));
}
chartObj.svg;
// Change chart size according to window size
chartObj.update_svg_size = function () {
chartObj.width = parseInt(chartObj.chartDiv.style("width"), 10) - (chartObj.margin.left + chartObj.margin.right);
chartObj.height = parseInt(chartObj.chartDiv.style("height"), 10) - (chartObj.margin.top + chartObj.margin.bottom);
/* Update the range of the scale with new width/height */
chartObj.xScale.range([0, chartObj.width]);
chartObj.yScale.range([chartObj.height, 0]);
if (!chartObj.svg) {return false;}
/* Else Update the axis with the new scale */
chartObj.svg.select('.x.axis').attr("transform", "translate(0," + chartObj.height + ")").call(chartObj.xAxis);
chartObj.svg.select('.x.axis .label').attr("x", chartObj.width / 2);
chartObj.svg.select('.y.axis').call(chartObj.yAxis);
chartObj.svg.select('.y.axis .label').attr("x", -chartObj.height / 2);
/* Force D3 to recalculate and update the line */
for (var y in yObjs) {
yObjs[y].path.attr("d", yObjs[y].line);
}
d3.selectAll(".focus.line").attr("y2", chartObj.height);
chartObj.chartDiv.select('svg').attr("width", chartObj.width + (chartObj.margin.left + chartObj.margin.right)).attr("height", chartObj.height + (chartObj.margin.top + chartObj.margin.bottom));
chartObj.svg.select(".overlay").attr("width", chartObj.width).attr("height", chartObj.height);
return chartObj;
};
chartObj.bind = function (selector) {
chartObj.mainDiv = d3.select(selector);
// Add all the divs to make it centered and responsive
chartObj.mainDiv.append("div").attr("class", "inner-wrapper").append("div").attr("class", "outer-box").append("div").attr("class", "inner-box");
chartSelector = selector + " .inner-box";
chartObj.chartDiv = d3.select(chartSelector);
d3.select(window).on('resize.' + chartSelector, chartObj.update_svg_size);
chartObj.update_svg_size();
return chartObj;
};
// Render the chart
chartObj.render = function () {
//Create SVG element
chartObj.svg = chartObj.chartDiv.append("svg").attr("class", "chart-area").attr("width", chartObj.width + (chartObj.margin.left + chartObj.margin.right)).attr("height", chartObj.height + (chartObj.margin.top + chartObj.margin.bottom)).append("g").attr("transform", "translate(" + chartObj.margin.left + "," + chartObj.margin.top + ")");
// Draw Lines
for (var y in yObjs) {
yObjs[y].path = chartObj.svg.append("path").datum(chartObj.data).attr("class", "line").attr("d", yObjs[y].line).style("stroke", yObjs[y].color).attr("data-series", y).on("mouseover", function () {
focus.style("display", null);
}).on("mouseout", function () {
focus.transition().delay(700).style("display", "none");
}).on("mousemove", mousemove);
}
// Draw Axis
chartObj.svg.append("g").attr("class", "x axis").attr("transform", "translate(0," + chartObj.height + ")").call(chartObj.xAxis).append("text").attr("class", "label").attr("x", chartObj.width / 2).attr("y", 30).style("text-anchor", "middle").text(chartObj.xAxisLable);
chartObj.svg.append("g").attr("class", "y axis").call(chartObj.yAxis).append("text").attr("class", "label").attr("transform", "rotate(-90)").attr("y", -42).attr("x", -chartObj.height / 2).attr("dy", ".71em").style("text-anchor", "middle").text(chartObj.yAxisLable);
//Draw tooltips
var focus = chartObj.svg.append("g").attr("class", "focus").style("display", "none");
for (var y in yObjs) {
yObjs[y].tooltip = focus.append("g");
yObjs[y].tooltip.append("circle").attr("r", 5);
yObjs[y].tooltip.append("rect").attr("x", 8).attr("y","-5").attr("width",22).attr("height",'0.75em');
yObjs[y].tooltip.append("text").attr("x", 9).attr("dy", ".35em");
}
// Year label
focus.append("text").attr("class", "focus year").attr("x", 9).attr("y", 7);
// Focus line
focus.append("line").attr("class", "focus line").attr("y1", 0).attr("y2", chartObj.height);
//Draw legend
var legend = chartObj.mainDiv.append('div').attr("class", "legend");
for (var y in yObjs) {
series = legend.append('div');
series.append('div').attr("class", "series-marker").style("background-color", yObjs[y].color);
series.append('p').text(y);
yObjs[y].legend = series;
}
// Overlay to capture hover
chartObj.svg.append("rect").attr("class", "overlay").attr("width", chartObj.width).attr("height", chartObj.height).on("mouseover", function () {
focus.style("display", null);
}).on("mouseout", function () {
focus.style("display", "none");
}).on("mousemove", mousemove);
return chartObj;
function mousemove() {
var x0 = chartObj.xScale.invert(d3.mouse(this)[0]), i = chartObj.bisectYear(dataset, x0, 1), d0 = chartObj.data[i - 1], d1 = chartObj.data[i];
try {
var d = x0 - chartObj.xFunct(d0) > chartObj.xFunct(d1) - x0 ? d1 : d0;
} catch (e) { return;}
minY = chartObj.height;
for (var y in yObjs) {
yObjs[y].tooltip.attr("transform", "translate(" + chartObj.xScale(chartObj.xFunct(d)) + "," + chartObj.yScale(yObjs[y].yFunct(d)) + ")");
yObjs[y].tooltip.select("text").text(chartObj.yFormatter(yObjs[y].yFunct(d)));
minY = Math.min(minY, chartObj.yScale(yObjs[y].yFunct(d)));
}
focus.select(".focus.line").attr("transform", "translate(" + chartObj.xScale(chartObj.xFunct(d)) + ")").attr("y1", minY);
focus.select(".focus.year").text("Year: " + chartObj.xFormatter(chartObj.xFunct(d)));
}
};
return chartObj;
}
.chart-wrapper {
max-width: 950px;
min-width: 304px;
margin: 0 auto;
background-color: #FAF7F7;
}
.chart-wrapper .inner-wrapper {
position: relative;
padding-bottom: 50%;
width: 100%;
}
.chart-wrapper .outer-box {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.chart-wrapper .inner-box {
width: 100%;
height: 100%;
}
.chart-wrapper text {
font-family: sans-serif;
font-size: 11px;
}
.chart-wrapper p {
font-size: 16px;
margin-top: 5px;
margin-bottom: 40px;
}
.chart-wrapper .axis path,
.chart-wrapper .axis line {
fill: none;
stroke: #1F1F2E;
stroke-opacity: 0.7;
shape-rendering: crispEdges;
}
.chart-wrapper .axis path {
stroke-width: 2px;
}
.chart-wrapper .line {
fill: none;
stroke: steelblue;
stroke-width: 5px;
}
.chart-wrapper .legend {
min-width: 200px;
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
font-size: 16px;
padding: 10px 40px;
}
.chart-wrapper .legend>div {
margin: 0px 25px 10px 0px;
flex-grow: 0;
}
.chart-wrapper .legend p {
display: inline;
font-size: 0.8em;
font-family: sans-serif;
font-weight: 600;
}
.chart-wrapper .legend .series-marker {
height: 1em;
width: 1em;
border-radius: 35%;
background-color: crimson;
display: inline-block;
margin-right: 4px;
margin-bottom: -0.16rem;
}
.chart-wrapper .overlay {
fill: none;
pointer-events: all;
}
.chart-wrapper .focus circle {
fill: crimson;
stroke: crimson;
stroke-width: 2px;
fill-opacity: 15%;
}
.chart-wrapper .focus rect {
fill: lightblue;
opacity: 0.4;
border-radius: 2px;
}
.chart-wrapper .focus.line {
stroke: steelblue;
stroke-dasharray: 2, 5;
stroke-width: 2;
opacity: 0.5;
}
#media (max-width:500px) {
.chart-wrapper .line {
stroke-width: 3px;
}
.chart-wrapper .legend {
font-size: 14px;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<link rel="stylesheet" type="text/css" href="multiline.css">
<script src="https://d3js.org/d3.v3.js" charset="utf-8"></script>
<!--<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>-->
</head>
<body>
<div class="chart-wrapper" id="chart-line1"></div>
<script type="text/javascript">
d3.csv('https://gist.githubusercontent.com/asielen/44ffca2877d0132572cb/raw/9c18f31f9ffbb402b070d88efc9a6381d57c0735/multiline_data.csv', function(error, data) {
data.forEach(function(d) {
d.year = +d.year;
d.variableA = +d.variableA;
d.variableB = +d.variableB;
});
var chart = makeLineChart(
data,
'year', {
FinTechs: {
column: 'variableA',
color:'pink'
},
SMEs: {
column: 'variableB',
color: 'green'
},
}, {
xAxis: 'Year',
yAxis: 'Amount'
}
);
chart.bind('#chart-line1');
chart.render();
});
</script>
</body>
</html>

How to zoom faster in a SVG drawing with D3?

I am working on a project with D3.js that displays regions of interest (ROI) which are <g> elements with one <polygon> and one <text>. I noticed that zooming becomes very slow when there are a lot of ROI and it seems that this is mostly because of the texts, i.e. when they are hidden with display: none, zoom is much faster. The zoom speed is different in every browser: Firefox is quite fast, Chrome is average and Edge is slow.
I tried to speed up the text rendering by using the CSS property text-rendering: optimizeSpeed; but the difference is marginal. I noticed that some fonts are faster to render than others. Currently the best results I obtained is by using font-family: monospace;.
So my question is: How to zoom faster in an SVG drawing with D3? Is there a font that is known to be faster to render than others? Or is there a CSS, SVG or D3 trick that could help?
You can test the zoom speed with the snippet. If you click on the button, the text will be hidden an zooming will be much faster.
"use strict";
// Create data with this structure:
// DATA = [{ name: "", coords: [x0, y0, x1, y1, x2, y2, x3, y3]}]
var nbPolyX = 100;
var nbPolyY = 50;
var sqSize = 800 / nbPolyX;
var DATA = [];
for (let idY = 0; idY < nbPolyY; idY++) {
for (let idX = 0; idX < nbPolyX; idX++) {
DATA.push({
name: "x" + idX + "y" + idY,
coords: [
idX * sqSize + 1, idY * sqSize + 1,
(idX + 1) * sqSize - 1, idY * sqSize + 1,
(idX + 1) * sqSize - 1, (idY + 1) * sqSize - 1,
idX * sqSize + 1, (idY + 1) * sqSize - 1
]
})
}
}
var SVGELEM = {};
var ZOOMER = {};
var TRNSF = {
k: 1,
x: 0,
y: 0
};
var ZOOMER = {};
var GZOOM = {};
var ROI = {};
var POLY = {};
var TXT = {};
var BUTTON = {};
addButton();
addSVG();
function addSVG() {
ZOOMER = d3
.zoom()
.scaleExtent([0.9, 40])
.on("zoom", function () {
onZoom();
});
SVGELEM = d3.select("body").append("svg")
.attr("width", nbPolyX * sqSize)
.attr("height", nbPolyY * sqSize)
.call(ZOOMER);
GZOOM = SVGELEM.append("g");
ROI = GZOOM.selectAll("g")
.data(DATA)
.enter()
.append("g")
.classed("roi", true);
POLY = ROI.selectAll("polygon")
.data(function (d) {
return [d.coords];
})
.enter()
.append("polygon")
.attr("points", function (d) {
return d;
});
TXT = ROI.selectAll("text")
.data(function (d) {
var nbElem = d.coords.length;
// Polygon mean point X.
var xMean = 0;
for (let index = 0; index < nbElem - 1; index += 2) {
xMean += d.coords[index];
}
xMean /= nbElem / 2;
// Polygon mean point Y.
var yMean = 0;
for (let index = 1; index < nbElem; index += 2) {
yMean += d.coords[index];
}
yMean /= nbElem / 2;
// Return value.
var ret = {
name: d.name,
x: xMean,
y: yMean
};
return [ret];
})
.enter()
.append("text")
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
})
.text(function (d) {
return d.name;
});
}
function addButton() {
BUTTON = d3.select("body").append("button")
.text("HIDE TEXT")
.on("click", function btnOnClick() {
btnOnClick.state = !btnOnClick.state;
d3.selectAll("text").classed("cl_display_none", btnOnClick.state);
if (btnOnClick.state) d3.select(this).text("SHOW TEXT");
else d3.select(this).text("HIDE TEXT");
});
}
function onZoom() {
if (d3.event !== null) TRNSF = d3.event.transform;
GZOOM.attr(
"transform",
"translate(" + TRNSF.x + "," + TRNSF.y + ") scale(" + TRNSF.k + ")"
);
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>SVG ZOOM SPEED</title>
<style>
body {
text-align: center;
font-family: monospace;
display: block;
margin: 0 auto;
}
svg {
margin: 10px auto;
border: 1px solid;
display: block;
}
.roi polygon {
shape-rendering: optimizeSpeed;
vector-effect: non-scaling-stroke;
fill: rgba(0, 255, 0, 0.25);
stroke: rgba(0, 255, 0, 1);
stroke-width: 1px;
}
.roi text {
text-rendering: optimizeSpeed;
font-family: monospace;
font-size: 1px;
text-anchor: middle;
dominant-baseline: middle;
}
.cl_display_none {
display: none;
}
button {
width: 150px;
height: 50px;
font-family: monospace;
font-size: 15pt;
margin: 0;
}
</style>
</head>
<body>
<h3>SVG ZOOM SPEED</h3>
<p>Use the mouse wheel to zoom in and out the SVG drawing then hide the text with the button and observe the speed difference. Test it in different browsers.</p>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="zoom_speed.js"></script>
</body>
</html>

Bar chart to take account of negative values

I have the following data in a csv file called BarData.csv:
Fruit,dt,amount
Apple,12/28/2016,-1256
Apple,12/29/2016,-500
Apple,12/30/2016,3694
Apple,12/31/2016,5586
Apple,1/1/2017,4558
Apple,1/2/2017,6696
Apple,1/3/2017,7757
Apple,1/4/2017,8528
Apple,1/5/2017,5543
Apple,1/6/2017,3363
Apple,1/7/2017,5464
Pear,12/25/2017,250
Pear,12/26/2017,669
Pear,12/27/2017,441
Pear,12/28/2017,159
Pear,12/29/2017,357
Pear,12/30/2017,775
Pear,12/31/2017,669
The following html, css, and javascript is in one .html file:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>BAR SINGLE FUNCTION</title>
<script src="http://d3js.org/d3.v3.js"></script>
<style type="text/css">
#radioDiv {
top: 45px;
font-family: verdana;
font-size: 8px;
width: 455px;
}
#TOPbarChart {
position: absolute;
top: 50px;
left: 30px;
width: 750px;
height: 195px;
}
.axis--y path,
.axis--x path {
display: none;
}
.axis--x line,
.axis--y line {
stroke: black;
fill: none;
stroke-width: 2px
}
.yAxis text,
.xAxis text {
font: 7pt Verdana;
stroke: none;
fill: black;
}
.title,
.titleX {
font-family: Verdana;
font-size: 10px;
}
</style>
</head>
<body>
<div id="radioDiv">
<label>
<input id="radioFrt" type="radio" name="frt" value="Apple" class="radioB" checked> APPLE
</label>
<label>
<input type="radio" name="frt" value="Pear" class="radioB"> PEAR
</label>
</div>
<div id="TOPbarChart"></div>
<script type="text/javascript">
var currentFruit = "Apple";
var currentColr = "#00a5b6";
var barDataCSV_Dly = "BarData.csv";
//
//
// radio button
document.getElementById("radioFrt").checked = true;
d3.selectAll('input[name="frt"]').on("change", function change() {
currentFruit = this.value;
TOPbarChart(currentFruit, currentColr);
});
//FORMATS
var parseDate = d3.time.format("%m/%d/%Y").parse;
//
// BASIC SIZING
//
function barChartBasics() {
var margin = {
top: 25,
right: 35,
bottom: 25,
left: 70
},
width = 550 - margin.left - margin.right,
height = 155 - margin.top - margin.bottom,
colorBar = d3.scale.category20(),
barPaddingFine = 1,
barPaddingThick = 2;
return {
margin: margin,
width: width,
height: height,
colorBar: colorBar,
barPaddingFine: barPaddingFine,
barPaddingThick: barPaddingThick
};
}
// create svg element
var basics = barChartBasics();
var svg = d3.select("#TOPbarChart")
.append("svg")
.attr({
"width": basics.width + basics.margin.left + basics.margin.right,
"height": basics.height + basics.margin.top + basics.margin.bottom,
id: "svgTOPbarChart"
});
// create svg group
var plot = svg
.append("g")
.attr({
"transform": "translate(" + basics.margin.left + "," + basics.margin.top + ")",
id: "svgPlotTOPbarChart"
});
var axisPadding = 2;
var leftAxisGroup = svg
.append('g')
.attr({
transform: 'translate(' + (basics.margin.left - axisPadding) + ',' + (basics.margin.top) + ')',
'class': "yAxis axis--y",
id: "yAxisGTOPbarChart"
});
var bottomAxisGroup = svg
.append('g')
.attr({
'class': "xAxis axis--x",
id: "xAxisGTOPbarChart"
});
var titleTxt = svg.append("text")
.attr({
x: basics.margin.left + 12,
y: 20,
'class': "title",
'text-anchor': "start"
})
// create scales with ranges
var xScale = d3.time.scale().range([0, basics.width]);
var yScale = d3.scale.linear().range([basics.height, 0]);
function TOPbarChart(
frt, colorChosen) {
// get the data
d3.csv(barDataCSV_Dly, function(rows) {
TOPbarData = rows.map(function(d) {
return {
"Fruit": d.Fruit,
"dt": parseDate(d.dt),
"amount": +d.amount
};
}).filter(function(row) {
if (row['Fruit'] == frt) {
return true;
}
});
// create domains for the scales
xScale.domain(d3.extent(TOPbarData, function(d) {
return d.dt;
}));
var amounts = TOPbarData.map(function(d) {
return d.amount;
});
var yMax = d3.max(amounts);
var yMin = d3.min(amounts);
var yMinFinal = 0;
if (yMin < 0) {
yMinFinal = yMin;
}
yScale.domain([yMinFinal, yMax]);
// introduce the bars
// var plot = d3.select("#svgPlotTOPbarChart")
var sel = plot.selectAll("rect")
.data(TOPbarData);
sel.enter()
.append("rect")
.attr({
x: function(d, i) {
return xScale(d.dt);
},
y: function(d) {
return yScale(d.amount);
},
width: (basics.width / TOPbarData.length - basics.barPaddingFine),
height: function(d) {
return basics.height - yScale(d.amount);
},
fill: colorChosen,
'class': "bar"
});
// this little function will create a small ripple affect during transition
var dlyRipple = function(d, i) {
return i * 100;
};
sel
.transition()
.duration(dlyRipple) //1000
.attr({
x: function(d, i) {
return xScale(d.dt);
},
y: function(d) {
return yScale(d.amount);
},
width: (basics.width / TOPbarData.length - basics.barPaddingFine),
height: function(d) {
return basics.height - yScale(d.amount);
},
fill: colorChosen
});
sel.exit().remove();
// add/transition y axis - with ticks and tick markers
var axisY = d3.svg.axis()
.orient('left')
.scale(yScale)
.tickFormat(d3.format("s")) // use abbreviations, e.g. 5M for 5 Million
.outerTickSize(0);
leftAxisGroup.transition().duration(1000).call(axisY);
// add/transition x axis - with ticks and tick markers
var axisX = d3.svg.axis()
.orient('bottom')
.scale(xScale);
bottomAxisGroup
.attr({
transform: 'translate(' + (basics.margin.left + ((basics.width / TOPbarData.length) / 2)) + ',' + (basics.margin.top + basics.height) + ')',
})
.transition().duration(1000).call(axisX.ticks(5));
titleTxt.text("Daily: last " + TOPbarData.length + " days");
// console.log(TOPbarData.length)
});
}
//
//
//
//
TOPbarChart(currentFruit, currentColr);
//
//
//
//
</script>
</body>
</html>
When all the data is positive everything is pretty much ok - but when some of the data is negative we can see the result in this plunker demo:
http://plnkr.co/edit/1hudJYkRq2MnuIlwxXZi?p=preview
How do I amend the code so that:
- the negative bars are shown?
- the base of the positive bars moves vertically up when negative numbers are included?
- the vertical movement is also included in the transition?
Above is more than 1 question but help on any would be appreciated.
The key is to play with the y and height attributes of the bars to position them correctly.
For y, change it to:
y: function(d) {
return yScale(Math.max(0, d.amount));
},
And for the height, change it to:
height: function(d) {
return Math.abs(yScale(d.amount) - yScale(0));
},
You can then style the negative bars to make them a different color.
Check the updated Plunkr - http://plnkr.co/edit/q7dQsPW0PiPuwFTy8gLN?p=preview
Edit:
For the coloring part, you can achieve it with a 1 liner if you want to reduce lines and want more simplicity.
Instead of:
fill: function(d) {
var col = colorChosen
if (d.amount < 0) {
col = "#FF0000";
}
return col;
},
});
You can do:
fill: function(d) {
return d.amount < 0 ? "#FF0000" : colorChosen;
},

Displaying SVG elements with D3 and d3.slider: Uncaught TypeError: Cannot read property 'length' of undefined

I'm trying to display some circles based on their geo location with a slider (per day). The data is saved in a vorfaelle.json file which is here and the HTML/d3 file looks like this.
<!DOCTYPE html>
<head>
<title>D3 Mapping Timeline</title>
<meta charset="utf-8">
<link rel="stylesheet" href="d3.slider.css" />
<style>
path {
fill: none;
stroke: #333;
stroke-width: .5px;
}
.land-boundary {
stroke-width: 1px;
}
.county-boundary {
stroke: #ddd;
}
.site {
stroke-width: .5px;
stroke: #333;
fill: #9cf;
}
#slider3 {
margin: 20px 0 10px 20px;
width: 900px;
}
</style>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="d3.slider.js"></script>
</head>
<body>
<div id="slider3"></div>
<script>
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var width = 1240,
height = 720;
var projection = d3.geo.mercator()
.translate([width / 2, height / 2])
.scale((width - 1) / 2 / Math.PI);
d3.json("vorfaelle.json", function(error, data){
console.log(data.features[1].geometry.coordinates);
window.site_data = data;
});
var displaySites = function(data) {
var sites = svg.selectAll(".site")
.data(data);
sites.enter().append("circle")
.attr("class", "site")
.attr("cx", function(d) {
for (var i = 0; i < d.features.length+1; i++) {
console.log(d.features[i].geometry.coordinates[0]);
return projection(d.features[i].geometry.coordinates[0])
//return projection([d.lng, d.lat])[0];
}
})
.attr("cy", function(d) {
for (var i = 0; i < d.features.length+1; i++) {
console.log(d.features[i].geometry.coordinates[1]);
return projection([d.features[i].geometry.coordinates[1]])
//return projection([d.lng, d.lat])[0];
}
})
.attr("r", 1)
.transition().duration(400)
.attr("r", 5);
sites.exit()
.transition().duration(200)
.attr("r",1)
.remove();
};
// var minDateUnix = moment('2014-07-01', "YYYY MM DD").unix();
// var maxDateUnix = moment('2015-07-21', "YYYY MM DD").unix();
var dateParser = d3.time.format("%d.%m.%Y").parse;
var minDate = dateParser("01.01.2015");
var maxDate = dateParser("31.12.2015");
console.log(minDate);
var secondsInDay = 60 * 60 * 24;
d3.select('#slider3').call(d3.slider()
.axis(true).min(minDate).max(maxDate).step(1)
.on("slide", function(evt, value) {
var newData = _(site_data).filter( function(site) {
for (var i = 0; i < site.features.length+1; i++) {
var date = dateParser(site.features[2].properties.date)
return date < value;
}
})
console.log("New set size ", newData.length);
displaySites(newData);
})
);
</script>
</body>
I am not sure if I am filtering the data properly at the end as I a was experimenting with this example and my data. When I move the slider I get this error:
For filtering you are do this which is wrong usage of filter as filter operates on an array.
var newData = _(site_data).filter( function(site) {
for (var i = 0; i < site.features.length+1; i++) {
var date = dateParser(site.features[2].properties.date)
return date < value;
}
})
You can do filtering like shown below:
d3.select('#slider3').call(d3.slider()
.axis(true).min(minDate).max(maxDate).step(1)
.on("slide", function(evt, value) {
newData = site_data.features.filter(function(d){
//convert the value to date
//convert the d.properties.date to date object using parser
return dateParser(d.properties.date) < new Date(value);
});
displaySites(newData);
})
);
Again in your code you doing a for loop to calculate the cx of the circle which is wrong:
.attr("cx", function(d) {
for (var i = 0; i < d.features.length+1; i++) {
console.log(d.features[i].geometry.coordinates[0]);
return projection(d.features[i].geometry.coordinates[0])
//return projection([d.lng, d.lat])[0];
}
})
There is no need for a for loop you should do like this:
.attr("cx", function(d) {
var p = projection(d.geometry.coordinates);
return p[0];
})
.attr("cy", function(d) {
var p = projection(d.geometry.coordinates);
return p[1]
})
Working code here
Hope this helps!

Categories