I have two datasets in my chart and I'm switching between them by clicking to the paragraphs (I'm drawing the buttons there later). The datasets have the different dimensions so I want to replace one another in the tooltip according to the chosen dataset. I can adjust the tooltip once
.on("mouseover", function(d, i) {
var tickDate = d3.select(d3.selectAll(".axis .tick text")[0][i]).data()[0];
console.log (tickDate);
var formatDate = RU.timeFormat("%B %Y");
var tooltipDate = formatDate(tickDate);
//Get this bar's x/y values, then augment for the tooltip
var xPosition = parseFloat(d3. select(this). attr("x")) + ((barWidth - barPadding) / 2);
var yPosition = parseFloat(d3. select(this). attr("y")) / 2 + h / 2;
//Update the tooltip position and value
d3.select("#tooltip" )
.style("left" , xPosition + "px")
.style("top" , yPosition + "px")
.select("#value")
.text(d + " DIMENSION1");
d3.select("#tooltip" )
.select("#label")
.text(tooltipDate);
//Show the tooltip
d3.select("#tooltip" ).
classed("hidden" , false);
d3.select(this)
.attr("fill", "orange");
})
but I don't manage to refresh it after the switching. There is the text "DIMENSION1" in both cases now, my purpose is the text "DIMENSION2" appearance after switching and return "DIMENSION1" after the choosing of the initial dataset.
Here is my fiddle: https://jsfiddle.net/anton9ov/dw1xp1ux/
Several problems here :
Avoid code duplication by creating a function transition(dataset, dimension) called for the transitions
You don't update the mouseover event when you change you dataset. So call your mouseover function each time you update the data
svg.selectAll("rect").on("mouseover", function(d, i) {
// Your mouseover function
});
See this fiddle https://jsfiddle.net/bfz97hdw/
(I also fixed the color issue)
Related
I have a problem with a panel development. The panel generates a bar graph for the current reading and draws lines for the lower and upper limits. This works great. Now I want to draw a sparkline to it. I first draw this sparkline in a separate div element below the actual graph.
In the graph I have two rect elements, one for the background and one for the actual bar. My goal now is to determine the size of the background rect element, then assign this size to my div for the sparkline and then place the sparkline div over the graph.
But the problem is that I can't access the rect element (it's just not found).
Hopefully my question is understandable. What am I doing wrong?
Here is the code snippet:
...
var panelID = "dd-multistat-panel-" + id;
var tooltipDivID = "dd-multistat-panel-tooltip-" + id;
var RectBackID = "dd-multistat-panel-back-" + id;
var RectBackClass = "dd-multistat-panel-back-" + id;
var RectBarID = "dd-multistat-panel-bar-" + id;
...
// draw the background
svg
.append("g")
//.attr("id", RectBackID)
//.attr("class", RectBackClass)
.selectAll("rect")
.data(stripedata)
.enter()
.append("rect")
.attr("id", RectBackID)
.attr("class", RectBackClass)
.attr("width", w)
.attr("height", stripeScale.step())
.attr("x", left)
.attr("y", function (d) {
return stripeScale(d);
})
.attr("fill", "rgba(0,0,0,0)")
.attr("stroke", OutlineColor)
.on("mouseover", function (d) {
if (showTooltips || Links.length /* && i < data.length*/)
tooltipShow(d);
})
.on("mouseleave", function () {
if (!isInTooltip) {
tooltipHide(false);
}
});
...
//my seach code
var BarClassID = "." + panelID;
var PanelBackID = "#" + RectBackID;
var PanelBackClass = "." + RectBackClass;
console.log("var findBar = d3.select(" + BarClassID + ")");
var findBar = d3.select(BarClassID);
console.log(findBar.nodes()); // --> finds 1 svg
console.log("var findRect = findBar.selectAll(" + PanelBackID + ")");
var findRect = findBar.selectAll(PanelBackID);
console.log(findRect.nodes()); // --> finds 0 elements
console.log("var findRect2 = d3.selectAll(" + PanelBackID + ")");
var findRect2 = d3.selectAll(PanelBackID);
console.log(findRect2.nodes()); // --> finds 0 elements
console.log("var findRect3 = d3.selectAll(" + PanelBackClass + ")");
var findRect3 = d3.selectAll(PanelBackClass);
console.log(findRect3.nodes()); // --> finds 0 elements
console.log("var findRect4 = d3.selectAll(svg)");
var findRect4 = d3.selectAll("svg");
console.log(findRect4.nodes()); // --> finds 55 svg
console.log("var findRect5 = d3.selectAll(g)");
var findRect5 = d3.selectAll("g");
console.log(findRect5.nodes()); // --> finds 0 elements
console.log("var findRect6 = d3.selectAll(rect)");
var findRect6 = d3.selectAll("rect");
console.log(findRect6.nodes()); // --> finds 0 elements
Instead of creating multiple svgs, you should create one svg and append your bar graph and spakrline to that.
In case you stick to same logic, try using d3-selection to select your rect.
import * as d3Select from 'd3-selection';
d3Select.select('#your-rect-id');
Here are some helpful links -
ngx-charts
Simple Bar Graph
Graph Gallery
I have a svg element ; the nodes, links, labels etc. are appended to it. I got the zoom-to-particular-node-by-name functionality running but the issue is after zooming automatically to the respective node , whenever I try to pan svg (by clicking and dragging it around), it resets the zoom and the coordinates to how it was before I zoomed to a particular node. I think it has to do with the way d3.event.transform works but I am not able to fix it. I want to be able to continue panning and zooming from the node I zoomed to without resetting any values.
(Also, from a bit of debugging , I observed that the cx and cy coordinates for the nodes did not change by zooming and panning from the code, but If I were to zoom and pan to a node manually , then it would. I guess that is the problem)
var svg1 = d3.select("svg");
var width = +screen.width;
var height = +screen.height - 500;
svg1.attr("width", width).attr("height", height);
var zoom = d3.zoom();
var svg = svg1
.call(
zoom.on("zoom", function() {
svg.attr("transform", d3.event.transform);
})
)
.on("dblclick.zoom", null)
.append("g");
function highlightNode() {
var userInput = document.getElementById("targetNode");
theNode = d3.select("#" + userInput.value);
const isEmpty = theNode.empty();
if (isEmpty) {
document.getElementById("output").innerHTML = "Given node doesn't exist";
} else {
document.getElementById("output").innerHTML = "";
}
svg
.transition()
.duration(750)
.attr(
"transform",
"translate(" +
-(theNode.attr("cx") - screen.width / 2) +
"," +
-(theNode.attr("cy") - screen.height / 4) +
")"
// This works correctly
);
}
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);
}
I am developing a line chart using D3.js library and AngularJS in Ionic.
I want to customize the colour of data point depending on the value of Y-axis. Say,
the value is in range 0-30 show the data point in green colour,
the value is in range 31-40 show the data point in yellow colour,
the value is in range 41-60 show the data point in red colour etc.
I am using D3 for the first time. Also, In my code I'll be fetching data dynamically(to be specific) from back-end json file. At the back-end, there is a parameter named e(dependent on data value of Y-axis) which ranges from 0 to 3 and accordingly colour code is set at front-end depending on the which value e has. Also, the e parameter will wary for different scenarios of Y axis(Sales, Tax). For sales, 0 means 110-150, but for Tax, 0 means 50-90.Can anyone help me with this?
Expected line chart image
You can try something like this to color the circle nodes for the y axis values (In my example below its data close):
svg.selectAll("dot")
.data(data)
.enter().append("circle")
.attr("r", 3.5)
.style("fill", function(d){
if (d.close < 100)
return "red";
if (d.close < 200)
return "brown";
if (d.close < 300)
return "green";
if (d.close < 400)
return "blue";
if (d.close < 500)
return "yellow" ;
if (d.close < 600)
return "pink" ;
if (d.close < 700)
return "orange" ;
})
For angular you need to make this as directive...
EDIT
I have added a div for showing tooltip like below
<div id="mytooltip" style="position: absolute; z-index: 10; visibility: hidden; top: 82px; left: 81px;">
<div id="ttclose"></div>
<div id="ttdate"></div>
</div>
Then on move event you can show and set values as below:
.on("mouseover", function () {
return d3.select("#mytooltip").style("visibility", "visible"); //making the tooltip visible
})
.on("mousemove", function (d) {
console.log()
d3.select("#mytooltip").style("top", (d3.mouse(this)[1] + 10) + "px").style("left", (d3.mouse(this)[0] + 10) + "px");
d3.select("#mytooltip").select("#ttdate").text(function () {
return d.date; //setting the date values to tooltip
});
d3.select("#mytooltip").select("#ttclose").text(function () {
return d.close; //setting the date values to tooltip
});
return;
})
.on("mouseout", function () {
return d3.select("#mytooltip").style("visibility", "hidden"); //hidding the tooltip
});
Full working code here
Hope this helps!
I am loading data from a google spreadsheet that contains the GDP of selected countries from the 1955 to 2012. From this I want to draw a treemap. So far so good.
I've loaded the data through out internal link and formatted into an object that d3 can handle, then got the layout to draw on the screen-all well and good. I've based it on the Mike Bostock tutorial at http://bl.ocks.org/mbostock/4063582.
The problem comes when I try to transition from a set of data from say 1955 to 2010. I'm confident that the function I'm using to generate the treemap layout is working because the initial display is correct. I pass it a date and it creates the treemap structure.
However when I trigger a change the transition seems to occur and the individual squares change size. But when I examine them I realise that they are all wrong and that I seem to have mapped the new set of value onto the wrong countries.
The newstructure looks visually correct but all the names are wrong. So I get things like cyprus having the largest GDP in 2012. Its as if I've got a list in alphabetical order thats having another set of values in order of magnitude applied to the rather that the new value for say the US being mapped the old value.
Going around in circles here as I'm still faily new to d3 so all help gratefully received.
Code looks like this:
/*global app:true JST:true d3:true*/
(function (window, $) {
'use strict';
var menuItems = [];
var menuType='measure';
var checboxItems= ['advanced','emerging'];
var ddID = '0';
var model=[];
var yearValue="2012"
var group="gdp";
var treeStruc={
name:[],
children:[]
}
var margin = {top: 25, right: 5, bottom: 5, left: 5},
width = 965 - margin.left - margin.right,
height = 650 - margin.top - margin.bottom;
var color = d3.scale.category10();
app.spreadsheet.get(function (data) {
// TODO: process the data
menuItems = data.measures
//console.log(data);
//console.log('menuItems', menuItems);
//crete dropdown and use toggle to swich display on and off
$('#dropDown').click(function () {
$('ul.ddMenuList').toggle();
});
//populate the dropdown menu
for (var k = 0; k <menuItems.length; k++) {
$('#ddList').append('<li id="dd_' + k + '"><a href="#">'+menuItems[k].menulist +'</li>');
};
//add functionality to dropDown menu
$('#ddList li').bind('click', function () {
ddID = this.id.split('_')[1];
var text = $(this).text();
//console.log ("ID=",ddID);
//console.log (text, "Measure=",menuItems[ddID].type);
$('#ddTitle').empty();
$('#ddTitle').append(text);
createCheckboxes()
});
function createCheckboxes() {
//decide which check boxes to populate
if (menuItems[ddID].type==="measure") {
group=menuItems[ddID].type
checboxItems=[];
$.each(menuItems, function (i) {
if (menuItems[i].type==="group"){
checboxItems.push (menuItems[i].checkbox);
}
//console.log (checboxItems);
});
}
else {
group=menuItems[ddID].type
checboxItems=[];
$.each(menuItems, function (i) {
if (menuItems[i].type==="measure"){
checboxItems.push (menuItems[i].checkbox);
}
//console.log (checboxItems);
});
}
//Populate the check boxes
console.log ("Populating check boxes");
$('#cbHolder').empty();
$('#cbHolder').append('<form>');
$.each(checboxItems, function (i) {
$('#cbHolder').append('<input type="checkbox" id="cb_'+i+'">'+checboxItems[i]);
$('#cbHolder').append('</form>');
//console.log ("checkboxItems",checboxItems[i]);
});
changed3 ()
}
//creates an object containing just the advanced countries
treeStruc={name:[],children:[]};
console.log ("group=",group);
$.each(checboxItems, function (k) {
console.log("Parent",checboxItems[k])
model=jQuery.grep(data.stats,function(e,i){return e[checboxItems[k]];});
console.log('model', model);
treeStruc.children.push({"name":checboxItems[k],"children":[]});
//Construct the children of 1 big group to be completed to be updated for each sheet
$.each(model, function (i) {
treeStruc.children[k].children.push({'name':model[i].countryname,'size':model[i] [group]});
});
});
console.log('treeStruc', treeStruc)
Handlebars.createOptionsHelper(data.options);
drawd3 ();
});
function generateTreemapLayout(filter){
return d3.layout.treemap()
.size([width, height])
.sticky(true)
.value(function(d) {
if(d.size[filter] < 0){
return 0;
}
return d.size[filter];
});
}
function drawd3() {
console.log ("function drawd3");
var treemap = generateTreemapLayout('y'+yearValue)
var div = d3.select("#d3Object").append("div")
.style("position", "relative")
.style("width", (width + margin.left + margin.right) + "px")
.style("height", (height + margin.top + margin.bottom) + "px")
.style("left", margin.left + "px")
.style("top", margin.top + "px");
var node = div.datum(treeStruc).selectAll(".node")
.data(treemap.nodes)
.enter().append("div")
.attr("class", "node")
.call(position)
.attr("id",function(d){
return d.name;
})
.style("background", function(d) { return d.children ? color(d.name) : null; })
.text(function(d) { return d.children ? null : d.name; });
};
function position() {
this.style("left", function(d) { return d.x + "px"; })
.style("top", function(d) { return d.y + "px"; })
.style("width", function(d) { return Math.max(0, d.dx - 1) + "px"; })
.style("height", function(d) { return Math.max(0, d.dy - 1) + "px"; });
}
function changed3() {
console.log ("function changed3");
//make a new treemap layout
var treemap = generateTreemapLayout('y'+1955);
console.log('treeStruc',treeStruc);
//redraw the treemap using transition instead of enter
var node = d3.select("#d3Object")
.datum(treeStruc).selectAll(".node")
.data(treemap.nodes)
.transition()
.duration(1500)
.call(position)
}
}(this, jQuery));
Many thanks to Tom Pearson my work colleague for this. The problem lies in where the data is bound to the item on the page. When you come to re draw the treemap because the data isn't bound to the div with a nique identifier like the object name it re maps the data to the first item o the list as it where. This means that something like China's gets given Belgium's information. simple solution is as follows Instead of
.data(treemap.nodes)
use
.data(treemap.nodes,function(d){
return d.name;
})
The are two instances of this in the original drawd3 function them in the changed3 function. Hope that helps anyone stuck with something similar