Issue: I want to update the bars in my graph so that when the "Dreamworks" button is clicked, it appends new bars and gets rid of the old ones. I know it is an enter(), exit() issue, but I do not know exactly how to implement it.
Context: When my button is clicked, it activates a function that extracts the inner HTML of my button and uses it to filter my data so only observations from a company remain. The code works, but instead of getting rid of old bars, it appends the new bars on top of the old ones. When you look in the console, a new "g" element appears (which contains the new "rects") every time the button is clicked. I lowered the opacity of the "rects" to show what is going on. I removed all exit() and remove() attempts from my code because nothing was working.
HTML Code:
<div class= "button-holder">
<button class= "button button-dreamworks">DreamWorks</button>
<button class= "button button-disney">Disney</button>
<button class= "button button-pixar">Pixar</button>
</div>
<div class = "chart chart1"></div>
JS Code:
async function drawBar() {
// 2. Create Chart Dimensions
const width = 600
let dimensions = {
width,
height: width*0.6,
margin: {
top: 30,
right: 10,
bottom: 50,
left: 50
}
}
dimensions.boundedWidth = dimensions.width
-dimensions.margin.right -dimensions.margin.left
dimensions.boundedHeight = dimensions.height
-dimensions.margin.top -dimensions.margin.left
// 3. Draw Canvas
const wrapper = d3.select(".chart1")
.append("svg")
.attr("width", dimensions.width)
.attr("height", dimensions.height)
// 4. Load Data
const raw_data = await d3.csv("./data/all_movie_data.csv")
const drawBarChart = function(company_name) {
const dataset = raw_data.filter(function(d){ return d["company"] == company_name })
const xAccessor = d => d["name"]
const yAccessor = d => parseFloat(d.budget)
let bounds = wrapper
.append("g")
.attr("class", "bounds")
.style(
"transform",
`translate(${dimensions.margin.left}px,${dimensions.margin.top}px)`
);
// 5. Create scales
const xScale = d3.scaleBand()
.domain(dataset.map(xAccessor))
.range([0,dimensions.boundedWidth])
.padding(0.4);
const yScale = d3.scaleLinear()
.domain(d3.extent(dataset,yAccessor))
.range([dimensions.boundedHeight, 0])
// 6. Draw Data
bounds.selectAll("rect")
.data(dataset)
.join("rect")
.attr("x", (d) => xScale(xAccessor(d)))
.attr("y", (d) => yScale(yAccessor(d)))
.attr("width", xScale.bandwidth())
.attr("height", (d) => dimensions.boundedHeight - yScale(yAccessor(d)))
.attr("fill", "blue");
}
//6. Interactions
drawBarChart("Pixar");
const button1 = d3.select(".button-dreamworks")
.node()
.addEventListener("click", onClick1)
function onClick1() {
const company = document.querySelector(".button-dreamworks").innerHTML;
drawBarChart(company);
}
}
drawBar();
You can find a version of my code in this code pen: https://codepen.io/larylc/pen/XWzbQGy
Everything is the same except for the data, which I just made up to show the issue.
Answer: I understand the enter(), exit() structure now.
By setting the "rects" as a variable (which I called bars) I can now manipulate them ( this gave me a selection object that was different from before).
I can now add exit() and remove() to the bars variable. This was not possible before.
I moved the bounds variable to the section where my canvas was, which completed the exit() and enter() pattern. Every time a button was clicked, the SVG elements in the DOM (the bars) would match the number of data elements that were supposed to be added. So if I had 22 bars in the DOM, it would update to 34(or whatever the new dataset was). I tried this before and it worked but my bars were not updating correctly, which brings me to my last point.
The last problem was the bars would be added or removed to match the number of new data points without changing existing ones. This meant that the DOM would not update the bars to match the actual data. So if I had 13 existing bars and the new data was 22, it would just keep the 13 and add the last 9. This did not necessarily match the data. So I needed to add the merge(bars) statement to ensure that all my bars (including the existing ones) would update.
My New JS Code (with all of the buttons working)
async function drawBar() {
// Create Chart Dimensions
const width = 600
let dimensions = {
width,
height: width*0.6,
margin: {
top: 30,
right: 10,
bottom: 50,
left: 50
}
}
dimensions.boundedWidth = dimensions.width
-dimensions.margin.right -dimensions.margin.left
dimensions.boundedHeight = dimensions.height
-dimensions.margin.top -dimensions.margin.left
// Draw Canvas
const wrapper = d3.select(".chart1")
.append("svg")
.attr("width", dimensions.width)
.attr("height", dimensions.height)
let bounds = wrapper
.append("g")
.attr("class", "bounds")
.style(
"transform",
`translate(${dimensions.margin.left}px,${dimensions.margin.top}px)`
);
// Load Data
const raw_data = await d3.csv("./data/all_movie_data.csv")
// Function that draws data
const drawBarChart = function(company_name) {
const dataset = raw_data.filter(function(d){ return d["company"] == company_name })
const xAccessor = d => d["name"]
const yAccessor = d => parseFloat(d.budget)
// Create scales
const xScale = d3.scaleBand()
.domain(dataset.map(xAccessor))
.range([0,dimensions.boundedWidth])
.padding(0.4);
const yScale = d3.scaleLinear()
.domain(d3.extent(dataset,yAccessor))
.range([dimensions.boundedHeight, 0])
// Draw Data
const bars = bounds.selectAll("rect")
.data(dataset)
bars.join("rect").merge(bars)
.attr("x", (d) => xScale(xAccessor(d)))
.attr("y", (d) => yScale(yAccessor(d)))
.attr("width", xScale.bandwidth())
.attr("height", (d) => dimensions.boundedHeight - yScale(yAccessor(d)))
.attr("fill", "blue")
.attr("opacity", 0.4)
bars.exit().remove();
}
// Interactions
drawBarChart("Pixar");
// All Buttons and functions that triggers data change
const button1 = d3.select(".button-dreamworks")
.node()
.addEventListener("click", onClick1)
function onClick1() {
const company = document.querySelector(".button-dreamworks").innerHTML;
drawBarChart(company);
}
const button2 = d3.select(".button-disney")
.node()
.addEventListener("click", onClick2)
function onClick2() {
const company2 = document.querySelector(".button-disney").innerHTML;
drawBarChart(company2);
}
const button3 = d3.select(".button-pixar")
.node()
.addEventListener("click", onClick3)
function onClick3() {
const company3 = document.querySelector(".button-pixar").innerHTML;
drawBarChart(company3);
}
}
drawBar();
Related
I am trying to brush over a scatterplot in d3, and I'm getting the selection points as a 2X2 array matrix. Console output and the plot are shown in the image.
The problem arises when I try to convert these points to the xScale and yScale domains of the plot. The console output for example saysUncaught TypeError: Cannot read properties of undefined (reading '336.001953125') because xScale is undefined.
Below is the code snippet where I define the brush and try to convert the points in the xScale domain.
updateVis(xvar, yvar) {
let vis = this;
// Specificy accessor functions
vis.colorValue = d => d.cylinders;
vis.xValue = d => d[xvar];
vis.yValue = d => d[yvar];
// Set the scale input domains
vis.xScale.domain([d3.min(vis.data, vis.xValue), d3.max(vis.data, vis.xValue)]);
vis.yScale.domain([0, d3.max(vis.data, vis.yValue)]);
vis.brush = d3.brush()
.extent([[vis.config.margin.left-3, vis.config.margin.top-10], [vis.width+45, vis.height+30]])
.on("start", brushstart);
function brushstart() {
let vis = this;
console.log(1)
console.log(vis.xScale)
//vis.search();
if(d3.event.selection != null) {
// cell is the SplomCell object
var brushExtent = d3.event.selection;
// Check if this g element is different than the previous brush
console.log(brushExtent)
console.log(vis.xScale)
console.log(vis.xScale[brushExtent[0][0]])
}
}
vis.brushG = vis.svg.append('g')
.attr('class', 'brush x-brush')
.call(vis.brush);
vis.renderVis();
}
Below is the entire class file
class Scatterplot {
/**
* Class constructor with basic chart configuration
* #param {Object}
* #param {Array}
*/
constructor(_config, _data, _xvar, _yvar) {
this.config = {
parentElement: _config.parentElement,
colorScale: _config.colorScale,
containerWidth: _config.containerWidth || 600,
containerHeight: _config.containerHeight || 500,
margin: _config.margin || {top: 25, right: 20, bottom: 20, left: 35},
tooltipPadding: _config.tooltipPadding || 15
}
this.data = _data;
this.xScale;
this.initVis();
}
/**
* We initialize scales/axes and append static elements, such as axis titles.
*/
initVis() {
let vis = this;
// Calculate inner chart size. Margin specifies the space around the actual chart.
vis.width = vis.config.containerWidth - vis.config.margin.left - vis.config.margin.right;
vis.height = vis.config.containerHeight - vis.config.margin.top - vis.config.margin.bottom;
// vis.xScale = d3.scaleLinear()
// .range([0, vis.width]);
vis.xScale = d3.scaleLinear()
.range([0, vis.width]);
vis.xScale2 = d3.scaleLinear()
.range([0, vis.width]);
vis.yScale = d3.scaleLinear()
.range([vis.height, 0]);
// Initialize axes
vis.xAxis = d3.axisBottom(vis.xScale)
.ticks(6)
.tickSize(-vis.height - 10)
.tickPadding(10)
.tickFormat(d => d);
//.tickFormat(d => d.cylinders);
vis.yAxis = d3.axisLeft(vis.yScale)
.ticks(6)
.tickSize(-vis.width - 10)
.tickPadding(10);
// Define size of SVG drawing area
vis.svg = d3.select(vis.config.parentElement)
.attr('width', vis.config.containerWidth)
.attr('height', vis.config.containerHeight);
// Append group element that will contain our actual chart
// and position it according to the given margin config
vis.chart = vis.svg.append('g')
.attr('transform', `translate(${vis.config.margin.left},${vis.config.margin.top})`);
// Append empty x-axis group and move it to the bottom of the chart
vis.xAxisG = vis.chart.append('g')
.attr('class', 'axis x-axis')
.attr('transform', `translate(0,${vis.height})`);
// Append y-axis group
vis.yAxisG = vis.chart.append('g')
.attr('class', 'axis y-axis');
}
/**
* Prepare the data and scales before we render it.
*/
updateVis(xvar, yvar) {
let vis = this;
// Specificy accessor functions
vis.colorValue = d => d.cylinders;
vis.xValue = d => d[xvar];
vis.yValue = d => d[yvar];
// Set the scale input domains
vis.xScale.domain([d3.min(vis.data, vis.xValue), d3.max(vis.data, vis.xValue)]);
vis.xScale2.domain([d3.min(vis.data, vis.xValue), d3.max(vis.data, vis.xValue)]);
//console.log(vis.xScale2)
vis.yScale.domain([0, d3.max(vis.data, vis.yValue)]);
vis.brush = d3.brush()
.extent([[vis.config.margin.left-3, vis.config.margin.top-10], [vis.width+45, vis.height+30]])
.on("start", brushstart);
//.on("brush", brushmove)
//.on("end", brushend);
function brushstart() {
let vis = this;
console.log(1)
console.log(vis.xScale2)
//vis.search();
if(d3.event.selection != null) {
// cell is the SplomCell object
var brushExtent = d3.event.selection;
// Check if this g element is different than the previous brush
console.log(brushExtent)
console.log(vis.xScale)
console.log(vis.xScale[brushExtent[0][0]])
}
}
vis.brushG = vis.svg.append('g')
.attr('class', 'brush x-brush')
.call(vis.brush);
vis.renderVis();
}
/**
* Bind data to visual elements.
*/
renderVis() {
let vis = this;
// Add circles
const circles = vis.chart.selectAll('.point')
.data(vis.data, d => d.name)
.join('circle')
.attr('class', 'point')
.attr('r', 4)
.attr('cy', d => vis.yScale(vis.yValue(d)))
.attr('cx', d => vis.xScale(vis.xValue(d)))
.attr('fill', d => vis.config.colorScale(vis.colorValue(d)));
// Tooltip event listeners
// Update the axes/gridlines
// We use the second .call() to remove the axis and just show gridlines
vis.xAxisG
.call(vis.xAxis)
.call(g => g.select('.domain').remove());
vis.yAxisG
.call(vis.yAxis)
.call(g => g.select('.domain').remove())
//vis.search();
}
}
I'm using D3 to zoom onto an image on click and on Mousewheel. Everything is working fine but the first zoom glitches a lot.
Here is the demo of the app.
This is how I'm zooming towards the objects:
const star = "https://gmg-world-media.github.io/skymap-v1dev/static/media/star.19b34dbf.svg";
const galaxy = "https://gmg-world-media.github.io/skymap-v1dev/static/media/galaxy.c5e7b011.svg";
const nebula = "https://gmg-world-media.github.io/skymap-v1dev/static/media/nebula.d65f45e5.svg";
const exotic = "https://gmg-world-media.github.io/skymap-v1dev/static/media/exotic.21ad5d39.svg";
const sWidth = window.innerWidth;
const sHeight = window.innerHeight;
const x = d3.scaleLinear().range([0, sWidth]).domain([-180, 180]);
const y = d3.scaleLinear().range([0, sHeight]).domain([-90, 90]);
const svg = d3.select("#render_map").append("svg").attr("width", sWidth).attr("height", sHeight);
const node = svg.append("g").attr('class', 'scale-holder');
const zoom = d3
.zoom()
.scaleExtent([1, 30])
.translateExtent([
[0, 0],
[sWidth, sHeight]
])
svg.call(zoom);
const imgG = node.append("g");
imgG
.insert("svg:image")
.attr("preserveAspectRatio", "none")
.attr("x", 0)
.attr("y", 0)
.attr("width", sWidth)
.attr("height", sHeight)
.attr("xlink:href", "https://gmg-world-media.github.io/skymap-v1dev/img-set/image-1.jpg");
imgG
.insert("svg:image")
.attr("preserveAspectRatio", "none")
.attr("x", 0)
.attr("y", 0)
.attr("width", sWidth)
.attr("height", sHeight)
.attr("xlink:href", "https://gmg-world-media.github.io/skymap-v1dev/img-set/image.jpg");
// Draw objects on map with icon size 8
drawObjects(8)
function drawObjects(size) {
const dataArray = [];
const to = -180;
const from = 180;
const fixed = 3;
const objectType = ["ST", "G", "N", "E"];
// Following loop is just for demo.
// Actual data comes from a JSON file.
for (let i = 0; i < 350; i++) {
const latitude = (Math.random() * (to - from) + from).toFixed(fixed) * 1;
const longitude = (Math.random() * (to - from) + from).toFixed(fixed) * 1;
const random = Math.floor(Math.random() * objectType.length);
dataArray.push({
"Longitude": longitude,
"Latitude": latitude,
"Category": objectType[random]
})
}
for (let index = 0; index < dataArray.length; index++) {
// Loop over the data
const item = dataArray[index]
const mY = y(Number(item.Latitude))
const mX = x(Number(item.Longitude))
if (node.select(".coords[index='" + index + "']").size() === 0) {
let shape = star;
// Plot various icons based on Category
switch (item.Category) {
case "ST":
shape = star;
break;
case "G":
shape = galaxy;
break;
case "N":
shape = nebula;
break;
case "E":
shape = exotic;
break;
}
const rect = node
.insert("svg:image")
.attr("class", "coords")
.attr("preserveAspectRatio", "none")
.attr("x", mX)
.attr("y", mY)
.attr("width", size)
.attr("height", size)
.attr("cursor", "pointer")
.attr("index", index)
.attr("xlink:href", shape)
.attr("opacity", "0")
.on("click", function() {
handleObjectClick(index, mX, mY)
})
// Add the objects on the map
rect.transition().duration(Math.random() * (2000 - 500) + 500).attr("opacity", "1")
}
}
}
function boxZoom(x, y) {
// Zoom towards the selected object
// This is the part responsible for zooming
svg
.transition()
.duration(1000)
.call(
zoom.transform,
d3.zoomIdentity
.translate(sWidth / 2, sHeight / 2)
.scale(6)
.translate(-x, -y)
);
}
function handleObjectClick(currentSelect, x, y) {
// Appending some thumbnails to the clicked object here...
//Call the zoom function
boxZoom(x, y)
}
#render_map {
width: 100vw;
height: 100vh;
margin: 0 auto;
overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="render_map">
</div>
This zoom doesn't seem to be working here. But it does definitely work in the app. I've not modified the piece of code responsible for zooming. (See this demo instead.)
The problem is that zoom jumps when you do it for the first time after a page load, and then it fixes itself.
I don't understand what I'm doing wrong here. Any hints would be lovely.
TIA!
The issue seems caused by a very expensive CSS repaint cycle. I tested this in Firefox by going to Performance in the DEV tools and starting a recording, then zooming for the first time.
I saw the fps drop enormously, and that the repaint took as much as 250ms. Normally, that is 10-50ms.
I have some pointers:
Why do you have two images behind each other? Big images are definitely the reason why repainting takes this long, and your image is 8000x4000 pixels! Start by removing the image that we're not even seeing;
Try adding an initial value of transform="translate(0, 0) scale(1)" to .scale-holder. I have a feeling that adding this the first time is what forces the entire screen to be repainted. Maybe changing an existing scale value is an easier mathematical operation than applying a scale value to something that was not scaled before;
If that doesn't help, compress the image to at most 1600 or even 1080 pixels wide. Us mortals should not even be able to see the difference.
I have two elements I need to render and a context of the big picture I am trying to achieve (a complete dashboard).
One is a chart that renders fine.
$scope.riskChart = new dc.pieChart('#risk-chart');
$scope.riskChart
.width(width)
.height(height)
.radius(Math.round(height/2.0))
.innerRadius(Math.round(height/4.0))
.dimension($scope.quarter)
.group($scope.quarterGroup)
.transitionDuration(250);
The other is a triangle, to be used for a more complex shape
$scope.openChart = d3.select("#risk-chart svg g")
.enter()
.attr("width", 55)
.attr("height", 55)
.append('path')
.attr("d", d3.symbol('triangle-up'))
.attr("transform", function(d) { return "translate(" + 100 + "," + 100 + ")"; })
.style("fill", fill);
On invocation of render functions, the dc.js render function is recognized and the chart is seen, but the d3.js render() function is not recognized.
How do I add this shape to my dc.js canvas (an svg element).
$scope.riskChart.render(); <--------------Works!
$scope.openChart.render(); <--------------Doesn't work (d3.js)!
How do I make this work?
EDIT:
I modified dc.js to include my custom chart, it is a work in progress.
dc.starChart = function(parent, fill) {
var _chart = {};
var _count = null, _category = null;
var _width, _height;
var _root = null, _svg = null, _g = null;
var _region;
var _minHeight = 20;
var _dispatch = d3.dispatch('jump');
_chart.count = function(count) {
if(!arguments.length)
return _count;
_count = count;
return _chart;
};
_chart.category = function(category) {
if(!arguments.length)
return _category
_category = category;
return _chart;
};
function count() {
return _count;
}
function category() {
return _category;
}
function y(height) {
return isNaN(height) ? 3 : _y(0) - _y(height);
}
_chart.redraw = function(fill) {
var color = fill;
var triangle = d3.symbol('triangle-up');
this._g.attr("width", 55)
.attr("height", 55)
.append('path')
.attr("d", triangle)
.attr("transform", function(d) { return "translate(" + 25 + "," + 25 + ")"; })
.style("fill", fill);
return _chart;
};
_chart.render = function() {
_g = _svg
.append('g');
_svg.on('click', function() {
if(_x)
_dispatch.jump(_x.invert(d3.mouse(this)[0]));
});
if (_root.select('svg'))
_chart.redraw();
else{
resetSvg();
generateSvg();
}
return _chart;
};
_chart.on = function(event, callback) {
_dispatch.on(event, callback);
return _chart;
};
_chart.width = function(w) {
if(!arguments.length)
return this._width;
this._width = w;
return _chart;
};
_chart.height = function(h) {
if(!arguments.length)
return this._height;
this._height = h;
return _chart;
};
_chart.select = function(s) {
return this._root.select(s);
};
_chart.selectAll = function(s) {
return this._root.selectAll(s);
};
function resetSvg() {
if (_root.select('svg'))
_chart.select('svg').remove();
generateSvg();
}
function generateSvg() {
this._svg = _root.append('svg')
.attr({width: _chart.width(),
height: _chart.height()});
}
_root = d3.select(parent);
return _chart;
}
I think I confused matters by talking about how to create a new chart, when really you just want to add a symbol to an existing chart.
In order to add things to an existing chart, the easiest thing to do is put an event handler on its pretransition or renderlet event. The pretransition event fires immediately once a chart is rendered or redrawn; the renderlet event fires after its animated transitions are complete.
Adapting your code to D3v4/5 and sticking it in a pretransition handler might look like this:
yearRingChart.on('pretransition', chart => {
let tri = chart.select('svg g') // 1
.selectAll('path.triangle') // 2
.data([0]); // 1
tri = tri.enter()
.append('path')
.attr('class', 'triangle')
.merge(tri);
tri
.attr("d", d3.symbol().type(d3.symbolTriangle).size(200))
.style("fill", 'darkgreen'); // 5
})
Some notes:
Use chart.select to select items within the chart. It's no different from using D3 directly, but it's a little safer. We select the containing <g> here, which is where we want to add the triangle.
Whether or not the triangle is already there, select it.
.data([0]) is a trick to add an element once, only if it doesn't exist - any array of size 1 will do
If there is no triangle, append one and merge it into the selection. Now tri will contain exactly one old or new triangle.
Define any attributes on the triangle, here using d3.symbol to define a triangle of area 200.
Example fiddle.
Because the triangle is not bound to any data array, .enter() should not be called.
Try this way:
$scope.openChart = d3.select("#risk-chart svg g")
.attr("width", 55)
.attr("height", 55)
.append('path')
.attr("d", d3.symbol('triangle-up'))
.attr("transform", function(d) { return "translate(" + 100 + "," + 100 + ")"; })
.style("fill", fill);
I'm having trouble translating a D3 example with a zoom behavior from v3 to v5. My code is based on this example: https://bl.ocks.org/mbostock/2206340 by Mike Bostock. I use react and I get these errors "d3.zoom(...).translate is not a function" and "d3.zoom(...).scale is not a function". I looked in the documentation, but could not find scale or translate just scaleBy and translateTo and translateBy. I can't figure out how to do it either way.
componentDidMount() {
this.drawChart();
}
drawChart = () => {
var width = window.innerWidth * 0.66,
height = window.innerHeight * 0.7,
centered,
world_id;
window.addEventListener("resize", function() {
width = window.innerWidth * 0.66;
height = window.innerHeight * 0.7;
});
var tooltip = d3
.select("#container")
.append("div")
.attr("class", "tooltip hidden");
var projection = d3
.geoMercator()
.scale(100)
.translate([width / 2, height / 1.5]);
var path = d3.geoPath().projection(projection);
var zoom = d3
.zoom()
.translate(projection.translate())
.scale(projection.scale())
.scaleExtent([height * 0.197, 3 * height])
.on("zoom", zoomed);
var svg = d3
.select("#container")
.append("svg")
.attr("width", width)
.attr("class", "map card shadow")
.attr("height", height);
var g = svg.append("g").call(zoom);
g.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);
var world_id = data2;
var world = data;
console.log(world);
var rawCountries = topojson.feature(world, world.objects.countries)
.features,
neighbors = topojson.neighbors(world.objects.countries.geometries);
console.log(rawCountries);
console.log(neighbors);
var countries = [];
// Splice(remove) random pieces
rawCountries.splice(145, 1);
rawCountries.splice(38, 1);
rawCountries.map(country => {
//console.log(parseInt(country.id) !== 010)
// Filter out Antartica and Kosovo
if (parseInt(country.id) !== parseInt("010")) {
countries.push(country);
} else {
console.log(country.id);
}
});
console.log(countries);
g.append("g")
.attr("id", "countries")
.selectAll(".country")
.data(countries)
.enter()
.insert("path", ".graticule")
.attr("class", "country")
.attr("d", path)
.attr("data-name", function(d) {
return d.id;
})
.on("click", clicked)
.on("mousemove", function(d, i) {
var mouse = d3.mouse(svg.node()).map(function(d) {
return parseInt(d);
});
tooltip
.classed("hidden", false)
.attr(
"style",
"left:" + mouse[0] + "px;top:" + (mouse[1] - 50) + "px"
)
.html(getCountryName(d.id));
})
.on("mouseout", function(d, i) {
tooltip.classed("hidden", true);
});
function getCountryName(id) {
var country = world_id.filter(
country => parseInt(country.iso_n3) == parseInt(id)
);
console.log(country[0].name);
console.log(id);
return country[0].name;
}
function updateCountry(d) {
console.log(world_id);
var country = world_id.filter(
country => parseInt(country.iso_n3) == parseInt(d.id)
);
console.log(country[0].name);
var iso_a2;
if (country[0].name === "Kosovo") {
iso_a2 = "xk";
} else {
iso_a2 = country[0].iso_a2.toLowerCase();
}
// Remove any current data
$("#countryName").empty();
$("#countryFlag").empty();
$("#countryName").text(country[0].name);
var src = "svg/" + iso_a2 + ".svg";
var img = "<img id='flag' class='flag' src=" + src + " />";
$("#countryFlag").append(img);
}
// Remove country when deselected
function removeCountry() {
$("#countryName").empty();
$("#countryFlag").empty();
}
// When clicked on a country
function clicked(d) {
if (d && centered !== d) {
centered = d;
updateCountry(d);
} else {
centered = null;
removeCountry();
}
g.selectAll("path").classed(
"active",
centered &&
function(d) {
return d === centered;
}
);
console.log("Clicked");
console.log(d);
console.log(d);
var centroid = path.centroid(d),
translate = projection.translate();
console.log(translate);
console.log(centroid);
projection.translate([
translate[0] - centroid[0] + width / 2,
translate[1] - centroid[1] + height / 2
]);
zoom.translate(projection.translate());
g.selectAll("path")
.transition()
.duration(700)
.attr("d", path);
}
// D3 zoomed
function zoomed() {
console.log("zoomed");
projection.translate(d3.event.translate).scale(d3.event.scale);
g.selectAll("path").attr("d", path);
}
};
render() {
return (
<div className="container-fluid bg">
<div class="row">
<div className="col-12">
<h2 className="header text-center p-3 mb-5">
Project 2 - World value survey
</h2>
</div>
</div>
<div className="row mx-auto">
<div className="col-md-8">
<div id="container" class="mx-auto" />
</div>
<div className="col-md-4">
<div id="countryInfo" className="card">
<h2 id="countryName" className="p-3 text-center" />
<div id="countryFlag" className="mx-auto" />
</div>
</div>
</div>
</div>
);
}
I won't go into the differences between v3 and v5 partly because it has been long enough that I have forgotten much of the specifics and details as to how v3 was different. Instead I'll just look at how to implement that example with v5. This answer would require adaptation for non-geographic cases - the geographic projection is doing the visual zooming in this case.
In your example, the zoom keeps track of the zoom state in order to set the projection properly. The zoom does not set a transform to any SVG element, instead the projection reprojects the features each zoom (or click).
So, to get started, with d3v5, after we call the zoom on our selection, we can set the zoom on a selected element with:
selection.call(zoom.transform, transformObject);
Where the base transform object is:
d3.zoomIdentity
d3.zoomIdentity has scale (k) of 1, translate x (x) and y (y) values of 0. There are some methods built into the identity prototype, so a plain object won't do, but we can use the identity to set new values for k, x, and y:
var transform = d3.zoomIdentity;
transform.x = projection.translate()[0]
transform.y = projection.translate()[1]
transform.k = projection.scale()
This is very similar to the example, but rather than providing the values to the zoom behavior itself, we are building an object that describes the zoom state. Now we can use selection.call(zoom.transform, transform) to apply the transform. This will:
set the zoom's transform to the provided values
trigger a zoom event
In our zoom function we want to take the updated zoom transform, apply it to the projection and then redraw our paths:
function zoomed() {
// Get the new zoom transform
transform = d3.event.transform;
// Apply the new transform to the projection
projection.translate([transform.x,transform.y]).scale(transform.k);
// Redraw the features based on the updaed projection:
g.selectAll("path").attr("d", path);
}
Note - d3.event.translate and d3.event.scale won't return anything in d3v5 - these are now the x,y,k properties of d3.event.transform
Without a click function, we might have this, which is directly adapted from the example in the question. The click function is not included, but you can still pan.
If we want to include a click to center function like the original, we can update our transform object with the new translate and call the zoom:
function clicked(d) {
var centroid = path.centroid(d),
translate = projection.translate();
// Update the translate as before:
projection.translate([
translate[0] - centroid[0] + width / 2,
translate[1] - centroid[1] + height / 2
]);
// Update the transform object:
transform.x = projection.translate()[0];
transform.y = projection.translate()[1];
// Apply the transform object:
g.call(zoom.transform, transform);
}
Similar to the v3 version - but by applying the zoom transform (just as we did initially) we trigger a zoom event, so we don't need to update the path as part of the click function.
All together that might look like this.
There is on detail I didn't include, the transition on click. As we triggering the zoomed function on both click and zoom, if we included a transition, panning would also transition - and panning triggers too many zoom events for transitions to perform as desired. One option we have is to trigger a transition only if the source event was a click. This modification might look like:
function zoomed() {
// Was the event a click?
var event = d3.event.sourceEvent ? d3.event.sourceEvent.type : null;
// Get the new zoom transform
transform = d3.event.transform;
// Apply the new transform to the projection
projection.translate([transform.x,transform.y]).scale(transform.k);
// Redraw the features based on the updaed projection:
(event == "click") ? g.selectAll("path").transition().attr("d",path) : g.selectAll("path").attr("d", path);
}
Generally I make sure to include a code example for my problem, however in this case my code is 100% similar to the following D3 Radio Button example, which I am simply trying to include in a react component of mine.
The relevant code from the example is the on-click handler:
.on("click",function(d,i) {
updateButtonColors(d3.select(this), d3.select(this.parentNode))
d3.select("#numberToggle").text(i+1)
});
however, rather than toggling a number, I am trying to change the state of my react app when this radio button is clicked. For now, let's say I'm simply trying to set the state to be one of 1, 2, or 3, that way (i + 1) is the state I'd like to set.
I tried calling setState() directly in the on click handler here, however my state didn't change. Any thoughts on how I can do this? Let me know if more of my code is needed here.
Edit: I've tried adding a snippet of what I have so far, but i'm struggling to get it to work here on stackoverflow.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
chartType: 1
}
}
drawChartTypeButton() {
// colors for different button states
const defaultColor= "#7777BB"
const hoverColor= "#0000ff"
const pressedColor= "#000077"
const bWidth= 8; //button width
const bHeight= 5; //button height
const bSpace= 1; //space between buttons
const x0 = 5; //x offset
const y0 = 5; //y offset
const labels = [1, 2, 3];
const updateButtonColors = function(button, parent) {
parent.selectAll("rect")
.attr("fill",defaultColor)
button.select("rect")
.attr("fill",pressedColor)
}
// groups for each button (which will hold a rect and text)
const chartTypeButton = d3.select('g.allbuttons')
const buttonGroups= chartTypeButton.selectAll("g.button")
.data(labels)
.enter()
.append("g")
.attr("class", "button")
.style("cursor", "pointer")
.on("click", function(d,i) {
updateButtonColors(d3.select(this), d3.select(this.parentNode))
this.setState({chartType: 2})
})
.on("mouseover", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill",hoverColor);
}
})
.on("mouseout", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill",defaultColor);
}
})
buttonGroups.append("rect")
.attr("class","buttonRect")
.attr("width",bWidth)
.attr("height",bHeight)
.attr("x", function(d,i) {return x0+(bWidth+bSpace)*i;})
.attr("y",y0)
.attr("rx",1) //rx and ry give the buttons rounded corners
.attr("ry",1)
.attr("fill",defaultColor)
// adding text to each toggle button group, centered
// within the toggle button rect
buttonGroups.append("text")
.attr("class","buttonText")
.attr("font-family", "arial")
.attr("font-size", "0.1em")
.attr("x",function(d,i) {
return x0 + (bWidth+bSpace)*i + bWidth/2;
})
.attr("y",y0)
.attr("text-anchor","middle")
.attr("dominant-baseline","central")
.attr("fill","black")
.text(function(d) {return d;})
}
componentDidMount() {
const chart = d3.select('.chart')
.attr('width', 320)
.attr('height', 240)
.attr("viewBox", "0, 0, " + 50 + ", " + 50 + "")
this.drawChartTypeButton();
}
render() {
return(
<div className='container'>
<svg className='chart'>
<g className="allbuttons" />
</svg>
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id='root'>
Damnit Work
</div>
You seem to be mixing up the this scope inside the click handler, you both use the this for the d3 selector as for the react component.
Normally we could retain the this scope using arrow functions, but as you seem to need it for d3 aswell, just create a local variable that saves the current context, so you can reuse it in your click function
// create a local reference to "this" in the drawCharTypeButton function
const self = this;
// use the local reference to update the componenents state
.on("click", function(d,i) {
updateButtonColors(d3.select(this), d3.select(this.parentNode));
self.setState({chartType: 2});
})
Then your current code would be working (true it only shows the 3 buttons, and selects either of the 3)
Please note that in your sample code, the chartWidth and chartHeight variable were undefined, so I set them to 320x240 so it matches a bit with the rendering space here on SO
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
chartType: 1
}
}
drawChartTypeButton() {
// colors for different button states
const defaultColor= "#7777BB"
const hoverColor= "#0000ff"
const pressedColor= "#000077"
const bWidth= 8; //button width
const bHeight= 6; //button height
const bSpace= 0.5; //space between buttons
const x0 = 5; //x offset
const y0 = 14; //y offset
const labels = [1, 2, 3];
const updateButtonColors = function(button, parent) {
parent.selectAll("rect")
.attr("fill",defaultColor)
button.select("rect")
.attr("fill",pressedColor)
}
// groups for each button (which will hold a rect and text)
const self = this;
const chartTypeButton = d3.select('g.allbuttons')
const buttonGroups= chartTypeButton.selectAll("g.button")
.data(labels)
.enter()
.append("g")
.attr("class", "button")
.style("cursor", "pointer")
.on("click", function(d,i) {
updateButtonColors(d3.select(this), d3.select(this.parentNode))
self.setState({chartType: 2})
})
.on("mouseover", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill",hoverColor);
}
})
.on("mouseout", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill",defaultColor);
}
})
buttonGroups.append("rect")
.attr("class","buttonRect")
.attr("width",bWidth)
.attr("height",bHeight)
.attr("x", function(d,i) {return x0+(bWidth+bSpace)*i;})
.attr("y",y0)
.attr("rx",5) //rx and ry give the buttons rounded corners
.attr("ry",5)
.attr("fill",defaultColor)
// adding text to each toggle button group, centered
// within the toggle button rect
buttonGroups.append("text")
.attr("class","buttonText")
.attr("font-family", "arial")
.attr("font-size", "0.1em")
.attr("x",function(d,i) {
return x0 + (bWidth+bSpace)*i + bWidth/2;
})
.attr("y",y0+bHeight/2)
.attr("text-anchor","middle")
.attr("dominant-baseline","central")
.attr("fill","white")
.text(function(d) {return d;})
}
componentDidMount() {
const chart = d3.select('.chart')
.attr('width', 160)
.attr('height', 120)
.attr("viewBox", "0, 0, " + 50 + ", " + 50 + "")
this.drawChartTypeButton();
}
render() {
return(
<div className='container'>
<svg className='chart'>
<g className="allbuttons" />
</svg>
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id='root'>
Damnit Work
</div>
A nitpick on the combination of d3, react, best practice, you should try to do all DOM manipulations inside react instead.
Now for a chart that might not be completely possible, but those 3 buttons can easily be rendered without the need of d3
I haven't combined these rendering engines yet, so I cannot really say if there are downsides to your current approach