I have 3 pie charts that get loaded with an animation.
Each of this chart, has a text inside, the percent number, that one chart is loaded follow the animation and start the counting from 0% to tot%
I was able to show the text in one pie chart, but when i adapt the code to be used from 3 pie charts, i cannot find the way to display 3 text.
I loop trought the 3 div and trhough the 3 percent number, i can see the correct percent number in the console, but then nothing get displayed inside the pie chart :/
I'm new to d3 so it might be much easier then what i can see.
app.numbers = {
calcPerc : function(percent) {
return [percent, 100-percent];
},
drawDonutChart : function(el, percent, width, height, text_y) {
width = typeof width !== undefined ? width : 290; //width
height = typeof height !== undefined ? height : 290; //height
text_y = typeof text_y !== undefined ? text_y : "-.10em";
var radius = Math.min(width, height) / 2;
var pie = d3.pie().sort(null);
var dataset = {
lower: this.calcPerc(0),
upper: this.calcPerc(percent)
}
//this.percents = percent;
this.arc = d3.arc()
.innerRadius(radius - 20)
.outerRadius(radius);
var svg = d3.select(el).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll("path")
.data(pie(dataset.lower))
.enter().append("path")
.attr("class", function(d, i) { return "color" + i })
.attr("d", this.arc)
.each(function(d) { this._currentArc = d; }); // store the initial values
// add text in the center of the donut
this.text = svg.append('text')
.attr("text-anchor", "middle")
.attr("dy", text_y)
.attr("d", percent)
if (typeof(percent) === "string") {
this.text.text(percent);
} else {
var self = this;
var timeout = setTimeout( function () {
clearTimeout(timeout);
path = path.data(pie(dataset.upper)); // update the data
path.transition().ease(d3.easeExp).duration('500').attrTween("d", function(a){
var progress = 0;
var format = d3.format(".0%");
// Store the displayed angles in _currentArc.
// Then, interpolate from _currentArc to the new angles.
// During the transition, _currentArc is updated in-place by d3.interpolate.
var i = d3.interpolate(this._currentArc, a);
var i2 = d3.interpolate(progress, percent);
this._currentArc = i(0);
return function(t) {
$(self.text).each(function(){
$(this).text( format(i2(t) / 100) );
});
return self.arc(i(t));
};
}); // redraw the arcs
}, 200);
}
},
init : function(){
$('.donut').each( function() {
var percent = $(this).data('donut');
app.numbers.drawDonutChart(
this,
percent,
190,
190,
".35em"
);
})
}
}
}
// initialize pie on scroll
window.onscroll = function() {
app.numbers.init();
}
Html:
<section class="row numbers section-bkg section-bkg--blue">
<div class="container">
<div class="col-xs-12 col-sm-4">
<div class="donut" data-donut="42"></div>
</div>
<div class="col-xs-12 col-sm-4">
<div class="donut" data-donut="12"></div>
</div>
<div class="col-xs-12 col-sm-4">
<div class="donut" data-donut="86"></div>
</div>
</div>
</section>
Any idea how should i display the text?
i'm pretty sure the problem is here:
$(self.text).each(function(){
$(this).text( format(i2(t) / 100) );
});
Despite mixing jQuery and D3 never being a good idea (try to avoid it), it seems that that was not the problem. The problem seems to be the fact that you're changing the text inside the factory, using a previously assigned self.
One solution (without changing your code too much, certainly there are better ways to refactor it) is getting the texts based on the current <path>:
var self2 = this;//this is the transitioning path
return function(t) {
d3.select(self2.parentNode).select("text").text(format(i2(t) / 100));
return self.arc(i(t));
};
Here is your code (without jQuery):
var app = {};
var color = d3.scaleOrdinal(d3.schemeCategory10)
app.numbers = {
calcPerc: function(percent) {
return [percent, 100 - percent];
},
drawDonutChart: function(el, percent, width, height, text_y) {
width = typeof width !== undefined ? width : 290; //width
height = typeof height !== undefined ? height : 290; //height
text_y = typeof text_y !== undefined ? text_y : "-.10em";
var radius = Math.min(width, height) / 2;
var pie = d3.pie().sort(null);
var dataset = {
lower: this.calcPerc(0),
upper: this.calcPerc(percent)
}
//this.percents = percent;
this.arc = d3.arc()
.innerRadius(radius - 20)
.outerRadius(radius);
var svg = d3.select(el).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll("path")
.data(pie(dataset.lower))
.enter().append("path")
.attr("fill", function(d, i) {
return color(i)
})
.attr("d", this.arc)
.each(function(d) {
this._currentArc = d;
}); // store the initial values
// add text in the center of the donut
this.text = svg.append('text')
.attr("text-anchor", "middle")
.attr("dy", text_y)
.attr("d", percent)
this.text.text(percent);
var self = this;
path = path.data(pie(dataset.upper)); // update the data
path.transition().ease(d3.easeExp).duration(1000).attrTween("d", function(a) {
var progress = 0;
var format = d3.format(".0%");
// Store the displayed angles in _currentArc.
// Then, interpolate from _currentArc to the new angles.
// During the transition, _currentArc is updated in-place by d3.interpolate.
var i = d3.interpolate(this._currentArc, a);
var i2 = d3.interpolate(progress, percent);
this._currentArc = i(0);
var self2 = this;
return function(t) {
d3.select(self2.parentNode).select("text").text(format(i2(t) / 100));
return self.arc(i(t));
};
}); // redraw the arcs
},
init: function() {
d3.selectAll('.donut').each(function() {
var percent = d3.select(this).node().dataset.donut;
app.numbers.drawDonutChart(
this,
percent,
190,
190,
".35em"
);
})
}
}
app.numbers.init();
<script src="https://d3js.org/d3.v4.min.js"></script>
<section class="row numbers section-bkg section-bkg--blue">
<div class="container">
<div class="col-xs-12 col-sm-4">
<div class="donut" data-donut="42"></div>
</div>
<div class="col-xs-12 col-sm-4">
<div class="donut" data-donut="12"></div>
</div>
<div class="col-xs-12 col-sm-4">
<div class="donut" data-donut="86"></div>
</div>
</div>
</section>
Related
I am building a d3.js version 4 -- liquid chart -- it has been derived from this waterchart circle gauges - but I am looking to make square bar/variants.
Bug/Problem -- fix the code so the liquid chart fills correctly as a bar percentage.
-- the difference between these two jsfiddles is just the config1.fillShape parameter. -- rect/circle
// broken bar version
http://jsfiddle.net/0ht35rpb/132/
// working old round gauge version
http://jsfiddle.net/0ht35rpb/133/
I am not sure what needs to be changed to get this working accordingly
I've been focusing on the starred aspects -- trying to test the result with config.height as a variable in the equations - but its not producing stable results -- 1% and 99% ranges.
waveHeightScale
waveHeightScaling
waveRiseScale
clipArea*
fillGroup*
waveRise
I am having issues trying to get the waveHeight/clipArea to understand the extra bar height -- when I have played with it in the past -- it fills up more space -- but then the bar height value doesn't feel right -- like 1% is too high.
here is the actual code -- it will be part of a reactjs component.
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import * as d3 from "d3";
import $ from 'jquery';
import _ from 'underscore';
import './liquidchart.css';
class LiquidChart extends Component {
componentDidMount() {
var $this = $(ReactDOM.findDOMNode(this));
console.log("chart--< this.props",this.props)
var val = $this.data("val");
var config = liquidFillGaugeDefaultSettings();
config.backgroundColor = this.props.backgroundColor;
config.textColor = this.props.textColor;
config.waveTextColor = this.props.waveTextColor;
config.waveStartColor = this.props.waveStartColor;
config.waveColorDuration = this.props.waveColorDuration;
config.waveColor = this.props.waveColor;
config.fillShape = this.props.fillShape;
config.circleThickness = this.props.circleThickness;
config.textVertPosition = this.props.textVertPosition;
config.waveAnimateTime = this.props.waveAnimateTime;
config.displayText = this.props.displayText == "true";
config.height = this.props.height;
config.width = this.props.width;
config.displayOverlay = this.props.displayOverlay == "true";
config.overlayImageSrc = this.props.overlayImageSrc;
config.overlayImageHeight = this.props.overlayImageHeight;
config.overlayImageWidth = this.props.overlayImageWidth;
config.axisLabel = this.props.axisLabel;
var gauge = loadLiquidFillGauge($this, val, config);
function liquidFillGaugeDefaultSettings(){
return {
height: 90,
width: 90,
minValue: 0, // The gauge minimum value.
maxValue: 100, // The gauge maximum value.
circleThickness: 0.05, // The outer circle thickness as a percentage of it's radius.
circleFillGap: 0.05, // The size of the gap between the outer circle and wave circle as a percentage of the outer circles radius.
backgroundColor: "#178BCA", // The color of the outer circle.
waveHeight: 0.1, // The wave height as a percentage of the radius of the wave circle.
waveCount: 2, // The number of full waves per width of the wave circle.
waveRiseTime: 2000, // The amount of time in milliseconds for the wave to rise from 0 to it's final height.
waveAnimateTime: 1500, // The amount of time in milliseconds for a full wave to enter the wave circle.
waveRise: true, // Control if the wave should rise from 0 to it's full height, or start at it's full height.
waveHeightScaling: true, // Controls wave size scaling at low and high fill percentages. When true, wave height reaches it's maximum at 50% fill, and minimum at 0% and 100% fill. This helps to prevent the wave from making the wave circle from appear totally full or empty when near it's minimum or maximum fill.
waveAnimate: true, // Controls if the wave scrolls or is static.
waveColor: "#178BCA", // The color of the fill wave.
waveOffset: .25, // The amount to initially offset the wave. 0 = no offset. 1 = offset of one full wave.
textVertPosition: .5, // The height at which to display the percentage text withing the wave circle. 0 = bottom, 1 = top.
textSize: 1, // The relative height of the text to display in the wave circle. 1 = 50%
valueCountUp: true, // If true, the displayed value counts up from 0 to it's final value upon loading. If false, the final value is displayed.
displayPercent: true, // If true, a % symbol is displayed after the value.
textColor: "#045681", // The color of the value text when the wave does not overlap it.
waveTextColor: "#A4DBf8", // The color of the value text when the wave overlaps it.
fillShape: "rect", // circle or rect - shape of wave
waveStartColor: "gold", // starting color
waveColorDuration : 1000, // how long it takes to change from starting color to wave color
displayText: true, // display the percentage text
displayOverlay: false, // display the overlay
overlayImageSrc: "", // overlay image source
overlayImageHeight: "", // overlay image height
overlayImageWidth: "", // overlay image width
axisLabel: "" //display a label at the bottom axis
};
}
function loadLiquidFillGauge(elementId, value, config) {
if(config == null) config = liquidFillGaugeDefaultSettings();
const chart = d3.select(elementId[0])
.append("svg")
.attr("width", config.width)
.attr("height", config.height);
const gauge = chart
.append("g")
.attr('transform','translate(0,0)');
if(config.displayOverlay){
const imgs = chart
.append("g")
.attr('transform','translate(0,0)')
.append("svg:image")
.attr("xlink:href", config.overlayImageSrc)
.attr("x", "0")
.attr("y", "0")
.attr("width", config.overlayImageWidth)
.attr("height", config.overlayImageHeight);
}
if(config.axisLabel){
const axisLabel = chart
.append("g")
.append("text")
.attr("x", config.width/2)
.attr("y", config.height)
.attr("dy", "-4px")
.style("text-anchor", "middle")
.text(config.axisLabel);
}
const randId = _.uniqueId('liquid_');
const radius = Math.min(parseInt(config.width), parseInt(config.height))/2;
const locationX = parseInt(config.width)/2 - radius;
var locationY = parseInt(config.height)/2 - radius;
if(config.fillShape == "rect"){
locationY = 0;
}
const fillPercent = Math.max(config.minValue, Math.min(config.maxValue, value))/config.maxValue;
let waveHeightScale = null;
if(config.waveHeightScaling){
waveHeightScale = d3.scaleLinear()
.range([0,config.waveHeight,0])
.domain([0,50,100]);
} else {
waveHeightScale = d3.scaleLinear()
.range([config.waveHeight,config.waveHeight])
.domain([0,100]);
}
const textPixels = (config.textSize*radius/2);
const textFinalValue = parseFloat(value).toFixed(2);
const textStartValue = config.valueCountUp?config.minValue:textFinalValue;
const percentText = config.displayPercent?"%":"";
const circleThickness = config.circleThickness * radius;
const circleFillGap = config.circleFillGap * radius;
const fillCircleMargin = circleThickness + circleFillGap;
const fillCircleRadius = radius - fillCircleMargin;
const waveHeight = fillCircleRadius*waveHeightScale(fillPercent*100);
const waveLength = fillCircleRadius*2/config.waveCount;
const waveClipCount = 1+config.waveCount;
const waveClipWidth = waveLength*waveClipCount;
// Rounding functions so that the correct number of decimal places is always displayed as the value counts up.
let textRounder = function(value){ return Math.round(value); };
if(parseFloat(textFinalValue) != parseFloat(textRounder(textFinalValue))){
textRounder = function(value){ return parseFloat(value).toFixed(1); };
}
if(parseFloat(textFinalValue) != parseFloat(textRounder(textFinalValue))){
textRounder = function(value){ return parseFloat(value).toFixed(2); };
}
// Data for building the clip wave area.
const data = [];
for(let i = 0; i <= 40*waveClipCount; i++){
data.push({x: i/(40*waveClipCount), y: (i/(40))});
}
// Scales for drawing the outer circle.
const gaugeCircleX = d3.scaleLinear().range([0,2*Math.PI]).domain([0,1]);
const gaugeCircleY = d3.scaleLinear().range([0,radius]).domain([0,radius]);
// Scales for controlling the size of the clipping path.
const waveScaleX = d3.scaleLinear().range([0,waveClipWidth]).domain([0,1]);
const waveScaleY = d3.scaleLinear().range([0,waveHeight]).domain([0,1]);
// Scales for controlling the position of the clipping path.
// The clipping area size is the height of the fill circle + the wave height, so we position the clip wave
// such that the it will overlap the fill circle at all when at 0%, and will totally cover the fill
// circle at 100%.
const waveRiseScale = d3.scaleLinear()
.range([(fillCircleMargin+fillCircleRadius*2+waveHeight),(fillCircleMargin-waveHeight)])
.domain([0,1]);
const waveAnimateScale = d3.scaleLinear()
.range([0, waveClipWidth-fillCircleRadius*2]) // Push the clip area one full wave then snap back.
.domain([0,1]);
// Scale for controlling the position of the text within the gauge.
const textRiseScaleY = d3.scaleLinear()
.range([fillCircleMargin+fillCircleRadius*2,(fillCircleMargin+textPixels*0.7)])
.domain([0,1]);
// Center the gauge within the parent SVG.
const gaugeGroup = gauge.append("g")
.attr('transform','translate('+locationX+','+locationY+')');
var drawOuterShell = function(){
// Draw the outer circle.
const gaugeCircleArc = d3.arc()
.startAngle(gaugeCircleX(0))
.endAngle(gaugeCircleX(1))
.outerRadius(gaugeCircleY(radius))
.innerRadius(gaugeCircleY(radius-circleThickness));
gaugeGroup.append("path")
.attr("d", gaugeCircleArc)
.style("fill", config.backgroundColor)
.attr('transform','translate('+radius+','+radius+')');
}
var drawOuterBlock = function(){
// Draw the outer block.
gaugeGroup.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", config.width)
.attr("height", config.height)
.style("fill", config.backgroundColor);
}
if(config.fillShape == "circle"){
drawOuterShell();
} else {
drawOuterBlock();
}
var appendText = function(relativeGroup, textColor){
// Text where the wave does not overlap.
const text = relativeGroup.append("text")
.text(textRounder(textStartValue) + percentText)
.attr("class", "liquidFillGaugeText")
.attr("text-anchor", "middle")
.attr("font-size", textPixels + "px")
.style("fill", textColor)
.attr('transform','translate('+radius+','+textRiseScaleY(config.textVertPosition)+')');
let textInterpolatorValue = textStartValue;
// Make the value count up.
if(config.valueCountUp){
text.transition()
.duration(config.waveRiseTime)
.tween("text", function() {
const i = d3.interpolateNumber(textInterpolatorValue, textFinalValue);
return (t) => {
textInterpolatorValue = textRounder(i(t));
// Set the gauge's text with the new value and append the % sign
// to the end
text.text(textInterpolatorValue + percentText);
}
});
}
}
if(config.displayText){
appendText(gaugeGroup, config.textColor);
}
// The clipping wave area.
const clipArea = d3.area()
.x(function(d) { return waveScaleX(d.x); } )
.y0(function(d) { return waveScaleY(Math.sin(Math.PI*2*config.waveOffset*-1 + Math.PI*2*(1-config.waveCount) + d.y*2*Math.PI));} );
if(config.fillShape == "circle") {
clipArea
.y1(function(d) { return (fillCircleRadius * 2 + waveHeight); } );
} else {
clipArea
.y1(function(d) { return (fillCircleRadius * 2 + waveHeight); } );
//.y1(function(d) { return (config.height - (fillCircleRadius * 2) + waveHeight); } );
}
const waveGroup = gaugeGroup.append("defs")
.append("clipPath")
.attr("id", "clipWave" + randId);
const wave = waveGroup.append("path")
.datum(data)
.attr("d", clipArea)
.attr("T", 0);
// The inner circle with the clipping wave attached.
const fillGroup = gaugeGroup.append("g")
.attr("clip-path", "url(#clipWave" + randId + ")");
var drawShapeWave = function(shape){
// Draw the wave shape.
if(shape == "circle") {
fillGroup.append("circle")
.attr("cx", radius)
.attr("cy", radius)
.attr("r", fillCircleRadius);
}else {
fillGroup.append("rect")
.attr("x", radius - fillCircleRadius)
.attr("y", radius - fillCircleRadius)
.attr("width", fillCircleRadius * 2)
.attr("height", fillCircleRadius * 2)
//.attr("height", config.height - (fillCircleRadius * 2));
}
fillGroup
.style("fill", config.waveStartColor)
.transition()
.duration(config.waveColorDuration)
.style("fill", config.waveColor);
}
drawShapeWave(config.fillShape);
if(config.displayText){
appendText(fillGroup, config.waveTextColor);
}
// Make the wave rise. wave and waveGroup are separate so that horizontal and vertical movement can be controlled independently.
const waveGroupXPosition = fillCircleMargin+fillCircleRadius*2-waveClipWidth;
if(config.waveRise){
waveGroup.attr('transform','translate('+waveGroupXPosition+','+waveRiseScale(0)+')')
.transition()
.duration(config.waveRiseTime)
.attr('transform','translate('+waveGroupXPosition+','+waveRiseScale(fillPercent)+')')
.on("start", function(){ wave.attr('transform','translate(1,0)'); }); // This transform is necessary to get the clip wave positioned correctly when waveRise=true and waveAnimate=false. The wave will not position correctly without this, but it's not clear why this is actually necessary.
} else {
waveGroup.attr('transform','translate('+waveGroupXPosition+','+waveRiseScale(fillPercent)+')');
}
if(config.waveAnimate) animateWave();
function animateWave() {
wave.attr('transform','translate('+waveAnimateScale(wave.attr('T'))+',0)');
wave.transition()
.duration(config.waveAnimateTime * (1-wave.attr('T')))
.ease(d3.easeLinear)
.attr('transform','translate('+waveAnimateScale(1)+',0)')
.attr('T', 1)
.on('end', function(){
wave.attr('T', 0);
animateWave(config.waveAnimateTime);
});
}
}
}
render() {
return (
<div
className="thermometer"
data-role="thermometer"
data-backgroundColor = {this.props.backgroundColor}
data-textColor = {this.props.textColor}
data-waveTextColor = {this.props.waveTextColor}
data-waveStartColor = {this.props.waveStartColor}
data-waveColorDuration = {this.props.waveColorDuration}
data-waveColor = {this.props.waveColor}
data-fillShape = {this.props.fillShape}
data-circleThickness = {this.props.circleThickness}
data-textVertPosition = {this.props.textVertPosition}
data-waveAnimateTime = {this.props.waveAnimateTime}
data-displayText = {this.props.displayText}
data-height = {this.props.height}
data-width = {this.props.width}
data-displayOverlay = {this.props.displayOverlay}
data-overlayImageSrc = {this.props.overlayImageSrc}
data-overlayImageHeight = {this.props.overlayImageHeight}
data-overlayImageWidth = {this.props.overlayImageWidth}
data-val = {this.props.val}
data-axisLabel = {this.props.axisLabel}
>
</div>
);
}
};
export default LiquidChart;
So instead of this.
w- 70
h- 200
it looks like this
I am creating a tree chart with d3.js, it works fine... but I want text to react to zooming, Here is the JSFiddle.
Please look at first node... it has lots of characters (in my case max will be 255)
When zoomed in or out, my text remains same, but I want to see all on zoom in.
var json = {
"name": "Maude Charlotte Licia Fernandez Maude Charlotte Licia Fernandez Maude Charlotte Licia Fernandez Maude Charlotte Licia FernandezMaude Charlotte Licia Fernandez Maude Charlotte Licia Fernandez Maude Charlotte Licia Fernandez Maude asdlkhkjh asd asdsd",
"id": "06ada7cd-3078-54bc-bb87-72e9d6f38abf",
"_parents": [{
"name": "Janie Clayton Norton",
"id": "a39bfa73-6617-5e8e-9470-d26b68787e52",
"_parents": [{
"name": "Pearl Cannon",
"id": "fc956046-a5c3-502f-b853-d669804d428f",
"_parents": [{
"name": "Augusta Miller",
"id": "fa5b0c07-9000-5475-a90e-b76af7693a57"
}, {
"name": "Clayton Welch",
"id": "3194517d-1151-502e-a3b6-d1ae8234c647"
}]
}, {
"name": "Nell Morton",
"id": "06c7b0cb-cd21-53be-81bd-9b088af96904",
"_parents": [{
"name": "Lelia Alexa Hernandez",
"id": "667d2bb6-c26e-5881-9bdc-7ac9805f96c2"
}, {
"name": "Randy Welch",
"id": "104039bb-d353-54a9-a4f2-09fda08b58bb"
}]
}]
}, {
"name": "Helen Donald Alvarado",
"id": "522266d2-f01a-5ec0-9977-622e4cb054c0",
"_parents": [{
"name": "Gussie Glover",
"id": "da430aa2-f438-51ed-ae47-2d9f76f8d831",
"_parents": [{
"name": "Mina Freeman",
"id": "d384197e-2e1e-5fb2-987b-d90a5cdc3c15"
}, {
"name": "Charlotte Ahelandro Martin",
"id": "ea01728f-e542-53a6-acd0-6f43805c31a3"
}]
}, {
"name": "Jesus Christ Pierce",
"id": "bfd1612c-b90d-5975-824c-49ecf62b3d5f",
"_parents": [{
"name": "Donald Freeman Cox",
"id": "4f910be4-b827-50be-b783-6ba3249f6ebc"
}, {
"name": "Alex Fernandez Gonzales",
"id": "efb2396d-478a-5cbc-b168-52e028452f3b"
}]
}]
}]
};
var boxWidth = 250,
boxHeight = 100;
// Setup zoom and pan
var zoom = d3.behavior.zoom()
.scaleExtent([.1, 1])
.on('zoom', function() {
svg.attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")");
})
// Offset so that first pan and zoom does not jump back to the origin
.translate([600, 600]);
var svg = d3.select("body").append("svg")
.attr('width', 1000)
.attr('height', 500)
.call(zoom)
.append('g')
// Left padding of tree so that the whole root node is on the screen.
// TODO: find a better way
.attr("transform", "translate(150,200)");
var tree = d3.layout.tree()
// Using nodeSize we are able to control
// the separation between nodes. If we used
// the size parameter instead then d3 would
// calculate the separation dynamically to fill
// the available space.
.nodeSize([100, 200])
// By default, cousins are drawn further apart than siblings.
// By returning the same value in all cases, we draw cousins
// the same distance apart as siblings.
.separation(function() {
return .9;
})
// Tell d3 what the child nodes are. Remember, we're drawing
// a tree so the ancestors are child nodes.
.children(function(person) {
return person._parents;
});
var nodes = tree.nodes(json),
links = tree.links(nodes);
// Style links (edges)
svg.selectAll("path.link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("d", elbow);
// Style nodes
var node = svg.selectAll("g.person")
.data(nodes)
.enter().append("g")
.attr("class", "person")
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Draw the rectangle person boxes
node.append("rect")
.attr({
x: -(boxWidth / 2),
y: -(boxHeight / 2),
width: boxWidth,
height: boxHeight
});
// Draw the person's name and position it inside the box
node.append("text")
.attr("text-anchor", "start")
.attr('class', 'name')
.text(function(d) {
return d.name;
});
// Text wrap on all nodes using d3plus. By default there is not any left or
// right padding. To add padding we would need to draw another rectangle,
// inside of the rectangle with the border, that represents the area we would
// like the text to be contained in.
d3.selectAll("text").each(function(d, i) {
d3plus.textwrap()
.container(d3.select(this))
.valign("middle")
.draw();
});
/**
* Custom path function that creates straight connecting lines.
*/
function elbow(d) {
return "M" + d.source.y + "," + d.source.x + "H" + (d.source.y + (d.target.y - d.source.y) / 2) + "V" + d.target.x + "H" + d.target.y;
}
body {
text-align: center;
}
svg {
margin-top: 32px;
border: 1px solid #aaa;
}
.person rect {
fill: #fff;
stroke: steelblue;
stroke-width: 1px;
}
.person {
font: 14px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3plus/1.8.0/d3plus.min.js"></script>
I made a sample of your requirement in this fiddle
It may need some more tweaking to position the text vertical middle; but this can be the base for you to work on. Calculations are done in the function wrap() and call on page load and zooming.
function wrap() {
var texts = d3.selectAll("text"),
lineHeight = 1.1, // ems
padding = 2, // px
fSize = scale > 1 ? fontSize / scale : fontSize,
// find how many lines can be included
lines = Math.floor((boxHeight - (2 * padding)) / (lineHeight * fSize)) || 1;
texts.each(function(d, i) {
var text = d3.select(this),
words = d.name.split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
tspan = text.text(null).append("tspan").attr("dy", "-0.5em").style("font-size", fSize + "px");
while ((word = words.pop())) {
line.push(word);
tspan.text(line.join(" "));
// check if the added word can fit in the box
if ((tspan.node().getComputedTextLength() + (2 * padding)) > boxWidth) {
// remove current word from line
line.pop();
tspan.text(line.join(" "));
lineNumber++;
// check if a new line can be placed
if (lineNumber > lines) {
// left align text of last line
tspan.attr("x", (tspan.node().getComputedTextLength() - boxWidth) / 2 + padding);
--lineNumber;
break;
}
// create new line
tspan.text(line.join(" "));
line = [word]; // place the current word in new line
tspan = text.append("tspan")
.style("font-size", fSize + "px")
.attr("dy", "1em")
.text(word);
}
// left align text
tspan.attr("x", (tspan.node().getComputedTextLength() - boxWidth) / 2 + padding);
}
// align vertically inside the box
text.attr("text-anchor", "middle").attr("y", padding - (lineHeight * fSize * lineNumber) / 2);
});
}
Also note that I've added the style dominant-baseline: hanging; to .person class
The code in this jsfiddle is an attempt to address the performance issues that you have with very large tree charts. A delay is set with setTimeout in the zoom event handler to allow zooming at "full speed", without text resizing. Once the zooming stops for a short time, the text is rearranged according to the new scaling:
var scaleValue = 1;
var refreshTimeout;
var refreshDelay = 0;
var zoom = d3.behavior.zoom()
.scaleExtent([.1, 1.5])
.on('zoom', function () {
svg.attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")");
scaleValue = d3.event.scale;
if (refreshTimeout) {
clearTimeout(refreshTimeout);
}
refreshTimeout = setTimeout(function () {
wrapText();
}, refreshDelay);
})
The delay (in milliseconds) depends on the number of nodes in the tree. You can experiment with the mathematical expression to find the best parameters for the wide range of node counts that you expect in your tree.
// Calculate the refresh delay
refreshDelay = Math.pow(node.size(), 0.5) * 2.0;
You can also set the parameters in calcFontSize to fit your needs:
// Calculate the font size for the current scaling
var calcFontSize = function () {
return Math.min(24, 10 * Math.pow(scaleValue, -0.25))
}
The initialization of the nodes has been slightly modified:
node.append("rect")
.attr({
x: 0,
y: -(boxHeight / 2),
width: boxWidth,
height: boxHeight
});
node.append("text")
.attr("text-anchor", "start")
.attr("dominant-baseline", "middle")
.attr('class', 'name')
.text(function (d) {
return d.name;
});
And the text is processed in wrapText:
// Adjust the font size to the zoom level and wrap the text in the container
var wrapText = function () {
d3.selectAll("text").each(function (d, i) {
var $text = d3.select(this);
if (!$text.attr("data-original-text")) {
// Save original text in custom attribute
$text.attr("data-original-text", $text.text());
}
var content = $text.attr("data-original-text");
var tokens = content.split(/(\s)/g);
var strCurrent = "";
var strToken = "";
var box;
var lineHeight;
var padding = 4;
$text.text("").attr("font-size", calcFontSize());
var $tspan = $text.append("tspan").attr("x", padding).attr("dy", 0);
while (tokens.length > 0) {
strToken = tokens.shift();
$tspan.text((strCurrent + strToken).trim());
box = $text.node().getBBox();
if (!lineHeight) {
lineHeight = box.height;
}
if (box.width > boxWidth - 2 * padding) {
$tspan.text(strCurrent.trim());
if (box.height + lineHeight < boxHeight) {
strCurrent = strToken;
$tspan = $text.append("tspan").attr("x", padding).attr("dy", lineHeight).text(strCurrent.trim());
} else {
break;
}
}
else {
strCurrent += strToken;
}
}
$text.attr("y", -(box.height - lineHeight) / 2);
});
}
Text wrapping can be process intensive if we have a lot of text. To address those issues, present in my first answer, this new version has improved performance, thanks to pre-rendering.
This script creates an element outside of the DOM, and stores all nodes and edges into it. Then it checks which elements would be visible, removing them from the DOM, and adding them back when appropriate.
I'm making use of jQuery for data(), and for selecting elements. In my example on the fiddle, there are 120 nodes. But it should work similarly for much more, as the only nodes rendered are the ones on screen.
I changed the zoom behaviour, so that the zoom is centered on the mouse cursor, and was surprised to see that the pan / zoom works on iOS as well.
See it in action.
UPDATE
I applied the timeout (ConnorsFan's solution), as it makes a big difference. In addition, I added a minimum scale for which text should be re-wrapped.
$(function() {
var viewport_width = $(window).width(),
viewport_height = $(window).height(),
node_width = 120,
node_height = 60,
separation_width = 100,
separation_height = 55,
node_separation = 0.78,
font_size = 20,
refresh_delay = 200,
refresh_timeout,
zoom_extent = [0.5, 1.15],
// Element outside DOM, to calculate pre-render
buffer = $("<div>");
// Parse "transform" attribute
function parse_transform(input_string) {
var transformations = {},
matches, seek;
for (matches in input_string = input_string.match(/(\w+)\(([^,)]+),?([^)]+)?\)/gi)) {
seek = input_string[matches].match(/[\w.\-]+/g), transformations[seek.shift()] = seek;
}
return transformations;
}
// Adapted from ConnorsFan's answer
function get_font_size(scale) {
fs = ~~Math.min(font_size, 15 * Math.pow(scale, -0.25));
fs = ~~(((font_size / scale) + fs) / 2)
return [fs, fs]
}
// Use d3plus to wrap the text
function wrap_text(scale) {
if (scale > 0.75) {
$("svg > g > g").each(function(a, b) {
f = $(b);
$("text", f)
.text(f.data("text"));
});
d3.selectAll("text").each(function(a, b) {
d3_el = d3.select(this);
d3plus.textwrap()
.container(d3_el)
.align("center")
.valign("middle")
.width(node_width)
.height(node_height)
.valign("middle")
.resize(!0)
.size(get_font_size(scale))
.draw();
});
}
}
// Handle pre-render (remove elements that leave viewport, add them back when appropriate)
function pre_render() {
buffer.children("*")
.each(function(i, el) {
d3.transform(d3.select(el).attr("transform"));
var el_path = $(el)[0],
svg_wrapper = $("svg"),
t = parse_transform($("svg > g")[0].getAttribute("transform")),
element_data = $(el_path).data("coords"),
element_min_x = ~~element_data.min_x,
element_max_x = ~~element_data.max_x,
element_min_y = ~~element_data.min_y,
element_max_y = ~~element_data.max_y,
svg_wrapper_width = svg_wrapper.width(),
svg_wrapper_height = svg_wrapper.height(),
s = parseFloat(t.scale),
x = ~~t.translate[0],
y = ~~t.translate[1];
if (element_min_x * s + x <= svg_wrapper_width &&
element_min_y * s + y <= svg_wrapper_height &&
0 <= element_max_x * s + x &&
0 <= element_max_y * s + y) {
if (0 == $("#" + $(el).prop("id")).length) {
if (("n" == $(el).prop("id").charAt(0))) {
// insert nodes above edges
$(el).clone(1).appendTo($("svg > g"));
wrap_text(scale = t.scale);
} else {
// insert edges
$(el).clone(1).prependTo($("svg > g"));
}
}
} else {
id = $(el).prop("id");
$("#" + id).remove();
}
});
}
d3.scale.category20();
var link = d3.select("body")
.append("svg")
.attr("width", viewport_width)
.attr("height", viewport_height)
.attr("pointer-events", "all")
.append("svg:g")
.call(d3.behavior.zoom().scaleExtent(zoom_extent)),
layout_tree = d3.layout.tree()
.nodeSize([separation_height * 2, separation_width * 2])
.separation(function() {
return node_separation;
})
.children(function(a) {
return a._parents;
}),
nodes = layout_tree.nodes(json),
edges = layout_tree.links(nodes);
// Style links (edges)
link.selectAll("path.link")
.data(edges)
.enter()
.append("path")
.attr("class", "link")
.attr("d", function(a) {
return "M" + a.source.y + "," + a.source.x + "H" + ~~(a.source.y + (a.target.y - a.source.y) / 2) + "V" + a.target.x + "H" + a.target.y;
});
// Style nodes
var node = link.selectAll("g.person")
.data(nodes)
.enter()
.append("g")
.attr("transform", function(a) {
return "translate(" + a.y + "," + a.x + ")";
})
.attr("class", "person");
// Draw the rectangle person boxes
node.append("rect")
.attr({
x: -(node_width / 2),
y: -(node_height / 2),
width: node_width,
height: node_height
});
// Draw the person's name and position it inside the box
node_text = node.append("text")
.attr("text-anchor", "start")
.text(function(a) {
return a.name;
});
// Text wrap on all nodes using d3plus. By default there is not any left or
// right padding. To add padding we would need to draw another rectangle,
// inside of the rectangle with the border, that represents the area we would
// like the text to be contained in.
d3.selectAll("text")
.each(function(a, b) {
d3plus.textwrap()
.container(d3.select(this))
.valign("middle")
.resize(!0)
.size(get_font_size(1))
.draw();
});
// START Create off-screen render
// Append node edges to memory, to allow pre-rendering
$("svg > g > path")
.each(function(a, b) {
el = $(b)[0];
if (d = $(el)
.attr("d")) {
// Parse d parameter from rect, in the format found in the d3 tree dom: M0,0H0V0V0
for (var g = d.match(/([MLQTCSAZVH])([^MLQTCSAZVH]*)/gi), c = g.length, h, k, f, l, m = [], e = [], n = 0; n < c; n++) {
command = g[n], void 0 !== command && ("M" == command.charAt(0) ? (coords = command.substring(1, command.length), m.push(~~coords.split(",")[0]), e.push(~~coords.split(",")[1])) : "V" == command.charAt(0) ? e.push(~~command.substring(1, command.length)) : "H" == command.charAt(0) && m.push(~~command.substring(1, command.length)));
}
0 < m.length && (h = Math.min.apply(this, m), f = Math.max.apply(this, m));
0 < e.length && (k = Math.min.apply(this, e), l = Math.max.apply(this, e));
$(el).data("position", a);
$(el).prop("id", "e" + a);
$(el).data("coords", {
min_x: h,
min_y: k,
max_x: f,
max_y: l
});
// Store element coords in memory
hidden_element = $(el).clone(1);
buffer.append(hidden_element);
}
});
// Append node elements to memory
$("svg > g > g").each(function(a, b) {
el = $("rect", b);
transform = b.getAttribute("transform");
null !== transform && void 0 !== transform ? (t = parse_transform(transform), tx = ~~t.translate[0], ty = ~~t.translate[1]) : ty = tx = 0;
// Calculate element area
el_min_x = ~~el.attr("x");
el_min_y = ~~el.attr("y");
el_max_x = ~~el.attr("x") + ~~el.attr("width");
el_max_y = ~~el.attr("y") + ~~el.attr("height");
$(b).data("position", a);
$(b).prop("id", "n" + a);
$(b).data("coords", {
min_x: el_min_x + tx,
min_y: el_min_y + ty,
max_x: el_max_x + tx,
max_y: el_max_y + ty
});
text_el = $("text", $(b));
0 < text_el.length && $(b).data("text", d3.select(text_el[0])[0][0].__data__.name);
// Store element coords in memory
hidden_element = $(b).clone(1);
// store node in memory
buffer.append(hidden_element);
});
// END Create off-screen render
d3_svg = d3.select("svg");
svg_group = d3.select("svg > g");
// Setup zoom and pan
zoom = d3.behavior.zoom()
.on("zoom", function() {
previous_transform = $("svg > g")[0].getAttribute("transform");
svg_group.style("stroke-width", 1.5 / d3.event.scale + "px");
svg_group.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
pre_render();
if (previous_transform !== null) {
previous_transform = parse_transform(previous_transform);
if (previous_transform.scale != d3.event.scale) {
// ConnorsFan's solution
if (refresh_timeout) {
clearTimeout(refresh_timeout);
}
scale = d3.event.scale;
refresh_timeout = setTimeout(function() {
wrap_text(scale = scale);
}, refresh_delay, scale);
}
}
});
// Apply initial zoom / pan
d3_svg.call(zoom);
});
I am working on horizontal segment bar chart. I want to make it so that the bar chart will animate the colour transition between individual segments depending on the value that is generated randomly every few seconds.
I also have a text box that at the moment says "hello" and it is moving simultaneously with the transition.It works only in one direction from left to right.
I cannot make the colour transition between segments and translating the text box from left to right. From left to right I mean that the number generated last time is greater than the currently generated number. The bar chart should turn off the segments from right to left. But it does it from left to right also the text box is acting weirdly when it comes to reverse translation.
I am also getting this error: g attribute transform: Expected transform function, "null". In my code I want to make a transition on my valueLabel and I think the way I am using is not correct. Despite this error the code gets executed.
My fiddle code is here
Many thanks for suggestions
var configObject = {
svgWidth : 1000,
svgHeight : 1000,
minValue : 1,
maxValue : 100,
midRange : 50,
highRange : 75,
numberOfSegments : 50
};
//define variables
var newValue;
var gaugeValue = configObject.minValue - 1;
var mySegmentMappingScale;
var rectArray=[];
//define svg
var svg = d3.select("body").append("svg")
.attr("width", configObject.svgWidth)
.attr("height", configObject.svgHeight)
.append("g")
.attr("transform", 'translate(' + configObject.svgWidth/2 + ',' + configObject.svgHeight/2 + ')');
//var myG=svg.append('g');
var valueLabel= svg.append("text")
.attr('x',0)
.attr('y', (configObject.svgHeight/13)+15)
.text("hello");
var backgroundRect=svg.append("rect")
.attr("fill", "black")
.attr("x",0)
.attr("y", 0)
.attr("width", (configObject.svgWidth/3))
.attr("height", configObject.svgHeight/13);
for(i = 0; i <= configObject.numberOfSegments; i++){
var myRect=svg.append("rect")
.attr("fill", "#2D2D2D")
.attr("x",i * ((configObject.svgWidth/3)/configObject.numberOfSegments))
.attr("y", 0)
.attr("id","rect"+i)
.attr("width", ((configObject.svgWidth/3)/configObject.numberOfSegments)-3)
.attr("height", configObject.svgHeight/13);
rectArray.push(myRect);
}
//define scale
function setmySegmentMappingScale(){
var domainArray = [];
var x=0;
for(i = configObject.minValue; i <= configObject.maxValue+1; i = i + (configObject.maxValue - configObject.minValue)/configObject.numberOfSegments){
if(Math.floor(i) != domainArray[x-1]){
var temp=Math.floor(i);
domainArray.push(Math.floor(i));
x++;
}
}
var rangeArray = [];
for(i = 0; i <= configObject.numberOfSegments+1; i++){// <=
rangeArray.push(i);
}
mySegmentMappingScale = d3.scale.threshold().domain(domainArray).range(rangeArray);
}
//generate random number
function generate(){
var randomNumber = Math.random() * (configObject.maxValue - configObject.minValue) + configObject.minValue;
newValue = Math.floor(randomNumber);
setmySegmentMappingScale();
animateSVG();
}
function animateSVG(){
var previousSegment = mySegmentMappingScale(gaugeValue) -1;
var newSegment = mySegmentMappingScale(newValue) -1;
if(previousSegment <= -1 && newSegment > -1){
for(i = 0; i <= newSegment; i++){
rectArray[i].transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.styleTween("fill", function() { return d3.interpolate( "#2D2D2D","red"); });
valueLabel
.transition().ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.attr("transform","translate(" + (i * ((configObject.svgWidth/3)/configObject.numberOfSegments)+((configObject.svgWidth/3)/configObject.numberOfSegments)) + "," + 0 + ")")
}
}
else if(newSegment > previousSegment){
for(i = previousSegment; i <= newSegment; i++){
rectArray[i].transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.styleTween("fill", function() { return d3.interpolate( "#2D2D2D","red"); });
//console.log(temp);
valueLabel
.transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.attr("transform","translate(" + (i * ((configObject.svgWidth/3)/configObject.numberOfSegments)+((configObject.svgWidth/3)/configObject.numberOfSegments)) + "," + 0 + ")")
}
}
else if(newSegment < previousSegment){
for(i = previousSegment; i > newSegment; i--){
rectArray[i].transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.styleTween("fill", function() { return d3.interpolate( "red","#2D2D2D"); });
valueLabel
.transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.attr("transform","translate(" + (i * ((configObject.svgWidth/3)/configObject.numberOfSegments)-((configObject.svgWidth/3)/configObject.numberOfSegments)) + "," + 0 + ")")
}
}
gaugeValue = newValue;
}
setInterval(function() {
generate()
}, 8000);
The problem is just the delay.
When newSegment > previousSegment, you set the delay like this:
.delay(function(d){return i * 90})
Which makes sense, because i is an increasing variable. But, when newSegment < previousSegment the same math doesn't work anymore: i is a decreasing variable, and the delay has to increase as i decreases, not the other way around.
This is what you need:
.delay(function(d){return Math.abs(i -previousSegment)*90})
Here is your updated fiddle: https://jsfiddle.net/b7usw2nr/
So I've created a pie chart that takes in some JSON data. Upon clicking a radio button, the pie chart's data will update depending on what option is chosen. However, I'm running into an issue because I'd like to put a D3 tooltip on the pie chart and have its values update whenever the pie chart updates... anyone know the best way to do that?
Here's what my pie chart looks like so far:
<form>
<label><input type="radio" name="dataset" value="females" checked> Females </label>
<label><input type="radio" name="dataset" value="males"> Males </label>
<label><input type="radio" name="dataset" value="totalNumber"> Both </label>
</form>
<script>
var width = 760, height = 300, radius = Math.min(width, height) / 2;
var legendRectSize = 18;
var legendSpacing = 4;
var color = d3.scale.ordinal()
.domain(["7-15", "16-24", "25-33", "34-42", "43-51", "52-60", "61-70"])
.range(["#ff8b3d", "#d65050", "#ab0e4d", "#6f0049", "#4a004b", "#d0743c", "#BE2625"]);
var arc = d3.svg.arc()
.outerRadius(radius * 0.9)
.innerRadius(radius * 0.3);
var outerArc = d3.svg.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.females; });
var svg = d3.select("#donut1").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 4 + "," + height / 2 + ")")
d3.json("graphdata.php", function(error, data) {
data.forEach(function(d) {
var path = svg.datum(data).selectAll("path")
.data(pie)
d.females = +d.females;
d.males = +d.males;
d.totalNumber = +d.totalNumber;
});
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([55, 0])
.html(function(d) {
return ("Victims:") + " " + d[value] + "<br>";
})
svg.call(tip);
var donut1 = svg.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc")
.on("mouseover", tip.show)
.on("mouseout", tip.hide);
donut1.append("path")
.style("fill", function(d) { return color(d.data.age); })
.attr("d", arc)
.each(function(d) {this._current = d; }); //stores the initial angles
/*Control for changing the radio buttons & filtering the data*/
d3.selectAll("input")
.on("change", change);
var timeout = setTimeout(function() {
d3.select("input[value=\"females\"]").property("checked", true).each(change);}, 2000);
function change() {
var path = svg.datum(data).selectAll("path")
var value = this.value;
clearTimeout(timeout);
pie.value(function(d) { return d[value]; }); //change value function
tip.html(function(d) { return "Victims:" + " " + d[value]; }); //change the tooltip
path = path.data(pie); //compute new angles
path.transition().duration(1800).attrTween("d", arcTween); //redraw the arcs, please!
}
function type(d) {
d.females = +d.females;
d.males = +d.males;
d.totalNumber = +d.totalNumber;
return d;
}
//Store displayed angles in _current
//Interpolate them from _current to new angles
//_current is updated in-place by d3.interpolate during transition
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
I know that the key lies in the change() function and within this line:
tip.html(function(d) { return "Victims:" + " " + d[value]; }); //change the tooltip
But d[value] is undefined when I try to use it on that line. I'm just not sure what else it could be..
edit:
Here's the graphdata.php file contents:
<?php include("connectDB.php");
$query = "SELECT sum(sex ='M') males, sum(sex ='F') females, CASE
WHEN age BETWEEN 7 AND 15 THEN '7-15'
WHEN age <= 24 THEN '16-24'
WHEN age <= 33 THEN '25-33'
WHEN age <= 42 THEN '34-42'
WHEN age <= 51 THEN '43-51'
WHEN age <= 52 THEN '52-60'
WHEN age <= 70 THEN '61-70'
END AS age,
COUNT(*) AS totalNumber
FROM people
GROUP BY CASE
WHEN age <= 15 THEN '7-15'
WHEN age <= 24 THEN '16-24'
WHEN age <= 33 THEN '25-33'
WHEN age <= 42 THEN '34-42'
WHEN age <= 51 THEN '43-51'
WHEN age <= 52 THEN '52-60'
WHEN age <= 70 THEN '61-70'
END";
$data = array();
if($result = mysqli_query($db,$query)) {
while ($row = mysqli_fetch_assoc($result))
{
$data[] = $row;
}
}
echo json_encode($data, JSON_PRETTY_PRINT);
?>
I got a friend to help me out. What he ended up doing was this:
function change() {
var path = svg.datum(data).selectAll("path")
var value = this.value;
clearTimeout(timeout);
pie.value(function(d) { return d[value]; }); //change value function
tip.html(function(d) {
console.log($( "input:checked" ).val() );
if($( "input:checked" ).val() == "males") {
hoverValue = d.data.males;
}
if($( "input:checked" ).val() == "females") {
hoverValue = d.data.females;
}
if($( "input:checked" ).val() == "totalNumber") {
hoverValue = d.data.totalNumber;
}
return "Victims:" + " " +hoverValue;
}); //change the tooltip
path = path.data(pie); //compute new angles
path.transition().duration(1800).attrTween("d", arcTween); //redraw the arcs, please!
}
In particular, what has changed is the tip.html function. We take the value of whatever radio value input is checked and use it to determine what data to display. However, my data was located further down into the object, so we had to navigate like so: d.data.males, for example. We set that value to a variable and call it in the return so that it displays in the tooltip.
I have a map which has been translated to make it fit on the canvas properly.
I'm trying to implement a way to zoom it and it does work, but it moves away from center when you zoom in, rather than centering on the mouse or even the canvas.
This is my code:
function map(data, total_views) {
var xy = d3.geo.mercator().scale(4350),
path = d3.geo.path().projection(xy),
transX = -320,
transY = 648,
init = true;
var quantize = d3.scale.quantize()
.domain([0, total_views*2/Object.keys(data).length])
.range(d3.range(15).map(function(i) { return "map-colour-" + i; }));
var map = d3.select("#map")
.append("svg:g")
.attr("id", "gb-regions")
.attr("transform","translate("+transX+","+transY+")")
.call(d3.behavior.zoom().on("zoom", redraw));
d3.json(url_prefix + "map/regions.json", function(json) {
d3.select("#regions")
.selectAll("path")
.data(json.features)
.enter().append("svg:path")
.attr("d", path)
.attr("class", function(d) { return quantize(data[d.properties.fips]); });
});
function redraw() {
var trans = d3.event.translate;
var scale = d3.event.scale;
if (init) {
trans[0] += transX;
trans[1] += transY;
init = false;
}
console.log(trans);
map.attr("transform", "translate(" + trans + ")" + " scale(" + scale + ")");
}
}
I've found that adding the initial translation to the new translation (trans) works for the first zoom, but for all subsequent zooms it makes it worse. Any ideas?
Here's a comprehensive starting-point: semantic zooming of force directed graph in d3
And this example helped me specifically (just rip out all the minimap stuff to make it simpler): http://codepen.io/billdwhite/pen/lCAdi?editors=001
var zoomHandler = function(newScale) {
if (!zoomEnabled) { return; }
if (d3.event) {
scale = d3.event.scale;
} else {
scale = newScale;
}
if (dragEnabled) {
var tbound = -height * scale,
bbound = height * scale,
lbound = -width * scale,
rbound = width * scale;
// limit translation to thresholds
translation = d3.event ? d3.event.translate : [0, 0];
translation = [
Math.max(Math.min(translation[0], rbound), lbound),
Math.max(Math.min(translation[1], bbound), tbound)
];
}
d3.select(".panCanvas, .panCanvas .bg")
.attr("transform", "translate(" + translation + ")" + " scale(" + scale + ")");
minimap.scale(scale).render();
}; // startoff zoomed in a bit to show pan/zoom rectangle
Though I had to tweak that function a fair bit to get it working for my case, but the idea is there. Here's part of mine. (E.range(min,max,value) just limits value to be within the min/max. The changes are mostly because I'm treating 0,0 as the center of the screen in this case.
// limit translation to thresholds
var offw = width/2*scale;
var offh = height/2*scale;
var sw = width*scale/2 - zoomPadding;
var sh = height*scale/2- zoomPadding;
translate = d3.event ? d3.event.translate : [0, 0];
translate = [
E.range(-sw,(width+sw), translate[0]+offw),
E.range(-sh,(height+sh), translate[1]+offh)
];
}
var ts = [translate[0], translate[1]];
var msvg = [scale, 0, 0, scale, ts[0], ts[1]];