My chart has multiple Y-Axes that have different domains.
When I am dragging on the chart, only the last y-axis is being updated.
I added each y-axis like below;
addYAxis(data, tag) { // tag is for index for each y-axis e.g: 0, 1, 2
const yScale = d3.scale.linear()
.domain([0, this.innerHeight])
.range([this.innerHeight, 0]);
const yAxis = d3.svg.axis()
.scale(yScale)
.orient(tag ? 'right' : 'left')
.tickSize(tag ? this.innerWidth + 50 * (tag - 1) : -this.innerWidth);
const yAxisElement = this.g.append('g')
.attr('class', 'y axis')
.call(yAxis);
this.yAxisList.push({yScale, yAxis, yAxisElement});
}
Here is the zoom list for each axis.
this.zoom.push(d3.behavior.zoom()
.x(this.xScale)
.y(this.yAxisList[tag].yScale) // when I replace [tag] with [0], then only first axis is being updated.
.scaleExtent([.5, 10])
.scale(this.currentZoom)
.translate(this.currentPan)
.on('zoom', () => this.zoomed(tag))
.on('zoomend', () => {
setTimeout(() => { this.zooming = false; }, 10);
}));
this.g.call(this.zoom[tag]) // when I replace [tag] with [0], then only first axis is being updated.
.on('dblclick.zoom', null);
And update them like below;
updateYAxis() {
this.yAxisList.forEach(({yAxisElement, yAxis}) => yAxisElement.call(yAxis));
}
Structure of this chart:
How to update all Y-axes while dragging on the chart?
Here,
.on('zoom', () => this.zoomed(tag))
you only updated current zoom, but should update other zoom objects with the current zoom values as well.
.on('zoom', () => {
this.zoom.forEach((zoom, t) => {
zoom.scale(this.zoom[tag].scale());
zoom.translate(this.zoom[tag].translate());
this.zoomed(t);
});
})
Related
I've got a d3 viz that I need to reset every time data changes. As an examples I setup a small fiddle.
click on the red box and move mouse to the right. Release mouse.
the transform X value will be displayed.
select another color from the drop down
repeat steps 1 and 2.
The transform does not start from 0 but from where we left it at the first time.
We have a requirement to reset the transform values.
This is our d3 code for the rectangle and zoom logic:
useEffect(() => {
const svg = d3.select(svgRef.current);
svg.append("g").attr("class", "group");
const g = d3.select(".group");
g.append("rect")
.attr("class", "red-rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 50)
.attr("height", 50)
.attr("fill", `${color}`);
const zoomed = (e) => {
console.log("transform", e.transform.x);
};
const zoom = d3
.zoom()
.on("zoom", zoomed)
.on("end", (e) => setTransfomX(e.transform.x));
svg.call(zoom);
}, [svgRef, color]);
I've tried to add a reset logic to the svg transform but without success
useEffect(() => {
const svg = d3.select(svgRef.current);
const redRect = d3.select(".red-rect");
const zoom = d3.zoom();
const onZoom = (e) => redRect.attr("transform", e.transform);
zoom.on("zoom", onZoom);
svg.call(zoomTransform, zoomIdentity);
}, [color]);
This logic would reset d3 zoom every time the 'color' prop changes.
I feel I'm very close but something's missing.
EDIT 09/01/23 - Fiddle link
I am trying to plot the data. See the following images:
And also the second image:
The following is the code that I have tried to manipulte:
private updateCircles(container, points: Example2D[]) {
let selection = container.selectAll("circle").data(points);
selection.enter().append("circle").attr("r", 3);
selection
.attr({
cx : (d: Example2D,i) => (i),
cy : (d: Example2D) => this.yScale(d.x1),
})
.style("fill", d => this.color(d.label));
}
}
The yScale is defined as:
this.yScale = d3.scale.linear()
.domain(yDomain)
.range([height - 1 * padding, 0]);
the yDomain = [-1,1]
What I am looking for is that the data must get reflected the exact way as it is in the thumbnail.
Let me know what I am missing to improve.
Try this
your tumbnail have differend height and width with new image
You get the data by doing
container.selectAll("circle").data(points);
Append it on selection by
selection.enter().append("circle").attr("r", 3);
but your svg container (width and height) on tumbnail and new image is different
Please try reformat the scale :
private updateCircles(container, points: Example2D[]) {
//get data
let selection = container.selectAll("circle").data(points);
// this is new image on the right width and hight
let rangeX = [ ] //find width [min,max] of your new image on the right
let rangeY = [ ] //find height [min,max] of your new image on the right
//try set new scale
let newxScale = d3.scale.linear()
.domain(xDomain)
.range(rangeX);
let newyScale = d3.scale.linear()
.domain(yDomain)
.range(rangeY);
//append with new scale
selection.enter().append("circle").attr("r", 3);
selection
.attr({
cx : (d: Example2D,i) => newxscale(i),
cy : (d: Example2D) => newyscale(d.x1),
})
.style("fill", d => this.color(d.label));
}
}
If you want to reflect the data same as the thumbnail. make sure your scaleX and scaleY is in the proper format.
I have the following bubble chart coded with dc.js which is built upon d3.js.
All is good, but for some reason I cannot see the tick marks. When I inspect the DOM I can see that they are present:
<line y2="6" x2="0"></line>
And I have applied CSS styles to them, but still they do not show!
#referrals-bubble-chart .axis .tick line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
I even added a stroke-width of 2px and still nothing shows! I know I'm targeting the right elements in my CSS because when I give it a stroke width of 10px and hover (Chrome Inspector), I see that the line is now 10px wide.
Why is this happening? Chart Code is below:
// define the referrals bubble chart attributes
referralsChart
.width(700)
.height(400)
.transitionDuration(1500) // (optional) define chart transition duration, :default = 750
.margins({top: 10, right: 50, bottom: 40, left: 50})
.dimension(diagnosisDimension)
//Bubble chart expect the groups are reduced to multiple values which would then be used
//to generate x, y, and radius for each key (bubble) in the group
.group(diagnosisDimensionGroup)
.colors(colorbrewer.RdYlGn[9]) // (optional) define color function or array for bubbles
.colorDomain([0, 100]) //(optional) define color domain to match your data domain if you want to bind data or color
.colorAccessor(function (d) {
// color - mapped to internal scale
return d.value.cost % 100;
})
.keyAccessor(function (p) {
// x-axis
return p.value.avgrtt / p.value.referrals;
})
.valueAccessor(function (p) {
// y-axis
return p.value.cost / 1000;
})
.radiusValueAccessor(function (p) {
// radius size - default is [0, 100]
return p.value.referrals;
})
.maxBubbleRelativeSize(0.1)
// .x(d3.scale.linear().domain([0, 5000]))
.x(d3.scale.linear().domain([1, 15]))
.y(d3.scale.linear().domain([1000, 10000]))
.r(d3.scale.linear().domain([0, 4000]))
//##### Elastic Scaling
//`.elasticX` and `.elasticX` determine whether the chart should rescale each axis to fit data.
//The `.yAxisPadding` and `.xAxisPadding` add padding to data above and below their max values in the same unit domains as the Accessors.
.elasticY(true)
.elasticX(false)
.yAxisPadding(200)
.xAxisLabel('Average Waiting Time - (weeks)') // (optional) render an axis label below the x axis
.yAxisLabel('Cost - (£1K)') // (optional) render a vertical axis lable left of the y axis
//#### Labels and Titles
//Labels are displaed on the chart for each bubble. Titles displayed on mouseover.
.renderLabel(true) // (optional) whether chart should render labels, :default = true
.label(function (p) {
return p.key;
})
.renderTitle(true) // (optional) whether chart should render titles, :default = false
.title(function (p) {
return [p.key,
"Referrals: " + p.value.referrals,
"Cost: £" + p.value.cost,
"RTT: " + p.value.avgrtt / p.value.referrals + " weeks"]
.join("\n");
})
//#### Customize Axis
//Set a custom tick format. Note `.yAxis()` returns an axis object, so any additional method chaining applies to the axis, not the chart.
.yAxis().tickFormat(function (v) {
return v;
});
As mentioned in the comments, it's hard to help you without a complete example, but this works for me. Since I don't have your data I made my own very simple data and adjusted a few things on my bubble chart.
var data = [];
for (var i = 1; i < 10; i++) {
data.push({
val: i
});
}
var ndx = crossfilter(data);
var dim = ndx.dimension(function(d) {
return d.val;
});
var group = dim.group().reduceSum(function(d) {
return d.val;
});
bubbleChart = dc.bubbleChart("#bubbleChart");
bubbleChart
.width(700)
.height(400)
.transitionDuration(1500)
.margins({top: 10, right: 50, bottom: 40, left: 50})
.dimension(dim)
.group(group)
.keyAccessor(function (p) {
return p.value;
})
.valueAccessor(function (p) {
return p.value;
})
.maxBubbleRelativeSize(0.1)
.x(d3.scale.linear().domain([-1, 10]))
.y(d3.scale.linear().domain([0, 10]))
.radiusValueAccessor(function (p) {
return p.value;
})
.r(d3.scale.linear().domain([0, 100]))
.elasticY(true)
.elasticX(false)
.yAxisPadding(200)
.xAxisLabel('Average Waiting Time - (weeks)')
.yAxisLabel('Cost - (£1K)')
.renderLabel(true)
.label(function (p) {
return p.key;
})
.renderTitle(true)
.title(function (p) {
return "This is the title";
})
.yAxis().tickFormat(function (v) {
return v;
});
dc.renderAll();
I want to code a reusable chart in d3 - a "normalized" stacked bar chart. The data are scaled from 0% -100% on the Y-axis - see: http://bl.ocks.org/mbostock/3886394
I have understood that I need to calculate the inverse value of the data, to scale from 0 (Y: lowest value 0%) to 1 (Y: highest value 100%).
var y = d3.scale.linear()
.range([height, 0]); // no data domain
dataSet = dataSet.map(function (d) {
return d.map(function (p, i) {
return { x: i, y: (1 / (p.Value / 100))};
});
});
However, my scaling is not working correctly
please have a look # http://jsfiddle.net/dB96T/1/
thx!!
I am using d3js to display a realtime representation of the views of a website. For this I use a stack layout and I update my dataset by JSON at the moment.
When there is only 1 or 2 views being displayed on the y axis, which is dynamic related to the amount of views in the graph, the axis labels are: 1 => 0, 0.2, 0.4, 0.6, 0.8, 1, the axis labels are: 2 => 0, 0.5, 1, 1.5, 2 This makes no sense for my dataset since it displays views of a page, and you can't have half a view.
I have a linear scale in d3js I base my y axis on
var y_inverted = d3.scale.linear().domain([0, 1]).rangeRound([0, height]);
According to the documentation of rangeRound() I should only get whole values out of this scale. For drawing my axis I use:
var y_axis = svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(0,0)")
.call(y_inverted.axis = d3.svg.axis()
.scale(y_inverted)
.orient("left")
.ticks(5));
Because it is a realtime application I update this every second by calling:
function update(){
y_inverted.domain([yStackMax, 0]);
y_axis.transition()
.duration(interval)
.ease("linear")
.call(y_inverted.axis);
}
yStackMax is calculated from a stacklayout, as far as I know the data used for the y values only contain integers.
var yStackMax = d3.max(layers, function(layer) {
return d3.max(layer, function(d) {
return d.y0 + d.y;
});
});
I have tried several things to get a proper value for my y axis.
d3.svg.axis()
.scale(y_inverted)
.orient("left")
.ticks(5).tickFormat(d3.format(",.0f"))
Got me the closest sofar, but it still displays 0, 0, 0, 1, 1, 1
Basically what I want is to only have 1 tick when yStackMax is 1, 2 ticks when it's 2, but it should also work if yStackMax is 12 or 1,000,000
Short answer: You can dynamically set the number of ticks. Set it to 1 to display only two tick labels:
var maxTicks = 5, minTicks = 1;
if (yStackMax < maxTicks) {
y_axis.ticks(minTicks)
}
else {
y_axis.ticks(maxTicks)
}
Long Answer (going a bit off topic):
While playing with your example I came up with a rather "complete solution" to all your formatting problems. Feel free to use it :)
var svg = d3.select("#svg")
var width = svg.attr("width")
var height = svg.attr("height")
var yStackMax = 100000
var interval = 500
var maxTicks = 5
var minTicks = 1
var y_inverted = d3.scale.linear().domain([0, 1]).rangeRound([0, height])
var defaultFormat = d3.format(",.0f")
var format = defaultFormat
var y_axis = d3.svg.axis()
.scale(y_inverted)
.orient("left")
.ticks(minTicks)
.tickFormat(doFormat)
var y_axis_root;
var decimals = 0;
function countDecimals(v){
var test = v, count = 0;
while(test > 10) {
test /= 10
count++;
}
return count;
}
function doFormat(d,i){
return format(d,i)
}
function init(){
y_axis_root = svg.append("g")
.attr("class", "y axis")
// I modified your example to move the axis to a visible part of the screen
.attr("transform", "translate(150,0)")
.call(y_axis)
}
// custom formatting functions:
function toTerra(d) { return (Math.round(d/10000000000)/100) + "T" }
function toGiga(d) { return (Math.round(d/10000000)/100) + "G" }
function toMega(d) { return (Math.round(d/10000)/100) + "M" }
function toKilo(d) { return (Math.round(d/10)/100) + "k" }
// the factor is just for testing and not needed if based on real world data
function update(factor){
factor = (factor) || 0.1;
yStackMax*=factor
decimals = countDecimals(yStackMax)
console.log("yStackMax decimals:",decimals, factor)
if (yStackMax < maxTicks) {
format = defaultFormat
y_axis.ticks(minTicks)
}
else {
y_axis.ticks(maxTicks)
if (decimals < 3 ) format = defaultFormat
else if(decimals < 6 ) format = toKilo
else if(decimals < 9 ) format = toMega
else if(decimals < 12) format = toGiga
else format = toTerra
}
y_inverted.domain([yStackMax, 0]);
y_axis_root.transition()
.duration(interval)
.ease("linear")
.call(y_axis);
}
init()
setTimeout(update, 200)
setTimeout(update, 400)
setTimeout(update, 600)
You can try it together with this html snippet:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.v2.js"></script>
</head>
<body>
<div><svg id="svg" width="200" height="300"></svg></div>
<script src="axis.js"></script>
<button id="button1" onclick="update(10)">+</button>
<button id="button2" onclick="update(0.1)">-</button>
</body>
</html>
I know it is a bit off topic but I usually like to provide running examples/solutions. Regard the additional formatting stuff as a bonus to the actual problem.
If you ask for a certain number of ticks (via axis.ticks() ) then d3 will try to give you that many ticks - but will try to use pretty values. It has nothing to do with your data.
Your solutions are to use tickFormat, as you did, to round all the values to integer values, only ask for one tick as Juve answered, or explicitly set the tick values using axis.tickValues([...]) which would be pretty easy used in conjunction with d3.range
rangeRound will not help in this case because it relates to the output range of the scale, which in this case is the pixel offset to plot at: between 0 and height.
Going off of Superboggly's answer, this is what worked for me. First I got the max (largest) number from the y domain using y.domain().slice(-1)[0] and then I built an array of tick values from that using d3.range()...
var y_max = y.domain().slice(-1)[0]
var yAxis = d3.svg.axis()
.scale(y)
.tickValues(d3.range(y_max+1))
.tickFormat(d3.format(",.0f"))
Or just let the ticks as they are and "hide" decimal numbers
d3.svg.axis()
.scale(y_inverted)
.orient("left")
.ticks(5).tickFormat(function(d) {
if (d % 1 == 0) {
return d3.format('.f')(d)
} else {
return ""
}
});
Here is the code:
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(d3.format(".2s"));