I have trouble updating the dots and axis of the scatter plot, and corresponding bar chart in the d3 visualization when I select a different attribute for the x-axis or y-axis. As shown in the image.
I reused the code from this example, made some changes to add a drop-down menu for both axis. When changing the axis from the drop-down it adds correct data points but won't remove the previous ones. I need help updating and removing the previous elements if the x-axis or y-axis are changed from the drop-down menu.
<!--
An example of linked views using model.js.
Curran Kelleher August 2014
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="styles.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
<!-- A functional reactive model library. github.com/curran/model -->
<script src="https://curran.github.io/model/cdn/model-v0.2.4.js"></script>
<style>
/* CSS for the visualization.
* Curran Kelleher 4/17/2014 */
/* Size the visualization container. */
#container {
position: fixed;
top: 30px;
bottom: 30px;
left: 30px;
right: 30px;
}
/* Style the visualization.
* This CSS is copied verbatim from the
* D3 scatter plot example found at
* http://bl.ocks.org/mbostock/3887118 */
body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.dot {
fill: black;
}
/* The following CSS is for brushing,
* adapted from http://bl.ocks.org/mbostock/4343214 */
.dot.selected {
fill: red;
}
.brush .extent {
stroke: gray;
fill-opacity: .125;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<div id="container"></div>
<div id = "axisSelectors">
<b>x-axis:</b>
<select id="xSelector" onchange="onCategoryChanged()">
<option value="sepalWidth">sepalWidth</option>
<option value="petalWidth">petalWidth</option>
</select>
<b>y-axis:</b>
<select id="ySelector" onchange="onCategoryChanged()">
<option value="sepalLength">sepalLength</option>
<option value="petalLength">petalLength</option>
</select>
</div>
<script>
function onCategoryChanged() {
var select = d3.select('#xSelector').node();
// Get current value of select element
var x_category = select.options[select.selectedIndex].value;
// Update chart with the selected category of letters
var select = d3.select('#ySelector').node();
var y_category = select.options[select.selectedIndex].value;
main(x_category,y_category);
}
function BarChart (container) {
var defaults = {
margin: {
top: 20,
right: 20,
bottom: 30,
left: 40
},
yAxisNumTicks: 10,
yAxisTickFormat: ""
},
model = Model(defaults),
xAxis = d3.svg.axis().orient("bottom"),
yAxis = d3.svg.axis().orient("left")
svg = d3.select(container).append('svg')
// Use absolute positioning on the SVG element
// so that CSS can be used to position the div later
// according to the model `box.x` and `box.y` properties.
.style('position', 'absolute'),
g = svg.append("g"),
xAxisG = g.append("g").attr("class", "x axis"),
yAxisG = g.append("g").attr("class", "y axis"),
yAxisText = yAxisG.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end");
// Encapsulate D3 Conventional Margins.
// See also http://bl.ocks.org/mbostock/3019563
model.when(["box", "margin"], function (box, margin) {
model.width = box.width - margin.left - margin.right,
model.height = box.height - margin.top - margin.bottom;
});
model.when("margin", function (margin) {
g.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
});
// Adjust Y axis tick mark parameters.
// See https://github.com/mbostock/d3/wiki/Quantitative-Scales#linear_tickFormat
model.when(['yAxisNumTicks', 'yAxisTickFormat'], function (count, format) {
yAxis.ticks(count, format);
});
// Respond to changes in size and offset.
model.when("box", function (box) {
// Resize the svg element that contains the visualization.
svg.attr("width", box.width).attr("height", box.height);
// Set the CSS `left` and `top` properties
// to move the SVG element to `(box.x, box.y)`
// relative to the container div to apply the offset.
svg
.style('left', box.x + 'px')
.style('top', box.y + 'px');
});
model.when("height", function (height) {
xAxisG.attr("transform", "translate(0," + height + ")");
});
model.when(["data", "xAttribute", "width"], function (data, xAttribute, width) {
model.xScale = d3.scale.ordinal()
.rangeRoundBands([0, width], .1)
.domain(data.map(function(d) { return d[xAttribute]; }));
});
model.when(["data", "yAttribute", "height"], function (data, yAttribute, height) {
model.yScale = d3.scale.linear()
.range([height, 0])
.domain([0, d3.max(data, function(d) { return d[yAttribute]; })]);
});
model.when(["xScale"], function (xScale) {
xAxis.scale(xScale)
xAxisG.call(xAxis);
});
model.when(["yScale"], function (yScale) {
yAxis.scale(yScale)
yAxisG.call(yAxis);
});
model.when("yAxisLabel", yAxisText.text, yAxisText);
model.when(["data", "xAttribute", "yAttribute", "xScale", "yScale", "height"],
function (data, xAttribute, yAttribute, xScale, yScale, height) {
var bars = g.selectAll(".bar").data(data);
bars.enter().append("rect").attr("class", "bar");
bars
.attr("x", function(d) { return xScale(d[xAttribute]); })
.attr("width", xScale.rangeBand())
.attr("y", function(d) { return yScale(d[yAttribute]); })
.attr("height", function(d) { return height - yScale(d[yAttribute]); });
bars.exit().remove();
});
return model;
}
// An adaptation of the [D3 scatter plot example](http://bl.ocks.org/mbostock/3887118)
// that uses `model.js`. This version, unlike the original example,
// is model driven and reactive. When a part of the model updates,
// only the parts of the visualization that depend on those parts
// of the model are updated. There are no redundant calls to visualization
// update code when multiple properties are changed simultaneously.
//
// Draws from this brushing example for interaction:
// http://bl.ocks.org/mbostock/4343214
//
// See also docs on quadtree:
// https://github.com/mbostock/d3/wiki/Quadtree-Geom
//
// Define the AMD module using the `define()` function
// provided by Require.js.
//define(['d3', 'model'], function (d3, Model) {
function ScatterPlot (div){
var x = d3.scale.linear(),
y = d3.scale.linear(),
xAxis = d3.svg.axis().scale(x).orient('bottom'),
yAxis = d3.svg.axis().scale(y).orient('left'),
// Use absolute positioning so that CSS can be used
// to position the div according to the model.
svg = d3.select(div).append('svg').style('position', 'absolute'),
g = svg.append('g'),
xAxisG = g.append('g').attr('class', 'x axis'),
yAxisG = g.append('g').attr('class', 'y axis'),
xAxisLabel = xAxisG.append('text')
.attr('class', 'label')
.attr('y', -6)
.style('text-anchor', 'end'),
yAxisLabel = yAxisG.append('text')
.attr('class', 'label')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end'),
// Add the dots group before the brush group,
// so that mouse events go to the brush
// rather than to the dots, even when the mouse is
// on top of a dot.
dotsG = g.append('g'),
brushG = g.append('g')
.attr('class', 'brush'),
brush = d3.svg.brush()
.on('brush', brushed),
dots,
quadtree,
model = Model();
model.when('xLabel', xAxisLabel.text, xAxisLabel);
model.when('yLabel', yAxisLabel.text, yAxisLabel);
model.when('margin', function (margin) {
g.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
});
model.when('box', function (box) {
svg.attr('width', box.width)
.attr('height', box.height);
// Set the CSS `left` and `top` properties
// to move the SVG element to `(box.x, box.y)`
// relative to the container div.
svg
.style('left', box.x + 'px')
.style('top', box.y + 'px')
});
model.when(['box', 'margin'], function (box, margin) {
model.width = box.width - margin.left - margin.right;
model.height = box.height - margin.top - margin.bottom;
});
model.when('width', function (width) {
xAxisLabel.attr('x', width);
});
model.when('height', function (height) {
xAxisG.attr('transform', 'translate(0,' + height + ')');
});
model.when(['width', 'height'], function (width, height) {
brush.x(d3.scale.identity().domain([0, width]));
brush.y(d3.scale.identity().domain([0, height]));
brushG
.call(brush)
.call(brush.event);
});
model.when(['width', 'height', 'data', 'xField', 'yField'], function (width, height, data, xField, yField) {
// Updated the scales
x.domain(d3.extent(data, function(d) { return d[xField]; })).nice();
y.domain(d3.extent(data, function(d) { return d[yField]; })).nice();
x.range([0, width]);
y.range([height, 0]);
// update the quadtree
quadtree = d3.geom.quadtree()
.x(function(d) { return x(d[xField]); })
.y(function(d) { return y(d[yField]); })
(data);
// update the axes
xAxisG.call(xAxis);
yAxisG.call(yAxis);
// Plot the data as dots
dots = dotsG.selectAll('.dot').data(data);
dots.enter().append('circle')
.attr('class', 'dot')
.attr('r', 3.5);
dots
.attr('cx', function(d) { return x(d[xField]); })
.attr('cy', function(d) { return y(d[yField]); });
dots.exit().remove();
});
return model;
function brushed() {
var e = brush.extent(), selectedData;
if(dots) {
dots.each(function(d) { d.selected = false; });
selectedData = search(e[0][0], e[0][1], e[1][0], e[1][1]);
dots.classed('selected', function(d) { return d.selected; });
}
model.selectedData = brush.empty() ? model.data : selectedData;
}
// Find the nodes within the specified rectangle.
function search(x0, y0, x3, y3) {
var selectedData = [];
quadtree.visit(function(node, x1, y1, x2, y2) {
var d = node.point, x, y;
if (d) {
x = node.x;
y = node.y;
d.visited = true;
if(x >= x0 && x < x3 && y >= y0 && y < y3){
d.selected = true;
selectedData.push(d);
}
}
return x1 >= x3 || y1 >= y3 || x2 < x0 || y2 < y0;
});
return selectedData;
}
}
// The main program that assembles the linked views.
//
// Curran Kelleher 4/17/2014
//require(['d3', 'scatterPlot', 'barChart'], function (d3, ScatterPlot, BarChart) {
function main(xvar,yvar){
// Grab the container div from the DOM.
var div = document.getElementById('container'),
// Add both visualizations to the same div.
// Each will create its own SVG element.
scatterPlot = ScatterPlot(div),
barChart = BarChart(div);
// Configure the scatter plot to use the iris data.
scatterPlot.set({
xField: xvar,
yField: yvar,
xLabel: xvar,
yLabel: yvar,
margin: { 'top': 20, 'right': 20, 'bottom': 30, 'left': 40 }
});
// Configure the bar chart to use the aggregated iris data.
barChart.set({
xAttribute: 'species',
yAttribute: 'count',
yAxisLabel: 'number of irises',
margin: { 'top': 20, 'right': 20, 'bottom': 30, 'left': 40 }
});
// Compute the aggregated iris data in response to brushing
// in the scatter plot, and pass it into the bar chart.
scatterPlot.when('selectedData', function (scatterData) {
var speciesCounts = {};
// Aggregate scatter plot data by counting
// the number of irises for each species.
scatterData.forEach(function (d) {
if(!speciesCounts[d.species]){
speciesCounts[d.species] = 0;
}
speciesCounts[d.species]++;
});
// Flatten the object containing species counts into an array.
// Update the bar chart with the aggregated data.
barChart.data = Object.keys(speciesCounts).map(function (species) {
return {
species: species,
count: speciesCounts[species]
};
});
});
// Load the iris data.
d3.tsv('data.tsv', function (d) {
d.sepalLength = +d.sepalLength;
d.sepalWidth = +d.sepalWidth;
d.petalLength = +d.petalLength;
d.petalWidth = +d.petalWidth;
return d;
}, function(error, data) {
// Set sizes once to initialize.
setSizes();
// Set sizes when the user resizes the browser.
window.addEventListener('resize', setSizes);
// Set the data.
scatterPlot.data = data;
});
// Sets the `box` property on each visualization
// to arrange them within the container div.
function setSizes(){
// Put the scatter plot on the left.
scatterPlot.box = {
x: 0,
y: 0,
width: div.clientWidth / 2,
height: div.clientHeight
};
// Put the bar chart on the right.
barChart.box = {
x: div.clientWidth / 2,
y: 0,
width: div.clientWidth / 2,
height: div.clientHeight
};
}
}
main('sepalWidth','sepalLength');
</script>
</body>
</html>
Related
Update:
The zooming stream graph question is resolved. Thank you, Andrew! But the zooming of the streamgraph doesn't align with the zooming of the X and Y axis.
Thisis the picture showing how it looks like now2
the original post is here:
I am new to StackOverflow and the javascript community. I am trying to zoom a streamgraph I did use javascript and D3 and I followed this tutorial: https://www.d3-graph-gallery.com/graph/interactivity_zoom.html#axisZoom.
My code can be viewed here: https://github.com/Feisnowflakes/zoomtest222/tree/main/streamgraph-test
However, currently, I can zoom X aXis and Y aXis, but not my stream graph is zoomable. I didn't see any errors in my console, so I am stuck now. Can anyone like to look at my codes and help me figure out why my streamgraph cannot be zoomed?
Thank you so much!
(function () {
// first, load the dataset from a CSV file
d3.csv("https://raw.githubusercontent.com/Feisnowflakes/zoomtest222/main/streamgraph-test/Los_Angeles_International_Airport_-_Passenger_Traffic_By_Terminal.csv")
.then(data => {
// log csv in browser console
console.log(data);
var advanceVisData = {};
var airport = new Set();
data.forEach(d => {
airport.add(d['Terminal']);
var period = new Date(d['ReportPeriod']);
if (period in advanceVisData) {
if (d['Terminal'] in advanceVisData[period]) {
advanceVisData[period][d['Terminal']] += Number(d['Passenger_Count']);
}
else {
advanceVisData[period][d['Terminal']] = Number(d['Passenger_Count']);
}
}
else {
advanceVisData[period] = {};
advanceVisData[period][d['Terminal']] = Number(d['Passenger_Count']);
}
});
console.log(airport);
console.log(advanceVisData);
// reformat the advanceVisData for d3.stack()
var formattedData = [];
Object.keys(advanceVisData).forEach(d => {
var item = {};
item['year'] = d;
airport.forEach(terminal => {
if (terminal in advanceVisData[d]) {
item[terminal] = advanceVisData[d][terminal];
} else {
item[terminal] = 0;
}
});
formattedData.push(item);
});
console.log(formattedData);
/*********************************
* Visualization codes start here
* ********************************/
var width = 1200;
var height = 400;
var margin = { left: 60, right: 20, top: 20, bottom: 60 };
//set the dimensions and margins of the graph
// append the svg object to the body of the page
var svg = d3.select('#container')
.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 + ")");
// List of groups = header of the csv files
var keys = Array.from(airport);
//stack the data?
var stackedData = d3.stack()
//.offset(d3.stackOffsetSilhouette)
.keys(keys)
(formattedData);
console.log(stackedData);
var max_val = 0;
var min_val = 0;
stackedData.forEach(terminal => {
terminal.forEach(year => {
if (year[0] < min_val) min_val = year[0];
if (year[1] < min_val) min_val = year[1];
if (year[0] > max_val) max_val = year[0];
if (year[1] > max_val) max_val = year[1];
})
});
//console.log(max_val, min_val);
// Add X axis
var x = d3.scaleTime()
.domain(d3.extent(formattedData, function (d) {
return new Date(d.year);
}))
.range([0, width]);
var xAxis = svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).ticks(20));
// Add Y axis
var y = d3.scaleLinear()
.domain([min_val, max_val])
.range([height, 0]);
var yAxis = svg.append("g")
.call(d3.axisLeft(y));
// color palette
var color = d3.scaleOrdinal()
.domain(keys)
.range(['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00', '#f781bf', "#87sbf", "#ff981bf","#d6a3b6", '#b3afb0', '#ddd8c2']);
// create a tooltip
var Tooltip = svg
.append("text")
.attr("x", 0)
.attr("y", 0)
.style("opacity", 0)
.style("font-size", 17)
// Show the areas
var stream = svg.append("g")
stream
.selectAll(".myStreamArea")
.data(stackedData)
.enter()
.append("path")
.attr("class", "myStreamArea")
.style("fill", function (d) {
return color(d.key);
})
.style("opacity", 1)
.attr("d", d3.area()
.x(function (d) {
return x(new Date(d.data.year));
})
.y0(function (d) {
return y(d[0]);
})
.y1(function (d) {
return y(d[1]);
})
);
// Set the zoom and Pan features: how much you can zoom, on which part, and what to do when there is a zoom
var zoom = d3.zoom()
.scaleExtent([.5, 20]) // This control how much you can unzoom (x0.5) and zoom (x20)
.extent([[0, 0], [width, height]])
.on("zoom", updateChart);
// This add an invisible rect on top of the chart area. This rect can recover pointer events: necessary to understand when the user zoom
svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "none")
.style("pointer-events", "all")
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.call(zoom);
// now the user can zoom and it will trigger the function called updateChart
// A function that updates the chart when the user zoom and thus new boundaries are available
function updateChart() {
// recover the new scale
var transform = d3.zoomTransform(this);
var newX = transform.rescaleX(x);
var newY = transform.rescaleY(y);
// var newX = d3.event.transform.rescaleX(x);
// var newY = d3.event.transform.rescaleY(y);
// update axes with these new boundaries
xAxis.call(d3.axisBottom(newX))
yAxis.call(d3.axisLeft(newY))
stream
.selectAll(".myStreamArea")
.attr("d", d3.area()
.x(function (d) {
return newX(new Date(d.data.year));
})
.y0(function (d) {
return newY(d[0]);
})
.y1(function (d) {
return newY(d[1]);
}));
}
})
})();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Zoomable streamgraph</title>
<script src="https://d3js.org/d3.v6.min.js"></script>
<style>
#tooltip {
min-width: 100px;
min-height: 50px;
background-color: white;
}
</style>
</head>
<body>
<div id="container"> <div id="tooltip"></div></div>
<script src="main.js"></script>
</body>
</html>
I'm using d3js to move circular dots from right to left with respect to current time. I have a couple of issues:
1. .exit().remove() don't work. Node doesn't get removed once it goes out of the view.
2. Transition of circles are a bit jumpy
var circles = g.selectAll('path')
circles.data(data)
.enter()
.append('path')
.attr("d", symbol.type(d3.symbolCircle))
.merge(circles)
.attr("transform", (d) => "translate(" + x(d.x) + "," + y(d.y) + ")");
circles.exit().remove();
You can see my full code here: http://jsfiddle.net/hsdhott/3tdhuLgm/
Besides your enter-exit-update pattern not being correct (please check the snippet bellow), the big problem here is the data:
selection.exit() method won't magically select — normally for using remove() later — an element based on any arbitrary criterion, such as "getting out of the view". It's based on the data only. And the problem is that your data never stops increasing:
if (count % 10 === 0) {
var point = {
x: globalX,
y: ((Math.random() * 200 + 50) >> 0)
};
data.push(point);
}
So, a very quick solution in this case is removing the data points based on your x domain:
data = data.filter(function(d) {
return d.x > globalX - 10000;
});
This is just a suggestion, use the logic you want for removing the objects from the data array. However, regardless the logic you use, you have to remove them.
Regarding the jumpy transition, the problem is that you're using selection.transition and setInterval. That won't work, chose one of them.
Here is your updated code:
var data = [];
var width = 500;
var height = 350;
var globalX = new Date().getTime();
/* var globalX = 0; */
var duration = 250;
var step = 10;
var count = 0;
var chart = d3.select('#chart')
.attr('width', width + 50)
.attr('height', height + 50);
var x = d3.scaleTime()
.domain([globalX, (globalX - 10000)])
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, 300])
.range([300, 0]);
// -----------------------------------
// Draw the axis
var xAxis = d3.axisBottom()
.scale(x)
.ticks(10)
.tickFormat(formatter);
var axisX = chart.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, 300)')
.call(xAxis);
// Append the holder for line chart and circles
var g = chart.append('g');
function formatter(time) {
if ((time.getSeconds() % 5) != 0) {
return "";
}
return d3.timeFormat('%H:%M:%S')(time);
}
function createData() {
// Generate new data
var point = {
x: globalX,
y: ((Math.random() * 200 + 50) >> 0)
};
data.push(point);
}
function callInterval() {
count++;
if (count % 3 === 0) createData();
}
// Main loop
function tick() {
// Generate new data
if (count % 10 === 0) {
var point = {
x: globalX,
y: ((Math.random() * 200 + 50) >> 0)
};
data.push(point);
}
data = data.filter(function(d) {
return d.x > globalX - 10000;
});
count++;
globalX = new Date().getTime();
var timer = new Date().getTime();
var symbol = d3.symbol().size([100]),
color = d3.schemeCategory10;
var circles = g.selectAll('path')
.data(data);
circles = circles.enter()
.append('path')
.attr("d", symbol.type(d3.symbolCircle))
.merge(circles)
.attr("transform", (d) => "translate(" + x(d.x) + "," + y(d.y) + ")");
circles.exit().remove();
// Shift the chart left
x.domain([timer - 10000, timer]);
axisX.call(xAxis);
g.attr('transform', null)
.attr('transform', 'translate(' + x(globalX - 10000) + ')');
// Remote old data (max 50 points)
if (data.length && (data[data.length - 1].x < (globalX - 10000))) data.shift();
}
tick();
setInterval(tick, 10);
.axis {
font-family: sans-serif;
font-size: 12px;
}
.line {
fill: none;
stroke: #f1c40f;
stroke-width: 3px;
}
.circle {
stroke: #e74c3c;
stroke-width: 3px;
fill: #FFF;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg id="chart"></svg>
I'm trying to get my data to show up in my graph, however I get an error that says that my data is "NaN" after I converted the Year and Miles column from strings to integers.
I'm guessing that it's something with my x_scale & y_scale...?
<!DOCTYPE html>
<html lang="en">
<head>
<description>
<!--charts - avg vehicle trip per mile, source: http://nhts.ornl.gov/2009/pub/stt.pdf-->
</description>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<link rel="stylesheet" href="/bootstrap-3.3.7-dist/css/bootstrap.min.css">
<script type="text/javascript" src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script type="text/javascript">
// global variables
var dataset;
d3.csv("avgVehicleTripLengthMiles.csv", function (error, data) {
if (error) {
console.log(error);
} else {
console.log(data);
}
// once loaded, data is copied to dataset because js is asynchronous
dataset = data;
createScatterplot();
});
/*
function typeConv() {
// type conversion from string to integer
var typeConv = dataset.forEach(function (d) {
d["Year"] = +d["Year"];
d["Miles"] = +d["Miles"];
return d;
});
}
*/
function createScatterplot() {
// TEST
var typeConv = dataset.forEach(function (d) {
d["Year"] = +d["Year"];
d["Miles"] = +d["Miles"];
return d;
});
var title = d3.select("body")
.append("h4")
.text("Avg. Vehicle Trip Length per Mile");
// dimensions of canvas
var padding = 30;
var margin = {
top: 20,
right: 40,
bottom: 20,
left: 40
},
w = 800 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
// create svg canvas
var svg_canvas = d3.select("body")
.append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom);
// create scale for axis
var x_scale = d3.scaleLinear().domain([1969, 2009]).range([padding, w - padding * 2]);
var y_scale =
d3.scaleLinear().domain([0, 20]).range([h - padding, padding]);
// r_scale created specifically for circles' radius to be mapped unto axes
var r_scale =
d3.scaleLinear().domain([0, d3.max(dataset, function (d) {
return d[1];
})]).range([0, 20]);
// define axis & ticks // .ticks(5) to x_axis and .ticks(1) to y_axis
var x_axis = d3.axisBottom().scale(x_scale);
var y_axis = d3.axisLeft().scale(y_scale);
// create group, "g" element, to create x_axis and y_axis
var x_group = svg_canvas.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(x_axis);
var y_group = svg_canvas.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + padding + ",0)")
.call(y_axis);
// create circles
svg_canvas.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function (d) {
return x_scale(d[0]);
})
.attr("cy", function (d) {
console.log(d); // TEST
return y_scale(d[1]);
})
.attr("cr", function (d) {
return r_scale(d[1]);
});
}
</script>
</body>
</html>
EDIT (new answer):
There are several issues, and I'll try to step through them one by one.
In order to test, I had to make up my own data. My test CSV file looked like this (so your final answer might change slightly if your file is different)
Year,Miles
2006,5.0
2007,7.2
2008,19.3
As was pointed out by #altocumulus in the comments above, your .attr() calls are referencing non-existant indexes, which might be part of the trouble.
The radius attribute for circles is r, not cr
I simplified the code by not calling a function for r, but rather doing a static value. You may want to play further with this.
The significantly changed portion of code is
svg_canvas.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function (d) {
return x_scale(d["Year"]);
})
.attr("cy", function (d) {
return y_scale(d["Miles"]);
})
.attr("r", function (d) {
return 5;
//return r_scale(d[1]);
});
You still have an issue with the x-axis acting like numbers, and not dates, making 2006 look like 2,006, for example. I haven't solved that issue here.
Lastly, I feel like you're complicating your code for no reason by trying to handle margin AND padding via the D3 code, when these end up meaning similar things in the Javascript context. I suggest accomplishing most of the margin/padding via CSS, which would simplify your code. Another example of an unnecessary complication is in the previous answer, below.
FIDDLE
OLD (incomplete, incorrect) ANSWER:
The return value of Array.forEach() is undefined, so it can't be assigned.
dataset.forEach(function (d) {
//d["Year"] = +d["Year"];
d["Miles"] = +d["Miles"];
// NOT NEEDED: return d;
});
If you need to keep your converted array separate, use Array.map().
// global variables
var dataset;
d3.csv("avgVehicleTripLengthMiles.csv", function (error, data) {
if (error) {
console.log(error);
} else {
console.log(data);
}
// once loaded, data is copied to dataset because js is asynchronous
dataset = data;
createScatterplot();
});
function createScatterplot() {
// TEST
var typeConv = dataset.forEach(function (d) {
// d["Year"] = +d["Year"];
d["Miles"] = +d["Miles"];
// return d;
});
var title = d3.select("body")
.append("h4")
.text("Avg. Vehicle Trip Length per Mile");
// dimensions of canvas
var padding = 30;
var margin = {
top: 20,
right: 40,
bottom: 20,
left: 40
},
w = 800 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
// create svg canvas
var svg_canvas = d3.select("body")
.append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom);
// create scale for axis
var x_scale = d3.scaleLinear().domain([1965, 2009]).range([padding, w - padding * 2]);
var y_scale =
d3.scaleLinear().domain([0, 20]).range([h - padding, padding]);
// r_scale created specifically for circles' radius to be mapped unto axes
var r_scale =
d3.scaleLinear().domain([0, d3.max(dataset, function (d) {
return d[1];
})]).range([0, 20]);
// define axis & ticks // .ticks(5) to x_axis and .ticks(1) to y_axis
var x_axis = d3.axisBottom().scale(x_scale);
var y_axis = d3.axisLeft().scale(y_scale);
// create group, "g" element, to create x_axis and y_axis
var x_group = svg_canvas.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(x_axis);
var y_group = svg_canvas.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + padding + ",0)")
.call(y_axis);
// create & color circles
svg_canvas.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function (d) {
return x_scale(d["Year"]);
})
.attr("cy", function (d) {
return y_scale(d["Miles"]);
})
.attr("r", function (d) {
return 5;
})
.style("fill", function (d) {
if (d["Trip Purpose"] === "All Purposes") {
return "pink";
} else if (d["Trip Purpose"] === "To or From Work") {
return "red";
} else if (d["Trip Purpose"] === "Shopping") {
return "blue";
} else if (d["Trip Purpose"] === "Other Family/Personal Errands") {
return "green";
} else if (d["Trip Purpose"] === "Social and Recreational") {
return "gray";
};
});
// create text label for x-axis
svg_canvas.append("text")
.attr("x", w / 2)
.attr("y", h + margin.top + 20)
.style("text-anchor", "middle")
.text("Year");
// create text label for y-axis
svg_canvas.append("text")
.attr("transform", "rotate(-90)")
.attr("x", (0 - margin.left / 2))
.attr("y", (h/2))
.style("text-anchor", "middle")
.text("Miles");
I'm trying to get mouse events to cooperate between different elements in a scatterplot. D3's brush component adds some listeners to the called element (e.g. svg.call(brush)). I also want to display points bound on the SVG, much like a scatterplot, and for those points to support mouseover events (for tooltips and other interactions).
A previous solution suggests drawing points before calling the brush, which supports mouseover on points while allowing the brush to be drawn and the extent modified. However, if the dragging motion for the brush starts upon a point (which I anticipate in very dense graphs), the brush component misbehaves when an extent is already active (translating the brush resizes the extent instead). You can try it out on this example, where the above suggested solution has been implemented.
I've narrowed the issue to how the event is handled in d3's brushstart() function, internal to the d3.svg.brush component. Here's what relevant variables look like when the brush is correctly working.
this eventTarget dragging resizing
-------------- ------------------------------------- ---------- ----------
Translating extent brush parent rect.extent true 0
Resizing extent brush parent rect (invisible rects for resizing) false e.g. "e"
Redrawing brush parent rect.background false 0
This is what it looks like currently, with the solution above:
this eventTarget dragging resizing
-------------------- -------------- ------------- ---------- ----------------
Translating extent brush parent circle false circle.datum()
Resizing extent brush parent circle false circle.datum()
Redrawing brush parent circle false circle.datum()
The real question is: how can I fudge the source of d3.event.target to match the first table? If I can do that, I can get the behavior I want. Thanks for any help!
If you've missed it, here's a bl.ock of this conundrum in action: http://bl.ocks.org/yelper/d38ddf461a0175ebd927946d15140947
Here's a quick hack which corrects the behavior:
.on('mousedown', function(d){
var e = brush.extent(),
m = d3.mouse(svg.node()), // pointer position with respect to g
p = [x.invert(m[0]), y.invert(m[1])]; // position in user space
if ( brush.empty() || // if there is no brush
(e[0][0] > d[0] || d[0] > e[1][0]
|| e[0][1] > d[1] || d[1] > e[1][1] ) // or our current circle is outside the bounds of the brush
) {
brush.extent([p,p]); // set brush to current position
} else {
d3.select(this).classed('extent', true); // else we are moving the brush, so fool d3 (I got this from looking at source code, it's how d3 determines a drag)
}
});
Working code below, updated block here.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.hidden {
opacity: 0.3;
}
.extent {
fill: #000;
fill-opacity: .125;
stroke: #fff;
}
</style>
<body>
<script src="//d3js.org/d3.v3.js"></script>
<script>
var margin = {top: 20, right: 50, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 350 - margin.top - margin.bottom;
var x = d3.scale.linear()
.range([0, width])
.domain([0, 10]);
var y = d3.scale.linear()
.range([height, 0])
.domain([0, 10]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var curPt = d3.select('body')
.append('p')
.html("Current point: ")
.append('span')
.attr('id', 'curPt');
var svg = d3.select('body').insert('svg', 'p')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,'+height+')')
.call(xAxis);
svg.append('g')
.attr('class', 'y axis')
.call(yAxis);
var brush = d3.svg.brush()
.x(x)
.y(y)
.on("brush", function() {
var e = brush.extent(),
c = svg.selectAll('circle');
c.classed('extent', false);
c.classed('hidden', function(d) {
return e[0][0] > d[0] || d[0] > e[1][0]
|| e[0][1] > d[1] || d[1] > e[1][1];
}
);
})
.on("brushend", function() {
if (brush.empty()) svg.selectAll('circle').classed('hidden', false);
});
svg.call(brush);
var data = d3.range(50).map(function() { return [Math.random() * 10, Math.random() * 10]; });
svg.append('g')
.attr('class', 'points')
.selectAll('circle')
.data(data).enter()
.append('circle')
.attr('cx', function(d) { return x(d[0]); })
.attr('cy', function(d) { return y(d[1]); })
.attr('r', 10)
.style('fill', 'steelblue')
.on('mouseover', function(d) {
curPt.html("[" + d[0] + ", " + d[1] + "]");
})
.on('mouseout', function(d) {
curPt.html("");
})
.on('mousedown', function(d){
var e = brush.extent(),
m = d3.mouse(svg.node()),
p = [x.invert(m[0]), y.invert(m[1])];
if ( brush.empty() ||
(e[0][0] > d[0] || d[0] > e[1][0]
|| e[0][1] > d[1] || d[1] > e[1][1] )
) {
brush.extent([p,p]);
} else {
d3.select(this).classed('extent', true);
}
});
</script>
Here is the working example:
https://jsfiddle.net/paradite/rpqusqdc/2/
Basically I used my previously coded range selection tool using drag event instead of brush: http://bl.ocks.org/paradite/71869a0f30592ade5246
It does not interfere with your circles. So you just need to get the current rect dimensions and update your circles accordingly:
// select your points here
var e = selectionRect.getCurrentAttributes();
svg.selectAll('circle').classed('hidden', function(d) {
return e.x1 > x(d[0]) || x(d[0]) > e.x2 || e.y1 > y(d[1]) || y(d[1]) > e.y2;
});
Of course you can remove parts of its logic as much of it is not necessary for your case.
I have made a bar chart based on revenue(x axis) and country(y axis) basis.My bar chart is done but I want to select multiple values of country from a dropdown and according to those values the bars of only that country should be shown others hide...For single selection i have done but for multiple values of country how to filter and show in D3 Js bar chart I am stuck.
The localDataJson contains the data:
localdatajson=[
{"Country";"USA","Revenue":"12","TurnOver":"16"},
{"Country";"Brazil","Revenue":"4.5","TurnOver":"16"},
{"Country";"Belzium","Revenue":"4.8","TurnOver":"16"},
{"Country";"Britain","Revenue":"20","TurnOver":"16"},
{"Country";"Canada","Revenue":"6.5","TurnOver":"16"},
{"Country";"DenMark","Revenue":"7.5","TurnOver":"16"}
]
text parameter would be an array in case of multiple selection like
for eg. text=["USA","Brazil","Britain"]
I want to show bars only for these three countries...
Here is my code
function revenueBar(localDataJson, text) {
var w = 400;
var h = 400;
var barPadding = 1;
var maxRevenue = 0;
var maxTurnOver = 0;
var padding = {
left: 45, right: 10,
top: 40, bottom: 60
}
var maxWidth = w - padding.left - padding.right;
var maxHeight = h - padding.top - padding.bottom;
for (var j = 0; j < localDataJson.length; j++) {
if (localDataJson[j].Revenue > maxRevenue) {
maxRevenue = localDataJson[j].Revenue;
}
}
for (var j = 0; j < localDataJson.length; j++) {
if (localDataJson[j].TurnOver > maxTurnOver) {
maxTurnOver = localDataJson[j].TurnOver;
}
}
var convert = {
x: d3.scale.ordinal(),
y: d3.scale.linear()
};
// Define your axis
var axis = {
x: d3.svg.axis().orient('bottom')
//y: d3.svg.axis().orient('left')
};
// Define the conversion function for the axis points
axis.x.scale(convert.x);
// axis.y.scale(convert.y);
// Define the output range of your conversion functions
convert.y.range([maxHeight, 0]);
convert.x.rangeRoundBands([0, maxWidth]);
convert.x.domain(localDataJson.map(function (d) {
return d.Country;
})
);
convert.y.domain([0, maxRevenue]);
$('#chartBar').html("");
var svg = d3.select("#chartBar")
.append("svg")
.attr("width", w)
.attr("height", h);
// The group node that will contain all the other nodes
// that render your chart
$('.bar-group').html("");
var chart = svg.append('g')
.attr({
class: 'container',
transform: function (d, i) {
return 'translate(' + padding.left + ',' + padding.top + ')';
}
});
chart.append('g') // Container for the axis
.attr({
class: 'x axis',
transform: 'translate(0,' + maxHeight + ')'
})
.call(axis.x)
.selectAll("text")
.attr("x", "-.8em")
.attr("y", ".15em")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)");// Insert an axis inside this node
$('.axis path').css("fill", "none");
chart.append('g') // Container for the axis
// .attr({
// class: 'y axis',
// height: maxHeight,
// })
//.call(axis.y);
var bars = chart
.selectAll('g.bar-group')
.data(localDataJson)
.enter()
.append('g') // Container for the each bar
.attr({
transform: function (d, i) {
return 'translate(' + convert.x(d.Country) + ', 1)';
},
class: 'bar-group'
});
//Here goes filter thing ,bar of filter values will be shown others hide
if (text != "All" && text != "Clear Filter") {
svg.selectAll('g.bar-group')
.filter(function (d) {
return text != d.Country;
})
.attr("display", "none");
svg.selectAll('g.bar-group')
.filter(function (d) {
return text == d.Country;
})
.attr("display", "inline");
}
var color = d3.scale.category20();
// var color = d3.scale.ordinal()
// .range(['#f1595f', '#79c36a', '#599ad3', '#f9a65a', '#9e66ab','#cd7058']);
bars.append('rect')
.attr({
y: maxHeight,
height: 0,
width: function (d) { return convert.x.rangeBand(d) - 3; },
class: 'bar'
})
.transition()
.duration(1500)
.attr({
y: function (d, i) {
return convert.y(d.Revenue);
},
height: function (d, i) {
return maxHeight - convert.y(d.Revenue);
}
})
.attr("fill", function (d, i) {
// return color(i);
return color(d.Country);
})
// for (var i = 0; i < text.length; i++) {
// }
svg.append("text")
.attr("x", (w + padding.left + padding.right) / 2)
.attr("y", 25)
.attr("class", "title")
.attr("text-anchor", "middle")
.text("Revenue Bar Chart")
;
var svgs = svg.select("g.container").selectAll("text.label")
// svgs.selectAll("text")
.data(localDataJson)
.enter()
.append("text")
.classed("label", true)
//.transition() // <-- This is new,
// .duration(5000)
.text(function (d) {
return (d.Revenue);
})
.attr("text-anchor", "middle")
//// Set x position to the left edge of each bar plus half the bar width
.attr("x", function (d, i) {
// return (i * (w / localDataJson.length)) + ((w / localDataJson.length - barPadding) / 2);
return convert.x(d.Country) + (convert.x.rangeBand(d) - 3) / 2;
})
.attr({
y: function (d, i) {
return convert.y(d.Revenue) +20;
// return maxHeight;
},
})
.attr("font-family", "sans-serif")
.attr("font-size", "13px")
.attr("fill", "white")
}
Garima you need to do it like this in the section where you hiding the bars using display:none
svg.selectAll('g.bar-group')
.filter(function (d) {
if(text.indexOf(d.Country) == -1)
return true;
else
return false;
})
.attr("display", "none");
Note: In this i am assuming that text is an array of selected countries even when its a single selection.
Hope this helps