I am developing a d3 chart that updates every second.
After leaving the tab opened in the background for a while get the "Oh Snap" error on the browser.
I have tracked down the issue to the axis of the chart that dynamically updates depending on the current limits of the data I am receiving (Minimum an Maximum).
CHART FUNCTION:
function realtimeChart($chartContainer, width, height) {
// data arrays
this.dataMin = [];
this.dataMax = [];
// chart dimensions
this.width = width || 500;
this.height = height || 500;
//
this.globalX = 0;
this.duration = 100;
this.max = 50;
this.step = 1;
//
this.chart = d3.select($chartContainer.get(0))
.attr('width', this.width)
.attr('height', this.height + 25);
var x = d3.scaleLinear()
.domain([0, this.width])
.range([40, this.width]),
y = d3.scaleLinear()
.domain([0, 10])
.range([this.height - 20, 10]);
this.line = d3.line()
.x(function(d) {
return x(d.x);
})
.y(function(d) {
return y(d.y);
});
this.smoothLine = d3.line()
.curve(d3.curveCardinal)
.x(function(d) {
return x(d.x);
})
.y(function(d) {
return y(d.y);
});
this.lineArea = d3.area()
.x(function(d) {
return x(d.x);
})
.y0(y(0))
.y1(function(d) {
return y(d.y);
})
.curve(d3.curveCardinal);
// Draw the axis
this.xAxis = d3.axisBottom().scale(x);
this.yAxis = d3.axisLeft().scale(y);
this.axisX = this.chart.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + (this.height - 20) + ')')
.call(this.xAxis);
this.axisY = this.chart.append('g')
.attr('class', 'y axis')
.attr('transform', 'translate(40, 0)')
.call(this.yAxis);
// Append the holder for line chart and fill area
this.pathMin = this.chart.append('path');
this.pathMax = this.chart.append('path');
this.next = function(min, max) {
// Generate new data
var pointMin = {
x: this.globalX,
y: min || 0
},
pointMax = {
x: this.globalX,
y: max || cur
};
this.dataMin.push(pointMin);
this.dataMax.push(pointMax);
this.globalX += this.step;
// Shift the chart left
x.domain([this.globalX - (this.max - this.step) - 1, this.globalX - 1]);
y.domain([min - 2, max + 2]);
//THE PROBLEM IS SOLVED WHEN I COMMENT THE AXIS UPDATE
this.axisX.transition()
.duration(this.duration)
.ease(d3.easeLinear, 2)
.call(this.xAxis);
this.axisY.transition()
.duration(this.duration)
.ease(d3.easeLinear, 2)
.call(this.yAxis);
// Draw new line
this.pathMin.datum(this.dataMin)
.attr('class', 'smoothline min')
.attr('d', this.smoothLine);
this.pathMax.datum(this.dataMax)
.attr('class', 'smoothline max')
.attr('d', this.smoothLine);
// Remote old data (max 49 pointCurs)
if (this.dataCur.length > 49) {
this.dataMin.shift();
this.dataMax.shift();
}
}
}
HOW I AM INITIALIZING THE CHART:
var $chartDiv;
$chartDiv = $("<div>")
.addClass("timerRow")
.addClass("chartRow")
.append(function() {
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
return $(svg)
.attr({
"width": "500",
"height": "500",
"class": "chart"
});
});
$chartDiv.data({
"chart": new realtimeChart(
$chartDiv.find("svg").first(),
$("#dataDiv").innerWidth() - 40,
150)
})
HOW I UPDATE THE CHART AFTER RECIEVING NEW DATA:
chart = $chartRow.data("chart")
chart.next(timeData.min, timeData.max);
Does anyone know how to solve this issue without removing the axis?
Related
I try to do a beeswarm plot with different radius; inspired by this code
The issue I have, is that my point are offset regarding my x axis:
The point on the left should be at 31.7%. I don't understand why, so I would appreciate if you could guide me. This could be improved by changing the domain of x scale, but this can't match the exact value; same issue if I remove the d3.forceCollide()
Thank you,
Data are available here.
Here is my code:
$(document).ready(function () {
function tp(d) {
return d.properties.tp60;
}
function pop_mun(d) {
return d.properties.pop_mun;
}
var margin = {top: 20, right: 20, bottom: 20, left: 40},
width = 1280 - margin.right - margin.left,
height = 300 - margin.top - margin.bottom;
var svg = d3.select("body")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var z = d3.scaleThreshold()
.domain([.2, .3, .4, .5, .6, .7])
.range(["#35ff00", "#f1a340", "#fee0b6",
"#ff0000", "#998ec3", "#542788"]);
var loading = svg.append("text")
.attr("x", (width) / 2)
.attr("y", (height) / 2)
// .attr("dy", ".35em")
.style("text-anchor", "middle")
.text("Simulating. One moment please…");
var formatPercent = d3.format(".0%"),
formatNumber = d3.format(".0f");
d3.json('static/data/qp_full.json').then(function (data) {
features = data.features
//1 create scales
var x = d3.scaleLinear()
.domain([0, d3.max(features, tp)/100])
.range([0, width - margin.right])
var y = d3.scaleLinear().domain([0, 0.1]).range([margin.left, width - margin.right])
var r = d3.scaleSqrt().domain([0, d3.max(features, pop_mun)])
.range([0, 25]);
//2 create axis
var xAxis = d3.axisBottom(x).ticks(20)
.tickFormat(formatPercent);
svg.append("g")
.attr("class", "x axis")
.call(xAxis);
var nodes = features.map(function (node, index) {
return {
radius: r(node.properties.pop_mun),
color: '#ff7f0e',
x: x(node.properties.tp60 / 100),
y: height + Math.random(),
pop_mun: node.properties.pop_mun,
tp60: node.properties.tp60
};
});
function tick() {
for (i = 0; i < nodes.length; i++) {
var node = nodes[i];
node.cx = node.x;
node.cy = node.y;
}
}
setTimeout(renderGraph, 10);
function renderGraph() {
// Run the layout a fixed number of times.
// The ideal number of times scales with graph complexity.
// Of course, don't run too long—you'll hang the page!
const NUM_ITERATIONS = 1000;
var force = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody().strength(-3))
.force('center', d3.forceCenter(width / 2, height/2))
.force('x', d3.forceX(d => d.x))
.force('y', d3.forceY(d => d.y))
.force('collide', d3.forceCollide().radius(d => d.radius))
.on("tick", tick)
.stop();
force.tick(NUM_ITERATIONS);
force.stop();
svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => d.radius)
.style("fill", d => z(d.tp60/100))
.on("mouseover", function (d, i) {
d3.select(this).style('fill', "orange")
console.log(i.tp60,i)
svg.append("text")
.attr("id", "t")
.attr("x", function () {
return d.x - 50;
})
.attr("y", function () {
return d.y - 50;
})
.text(function () {
return [x.invert(i.x), i.tp60]; // Value of the text
})
})
.on("mouseout", function (d, i) {
d3.select("#t").remove(); // Remove text location
console.log(i)
d3.select(this).style('fill', z(i.tp60/100));
});
loading.remove();
}
})
})
I have a codepen here - https://codepen.io/anon/pen/xpaYYw?editors=0010
Its a simple test graph but the date will be formatted like this.
I have dates on the x axis and amounts on the y
How can I use the x scale to set the width and x position of the bars.
layers.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('height', function(d, i) {
return height - y(d.one);
})
.attr('y', function(d, i) {
return y(d.one);
})
.attr('width', function(d, i) {
return 50;
})
.attr('x', function(d, i) {
return 80*i;
})
.style('fill', (d, i) => {
return colors[i];
});
The problem with your question has nothing to do with programming, or JavaScript, or D3... the problem is a basic dataviz concept (that's why I added the data-visualization tag in your question):
What you're trying to do is not correct! You should not use bars with a time scale. Time scales are for time series (in which we use dots, or dots connected by lines).
If you use bars with time in the x axis you'll face problems:
Positioning the bar: the left margin of the bar will be always at the date you set. The whole bar will lie after that date;
Setting the width of the bar: in a real bar chart, which uses categorical variables for the x axis, the width has no meaning. But in a time scale the width represents time.
However, just for the sake of explanation, let's create this bar chart with a time scale (despite the fact that this is a wrong choice)... Here is how to do it:
First, set the "width" of the bars in time. Let's say, each bar will have 10 days of width:
.attr("width", function(d){
return x(d3.timeDay.offset(d.date, 10)) - x(d.date)
})
Then, set the x position of the bar to the current date less half its width (that is, less 5 days in our example):
.attr('x', function(d, i) {
return x(d3.timeDay.offset(d.date, -5));
})
Finally, don't forget to create a "padding" in the time scale:
var x = d3.scaleTime()
.domain([d3.min(data, function(d) {
return d3.timeDay.offset(d.date, -10);
}), d3.max(data, function(d) {
return d3.timeDay.offset(d.date, 10);
})])
.range([0, width]);
Here is your code with those changes:
var keys = [];
var legendKeys = [];
var maxVal = [];
var w = 800;
var h = 450;
var margin = {
top: 30,
bottom: 40,
left: 50,
right: 20,
};
var width = w - margin.left - margin.right;
var height = h - margin.top - margin.bottom;
var colors = ['#FF9A00', '#FFEBB6', '#FFC400', '#B4EDA0', '#FF4436'];
var data = [{
"one": 4306,
"two": 2465,
"three": 2299,
"four": 988,
"five": 554,
"six": 1841,
"date": "2015-05-31T00:00:00"
}, {
"one": 4378,
"two": 2457,
"three": 2348,
"four": 1021,
"five": 498,
"six": 1921,
"date": "2015-06-30T00:00:00"
}, {
"one": 3404,
"two": 2348,
"three": 1655,
"four": 809,
"five": 473,
"six": 1056,
"date": "2015-07-31T00:00:00"
},
];
data.forEach(function(d) {
d.date = new Date(d.date)
})
for (var i = 0; i < data.length; i++) {
for (var key in data[i]) {
if (!data.hasOwnProperty(key) && key !== "date")
maxVal.push(data[i][key]);
}
}
var x = d3.scaleTime()
.domain([d3.min(data, function(d) {
return d3.timeDay.offset(d.date, -10);
}), d3.max(data, function(d) {
return d3.timeDay.offset(d.date, 10);
})])
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, d3.max(maxVal, function(d) {
return d;
})])
.range([height, 0]);
var svg = d3.select('body').append('svg')
.attr('class', 'chart')
.attr('width', w)
.attr('height', h);
var chart = svg.append('g')
.classed('graph', true)
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var layersArea = chart.append('g')
.attr('class', 'layers');
var layers = layersArea.append('g')
.attr('class', 'layer');
layers.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('height', function(d, i) {
return height - y(d.one);
})
.attr('y', function(d, i) {
return y(d.one);
})
// .attr('width', function(d, i) {
// return 50;
// })
.attr("width", function(d) {
return x(d3.timeDay.offset(d.date, 10)) - x(d.date)
})
.attr('x', function(d, i) {
return x(d3.timeDay.offset(d.date, -5));
})
.style('fill', (d, i) => {
return colors[i];
});
chart.append('g')
.classed('x axis', true)
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x)
.tickFormat(d3.timeFormat("%Y-%m-%d")).tickValues(data.map(function(d) {
return new Date(d.date)
})));
chart.append('g')
.classed('y axis', true)
.call(d3.axisLeft(y)
.ticks(10));
<script src="https://d3js.org/d3.v4.min.js"></script>
I am trying to add a tooltip for my dual line chart graph.
However, instead of using timeScale or scaleLinear, I used scalePoint to graph my chart.
I am trying to achieve the following effect:
https://bl.ocks.org/mbostock/3902569
this.x = d3.scalePoint().range([ this.margin.left, this.width - this.margin.right ]);
this.xAxis = d3.axisBottom(this.x);
this.x.domain(
this.dataArray.map(d => {
return this.format(d[ 'year' ]);
}));
Here is my mouseover function,
function mousemove() {
//d3.mouse(this)[ 0 ]
//x.invert
var x0 = d3.mouse(this)[ 0 ],
i = bisectDate(data, x0, 1),
d0 = data[ i - 1 ],
d1 = data[ i ],
d = x0 - d0.year > d1.year - x0 ? d1 : d0;
console.log(x0);
// focus.attr("transform", "translate(" + x(format(d.year)) + "," + y(d.housing_index_change) + ")");
// focus.select("text").text(d.housing_index_change);
}
Since I am using scalePoint, there is obviously no invert function to map the coordinates to my data. and I am only retrieving the first element in the array and it is the only one that is being display regardless of the position of the mouse.
So my question is, how can I implement the invert functionality here while still using scalePoint?
Thank you :)
You are right, there is no invert for a point scale. But you can create your own function to get the corresponding domain of a given x position:
function scalePointPosition() {
var xPos = d3.mouse(this)[0];
var domain = xScale.domain();
var range = xScale.range();
var rangePoints = d3.range(range[0], range[1], xScale.step())
var yPos = domain[d3.bisect(rangePoints, xPos) -1];
console.log(yPos);
}
Step by step explanation
First, we get the x mouse position.
var xPos = d3.mouse(this)[0];
Then, based on your scale's range and domain...
var domain = xScale.domain();
var range = xScale.range();
...we create an array with all the steps in the point scale using d3.range:
var rangePoints = d3.range(range[0], range[1], xScale.step())
Finally, we get the corresponding domain using bisect:
var yPos = domain[d3.bisect(rangePoints, xPos) -1];
Check the console.log in this demo:
var data = [{
A: "groupA",
B: 10
}, {
A: "groupB",
B: 20
}, {
A: "groupC",
B: 30
}, {
A: "groupD",
B: 10
}, {
A: "groupE",
B: 17
}]
var width = 500,
height = 200;
var svg = d3.selectAll("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var color = d3.scaleOrdinal(d3.schemeCategory10)
.domain(data.map(function(d) {
return d.A
}));
var xScale = d3.scalePoint()
.domain(data.map(function(d) {
return d.A
}))
.range([50, width - 50])
.padding(0.5);
var yScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {
return d.B
}) * 1.1])
.range([height - 50, 10]);
var circles = svg.selectAll(".circles")
.data(data)
.enter()
.append("circle")
.attr("r", 8)
.attr("cx", function(d) {
return xScale(d.A)
})
.attr("cy", function(d) {
return yScale(d.B)
})
.attr("fill", function(d) {
return color(d.A)
});
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale);
svg.append("g").attr("transform", "translate(0,150)")
.attr("class", "xAxis")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(50,0)")
.attr("class", "yAxis")
.call(yAxis);
svg.append("rect")
.attr("opacity", 0)
.attr("x", 50)
.attr("width", width - 50)
.attr("height", height)
.on("mousemove", scalePointPosition);
function scalePointPosition() {
var xPos = d3.mouse(this)[0];
var domain = xScale.domain();
var range = xScale.range();
var rangePoints = d3.range(range[0], range[1], xScale.step())
var yPos = domain[d3.bisect(rangePoints, xPos) - 1];
console.log(yPos);
}
.as-console-wrapper { max-height: 20% !important;}
<script src="https://d3js.org/d3.v4.min.js"></script>
I'm trying to replace the lines in this Sankey migration map with tapered arcs, such as are used here, but I'm having trouble even getting the tapered arc shape to appear within the same div as the map.
How do I replace L.polyline with tapered arcs from d3.svg.area?
Here are the code changes I've made to my Sankey map (codepen link above), without success:
d3.json('http://codepen.io/laissezpasser/pen/RaeKLQ.js', function(error, links) {
if (error) throw error;
var weightMin = d3.min(links, function(l) { return l.value }),
weightMax = d3.max(links, function(l) { return l.value });
var weightScale = d3.scale.linear()
.domain([weightMin, weightMax])
.range([0, 300]);
links.forEach(function (link) {
if (link.source != link.target) {
var path = [ link.source, link.target ].join('-');
var pathReverse = [ link.target, link.source ].join('-')
var linkStyle = {
color: (linkLayers[pathReverse]) ? 'violet' : 'orange',
weight: weightScale(link.value),
smoothFactor: 1,
opacity: .5,
fill: false,
class: path
};
var linkCenter = L.area([ atollLayers[link.source].center, atollLayers[link.target].center ]).getBounds().getCenter();
if (linkStyle.color == 'violet') {
var lineCenter = L.latLng(
(linkCenter.lat * .001) + linkCenter.lat,
(linkCenter.lng * .001) + linkCenter.lng
);
}
else {
var lineCenter = L.latLng(
linkCenter.lat - (linkCenter.lat * .001),
linkCenter.lng - (linkCenter.lng * .001)
);
}
var link = L.area([ atollLayers[link.source].center, lineCenter, atollLayers[link.target].center ], linkStyle);
var svg = d3.select("#map").select("svg"),
g = svg.append("g");
var area = d3.svg.area()
.x(function(d) { return d.x; })
.x0(function(d) { return d.x0; })
.y0(function(d) { return d.y0; })
.y1(function(d) { return d.y1; })
.interpolate("cardinal")
.tension(0);
var link = {
width: weightScale(link.value),
offset: link.value / 2 + Math.random(),
};
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
var path = svg.append("path")
.datum(toArc(link))
.attr("class", "area")
.attr("d", area);
svg.append(atollLayers)
.attr("x", link.source.center[0])
.attr("y", link.source.center[1]);
svg.append(atollLayers)
.attr("x", link.target.center[0])
.attr("y", link.target.center[1]);
I'm building a set of bar charts that will be updated dynamically with json data.
There will be occasions where the x.domain value is equal to zero or null so in those cases I don't want to draw the rectangle, and would like the overall height of my chart to adjust. However, the bars are being drawn based on data.length which may contain 9 array values, but some of those values are zeros, but render a white space within the graph.
I've attached an image of what is happening. Basically there are 9 data entries and only one of those actually contains the positive value, but the bars are still being drawn for all 9 points.
Here is my code:
d3.json("data/sample.json", function(json) {
var data = json.cand;
var margin = {
top: 10,
right: 20,
bottom: 30,
left: 0
}, width = parseInt(d3.select('#graphic').style('width'), 10),
width = width - margin.left - margin.right -50,
bar_height = 55,
num_bars = (data.length),
bar_gap = 18,
height = ((bar_height + bar_gap) * num_bars);
var x = d3.scale.linear()
.range([0, width]);
var y = d3.scale.ordinal()
.range([height, 0]);
var svg = d3.select('#graphic').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'] + ')');
var dx = width;
var dy = (height / data.length) + 8;
// set y domain
x.domain([0, d3.max(data, function(d) {
return d.receipts;
})]);
y.domain(0, d3.max(data.length))
.rangeBands([0, data.length * bar_height ]);
var xAxis = d3.svg.axis().scale(x).ticks(2)
.tickFormat(function(d) {
return '$' + formatValue(d);
});
var yAxis = d3.svg.axis()
.scale(y)
.orient('left')
.ticks(0)
.tickFormat(function(d) {
return '';
});
// set height based on data
height = y.rangeExtent()[1] +12;
d3.select(svg.node().parentNode)
.style('height', (height + margin.top + margin.bottom) + 'px');
// bars
var bars = svg.selectAll(".bar")
.data (data, function (d) {
if (d.receipts >= 1) {
return ".bar";
} else {
return null;
}
})
.enter().append('g');
bars.append('rect')
.attr("x", function(d, i) {
return 0;
})
.attr("y", function(d, i) {
if (d.receipts >= 1) {
return i * (bar_height + bar_gap - 4);
}else {
return null;
}
})
.attr("width", function(d, i) {
if (d.receipts >= 1) {
return dx * d.receipts;
} else {
return 0;
}
});
//y and x axis
svg.append('g')
.attr('class', 'x axis bottom')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis.orient('bottom'));
svg.append('g')
.call(yAxis.orient('left'));
});