The problem i am facing is that i am not able to write the function following in my code because of version mismatch.
Code is
var zoom = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([1, 10])
.on("zoom", zoomed);
I have tried by this way
const zoom = d3.zoom()
.scaleExtent([1, 4])
.x(this.xScale)
.on('zoom', () => {})
But it does not work for me.
How to write same function in d3 version 5? I want to make line chart scrollable in x axis with y axis as fixed position using d3 version 5
This is my implementation Basic Code
private createLineChart() {
this.width = 2000 - this.margin.left - this.margin.right;
this.height = 600 - this.margin.top - this.margin.bottom;
// X AXIS
this.xScale = d3.scaleBand()
.domain(this.dataset[0].fluencyData.map((data) => {
return new Date(data.date);
}))
.range([0, this.width]);
// Y AXIS
this.yScale = d3.scaleLinear()
.domain([0, 110])
.range([this.height, 0]);
// Line Generator
this.line = d3.line()
.x((data) => this.xScale(new Date(data.date)))
.y((data) => this.yScale(data.wcpm));
// .curve(d3.curveMonotoneX);
// Add SVG to Div
this.svg = d3.select('#displayChart').append('svg')
.attr('preserveAspectRatio', 'xMinYMin meet')
.attr(
'viewBox',
'0 0 ' +
(this.width + this.margin.left + this.margin.right) +
' ' +
(this.height + this.margin.top + this.margin.bottom))
// .attr('width', this.width + this.margin.left + this.margin.right)
// .attr('height', this.height + this.margin.top + this.margin.bottom)
.append('g')
.attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')');
// Define the div for the tooltip
this.toolTipDiv = d3.select('#displayChart').append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
// Append XAXIS to the SVG
this.svg.append('g')
.attr('class', 'xAxis')
.attr('transform', 'translate(0,' + this.height + ')')
.call(d3.axisBottom(this.xScale).tickSizeOuter(0).tickFormat(d3.timeFormat('%b %d')));
const zoom = d3.zoom()
.scaleExtent([1, 4])
.extent([100, 100], [this.width - 100, this.height - 100])
.x(this.xScale)
.on('zoom', () => {
console.log(d3.event.transform);
// this.svg.select('#displayChart').attr('d', this.line);
});
this.svg.call(zoom);
// Append YAXIS to SVG
this.svg.append('g')
.attr('class', 'yAxis')
.call(d3.axisLeft(this.yScale).tickSize(-this.width)
);
// Make a Path for Dataset
this.svg.append('path')
.datum(this.dataset[0].fluencyData)
.attr('class', 'line')
.attr('d', this.line)
.attr('transform', 'translate(' + this.margin.left + ',0)');
// Text Heading of DATE in chart
this.svg.append('text')
.attr('transform', 'translate(' + (-20) + ',' + (this.height + 13) + ')')
.attr('dy', '.35em')
.attr('class', ' xAxis')
.text('Date');
}
}
Error I am getting is
LineChartComponent_Host.ngfactory.js? [sm]:1 ERROR TypeError: d3__WEBPACK_IMPORTED_MODULE_2__.zoom(...).scaleExtent(...).x is not a function
at LineChartComponent.push../src/app/line-chart/line-chart.component.ts
With d3v3 and before, the zoom could track a scale's state. From the documentation, scale.x(): "Specifies an x-scale whose domain should be automatically adjusted when zooming." (docs). This modifies the original scale.
D3v4+ does not have zoom.x or zoom.y methods.
With d3v4+, the zoom does not track or modifiy a d3 scale's state. Infact, for d3v4+, the zoom behavior doesn't even track the current zoom state: "Zoom behaviors no longer store the active zoom transform (i.e., the visible region; the scale and translate) internally. The zoom transform is now stored on any elements to which the zoom behavior has been applied.(change log)".
As part of this, and more importantly, "Zoom behaviors are no longer dependent on scales, but you can use transform.rescaleX, transform.rescaleY, transform.invertX or transform.invertY to transform a scale’s domain(change log)".
So rather than have the zoom update the d3 scale, we need to do this ourselves. The most common way this is done is through a reference scale, which remains unchanged, and a scale to which we apply the zoom transform:
var zoom = d3.zoom()
.on("zoom",zoomed)
var x = d3.scaleLinear().... // working scale
var x2 = x.copy(); // reference scale.
function zoomed() {
x = d3.event.transform.rescaleX(x2) // update the working scale.
// do something...
}
So, something like this:
var x = d3.scale.linear()
.domain([0,1])
.range([0,500]);
var zoom = d3.behavior.zoom()
.x(x)
.scaleExtent([1, 10])
.on("zoom", zoomed);
var svg = d3.select("svg")
.call(zoom);
var axis = d3.svg.axis()
.orient("bottom")
.scale(x);
var axisG = svg.append("g")
.attr("transform", "translate(0,30)")
.call(axis);
function zoomed() {
axisG.call(axis);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<svg width="500" height="200"></svg>
Becomes something like that:
var x = d3.scaleLinear()
.domain([0,1])
.range([0,500]);
var x2 = x.copy(); // reference.
var zoom = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", zoomed);
var svg = d3.select("svg")
.call(zoom);
var axis = d3.axisBottom().scale(x)
var axisG = svg.append("g")
.attr("transform", "translate(0,30)")
.call(axis);
function zoomed() {
x = d3.event.transform.rescaleX(x2)
axis.scale(x);
axisG.call(axis);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="200"></svg>
Note that d3.event.transform.rescaleX is for continuous scales - you have an ordinal band scale, so we'll need to use a slightly modified approach for band and/or point scales:
var x = d3.scaleBand()
.domain(d3.range(10).map(function(d) { return d/10; }))
.range([0,500]);
var x2 = x.copy(); // reference.
var zoom = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", zoomed);
var svg = d3.select("svg")
.call(zoom);
var axis = d3.axisBottom().scale(x)
var axisG = svg.append("g")
.attr("transform", "translate(0,30)")
.call(axis);
function zoomed() {
// Rescale the range of x using the reference range of x2.
x.range(x2.range().map(function(d) {
return d3.event.transform.applyX(d);
}))
axisG.call(axis);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="200"></svg>
This is band/point scale solution is based on this issue and Bostock's proposed solution to it
Related
I encountered a problem when using d3.brush.the problem
The graph is outside the limit.
related code(var xScale is for the bigger graph and xScale_ is for the graph which has a brush):
var template = {
width: '1200',
height: '520',
padding: 20,
xScaleTick: 50,
yScaleTick: 20
};
function initScale(dataset) {
xScale = d3
.scaleLinear()
.domain([1, dataset[dataset.length - 1]['chapter']])
.range([template.padding, template.width - 2*template.padding]);
xAxis = d3
.axisBottom()
.scale(xScale)
.ticks(template.xScaleTick);
}
xScale_ = d3
.scaleLinear()
.domain([1, dataset[dataset.length - 1]['chapter']])
.range([template.padding, template.width - 2 * template.padding]);
xAxis_ = d3
.axisBottom()
.scale(xScale_)
.ticks(template.xScaleTick);
var brush = d3
.brushX()
.extent([[xScale_(1), 0], [timeline.width, timeline.height]])
.on('brush', brushed);
$timeline
.append('g')
.attr('class', 'brush')
.call(brush)
.call(brush.move, xScale_.range().map(value => value / 2));
function brushed() {
var s = d3.event.selection || xScale_.range();
var smap = s.map(xScale_.invert,xScale_);
xScale.domain(smap).nice();
xAxis = d3
.axisBottom()
.scale(xScale)
.ticks(template.xScaleTick);
$chart
.selectAll('g.area')
.select('path')
.attr('d', area)
.attr('transform', 'translate(' + template.padding + ',0)');
$chart.select('g.x').call(xAxis);
}
I think maybe I have made the graph's padding in a wrong way,but I don't know how to fix it.
Thanks for any help.Or any related examples will be helpful.
See this example: Zoomable Area
You can define a clipPath to crop the graph to the area you wish. Code from the example:
var areaPath = g.append("path")
.attr("clip-path", "url(#clip)")
.attr("fill", "steelblue");
g.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
I don't know if there is a better solution. I just appended a "rect" to overlap the part out of the limit.
Sorry for my poor English.
I'm working with D3 to create a line-graph. This graph is available here jsfiddle.
I'm trying to draw lines manually to represent certain data-point-values. I've tried to add comments to most of the lines in the code, so hopefully you can follow along.
My problem is that I cannot seem to draw negative numbers in a good way, if i do, then the graph-data-lines are misaligned. So my question is: How can i scale my graph so that I can show both negative and positive numbers? In this case, the graph should go from 2 to -2 based on the max/min values i've set.
currently. I'm scaling my graph like this
//
// Setup y scale
//
var y = d3.scale.linear()
.domain([0, max])
.range([height, 0]);
//
// Setup x scale
//
var x = d3.time.scale()
.domain(d3.extent(data, dateFn))
.range([0, width]);
In my mind, doing .domain([-2,max]) would be sufficient, but that seems to make things worse.
Also, my lines do not seem to match what the data-lines are saying. In the jsfiddle, the green line is set at 1. But the data-lines whose value are 1, are not on that green line.
So, this is pretty much a scale question i guess.
Visual (picasso-esc) representation of what the graph should look like if it worked.
As you want your y domain to be [-2, 2] as opposed to be driven by the data, you can remove a lot of setup and helper functions from your drawGraph function.
After drawing your graph, you can simply loop through the yLines array, and draw a line for each with the specified color, at the specified val according to your yScale.
Update: EDITED: As you will be supplied the values of nominal, upperTolerance, lowerTolerance, innerUpperTolerance, innerLowerTolerance from your endpoint (and they don't need to be calculated from the data on the client side), just feed those values into your data-driven yScale to draw the coloured lines.
Below I have just used the values 1, 1.8, -1.8, but you will receive values that will be more meaningfully tied to your data.
// Setup
const yLines = [{
val: 1,
color: 'green'
},
{
val: 1.8,
color: 'yellow'
},
{
val: -1.8,
color: 'red'
}
]
const margin = {
top: 10,
right: 80,
bottom: 60,
left: 20
};
const strokeWidth = 3;
const pointRadius = 4;
const svgWidth = 600;
const svgHeight = 600;
const width = svgWidth - margin.left - margin.right;
const height = svgHeight - margin.top - margin.bottom;
const stroke = '#2990ea'; // blue
const areaFill = 'rgba(41,144,234,0.1)'; // lighter blue
const format = d3.time.format("%b %e %Y");
const valueFn = function(d) {
return d.value
};
const dateFn = function(d) {
return format.parse(d.name)
};
// select the div and append svg to it
const graph = d3.select('#chart').append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.style('overflow', 'visible');
const transformGroup = graph.append('g')
.attr('tranform', `translate(${margin.left}, ${margin.right})`)
// Make a group for yLines
const extraLines = transformGroup.append('g')
.attr('class', 'extra-lines')
// Generate some dummy data
const getData = function() {
let JSONData = [];
for (var i = 0; i < 30; i++) {
JSONData.push({
"name": moment().add(i, 'days').format('MMM D YYYY'),
"value": Math.floor(Math.random() * (Math.floor(Math.random() * 20))) - 10
})
}
return JSONData.slice()
}
const drawGraph = function(data) {
console.log(data)
// Setup y scale
const y = d3.scale.linear()
.domain(d3.extent(data.map((d) => d.value)))
.range([height, 0]);
// Setup y axis
const yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(10)
.tickSize(0, 0, 0)
// append group & call yAxis
transformGroup.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + margin.left + ",0)")
.call(yAxis);
// Draw extra coloured lines from yLines array
extraLines.selectAll('.extra-line')
.data(yLines)
.enter()
.append('line')
.attr('class', 'extra-line')
.attr('x1', margin.left)
.attr('x2', svgWidth - margin.right)
.attr('stroke', d => d.color)
.attr('y1', d => y(+d.val))
.attr('y2', d => y(+d.val))
.attr('stroke-width', strokeWidth)
.attr('opacity', 0.5)
// Setup x scale
const x = d3.time.scale()
.domain(d3.extent(data, dateFn))
.range([0, width])
// function for filling area under chart
const area = d3.svg.area()
.x(d => x(format.parse(d.name)))
.y0(height)
.y1(d => y(d.value))
// function for drawing line
const line = d3.svg.line()
.x(d => x(format.parse(d.name)))
.y(d => y(d.value))
const lineStart = d3.svg.line()
.x(d => x(format.parse(d.name)))
.y(d => y(0))
// make the line
transformGroup.append('path')
.attr('stroke', stroke)
.attr('stroke-width', strokeWidth)
.attr('fill', 'none')
.attr('transform', `translate(${margin.left}, ${margin.top})`)
.attr('d', lineStart(data))
.attr('d', line(data))
// fill area under the graph
transformGroup.append("path")
.datum(data)
.attr("class", "area")
.attr('fill', areaFill)
.attr('transform', `translate(${margin.left}, ${margin.top})`)
.attr('d', lineStart(data))
.attr("d", area)
}
drawGraph(getData())
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"></script>
<div id="chart" style="margin: 0 auto;"></div>
I'm trying to use rescaleX on a scaleBand to add zoom functionality to a Grouped Bar Chart.
var x0 = d3.scaleBand()
.rangeRound([0, width])
.paddingInner(0.1);
var x1 = d3.scaleBand()
.padding(0.05);
var zoom = d3.zoom()
.scaleExtent([1, 8])
.translateExtent([[0, 0], [width, height]])
.extent([[0, 0], [width, height]])
.on("zoom", () => { zoomed() });
...
function zoomed() {
var t = d3.event.transform;
var x0t = t.rescaleX(this.x0);
var x1t = t.rescaleX(this.x1);
...
}
But t.rescaleX(this.x0) doesn't work on a scaleBand, how can I apply zoom functionallity to a scaleBand?
Here is a full codepen Grouped Bar Chart
I was able to achieve this by changing the zoomed function to:
function zoomed() {
var t = d3.event.transform;
// redefine the x0 domain range with the event transform scale (k)
x0.range([0, width * t.k]);
// transform .barGroup using redefined domain range and event transform params
g.selectAll(".barGroup")
.attr("transform", function(d) { return "translate(" + (x0(d.State) + t.x) + ",0)scale(" + t.k + ",1)"; });
// apply transform to .axis--x and call xAxis
g.select(".axis--x")
.attr("transform", "translate(" + t.x + "," + (height) + ")")
.call(xAxis);
}
Full codepen here
Update:
A better method of zooming a scaleBand bar chart by Mike Bostock (non-grouped)
https://beta.observablehq.com/#mbostock/d3-zoomable-bar-chart
Update 2:
Updated Grouped bar chart zoom function:
function zoomed() {
x0.range([0, width].map(d => d3.event.transform.applyX(d)));
x1.rangeRound([0, x0.bandwidth()]);
g.selectAll(".barGroup").attr("transform", function(d) { return "translate(" + x0(d.State) + ",0)"; });
g.selectAll(".bar").attr("x", function(d) { return x1(d.key); }).attr("width", x1.bandwidth());
g.select(".axis--x").call(xAxis);
}
Full Codepen: here
I'm working with d3 v4 and struggling to create a simple chart, to use as a reference for further development, which both utilises the d3.zoom() function and the chart resizes to fit the browser window. Whenever I get one of these working the other breaks. The Typescript code is within an Angular 4 component but that shouldn't have an impact.
First I initialise some properties:
// Create a variable to hold the DOM Element which the chart will be attached to.
this.element = this.chartContainer.nativeElement;
// Set the width and height
this.width = this.svgWidth - this.margin.left - this.margin.right;
this.height = this.svgHeight - this.margin.top - this.margin.bottom;
// create scale objects
this.xScale = d3.scaleLinear()
.domain([0, 100])
.range([0, this.width]);
this.yScale = d3.scaleLinear()
.domain([-10, -20])
.range([this.height, 0]);
// create axis objects
this.xAxis = d3.axisBottom(this.xScale);
this.yAxis = d3.axisLeft(this.yScale);
Then I define my zoomFunction():
this.zoomFunction = () => {
if (d3.event.transform != null) {
//console.log(d3.event.transform);
if (this.xScale != null) {
// create new scale ojects based on event
let new_xScale = d3.event.transform.rescaleX(this.xScale);
let new_yScale = d3.event.transform.rescaleY(this.yScale);
// update axes
this.xChartAxis.call(this.xAxis.scale(new_xScale));
this.yChartAxis.call(this.yAxis.scale(new_yScale));
// update circle
this.circles.attr("transform", d3.event.transform)
};
};
};
Define my zoom() function:
this.zoom = d3.zoom().on("zoom", this.zoomFunction);
Define my windowResize() function:
this.onWindowResize =() => {
// Set the width and height.
// The element.offsetWidth is the total width of the DOM element including scrollbars, borders etc.
this.svgWidth = this.element.offsetWidth;
this.svgHeight = this.element.offsetHeight;
// Update the width and height which the chart fits into.
this.width = this.element.offsetWidth - this.margin.left - this.margin.right;
this.height = this.element.offsetHeight - this.margin.top - this.margin.bottom;
// Update the width of the svgViewport.
this.svgViewport.attr("width", this.svgWidth);
this.svgViewport.attr("height", this.svgHeight);
// Update the size of the zoomView
this.zoomView
.attr("width", this.width)
.attr("height", this.height);
// Update the Range of the x scale.
this.xScale.range([0, this.width]);
this.yScale.range([0, this.height]);
// Update the axis.
this.innerSpace.select(".axisY")
.call(this.yAxis);
this.innerSpace.select(".axisX")
.attr("transform", "translate(0," + this.height + ")")
.call(this.xAxis);
};
Add an EventListener:
window.addEventListener("resize", this.onWindowResize);
Finally create my chart:
createChart() {
// Create a circle
this.originalCircle = {
"cx": 50,
"cy": -15,
"r": 20
};
// Append an svg object to the chart element and save in the variable 'svgViewport'.
this.svgViewport = d3.select(this.element).append('svg')
.attr("class", "chart")
.attr("width", this.svgWidth)
.attr("height", this.svgHeight);
// Inner Drawing Space
this.innerSpace = this.svgViewport.append("g")
.attr("class", "inner-space")
.attr("transform", "translate(" + this.margin.left + "," + this.margin.top + ")")
.call(this.zoom);
// Add the circle.
this.circles = this.innerSpace.append('circle')
.attr("id", "circles")
.attr("cx", this.xScale(this.originalCircle.cx))
.attr("cy", this.yScale(this.originalCircle.cy))
.attr('r', this.originalCircle.r);
// Draw Axis
this.xChartAxis = this.innerSpace.append("g")
.attr("class", "axisX")
.attr("transform", "translate(0," + this.height + ")")
.call(this.xAxis);
this.yChartAxis = this.innerSpace.append("g")
.attr("class", "axisY")
.call(this.yAxis);
// append zoom area
this.zoomView = this.innerSpace.append("rect")
.attr("class", "zoom")
.attr("width", this.width)
.attr("height", this.height)
.call(this.zoom);
};
The curious thing is that if I refresh the browser and resize the window the axis move and resize as expected, but the circle does not move. When I pan/zoom the circle moves and scales correctly and the axis move correctly. Having pan/zoomed and then resize the window the axis do not move any more and the circle remains static.
I think I am close, but I can't see the next step. Any suggestions very welcome.
I'm working on a d3 map with topojson format data.
I can draw the country shapes and the zoom and pane works fine.
The problem is when I try to plot cities on the map.
I can not figure out how to manage the zoom with those points: the point size must be the same, but the points must translate right.
this is an example, when I zoom the map, the points translate out of the map:
var width = 724;
var height = 768;
var objMap = null;
var x, y;
//Projection
projection = d3.geo.transverseMercator()
.center([2.5, -38.5])
.rotate([66, 0])
.scale((height * 56.5) / 33)
.translate([(width / 2), (height / 2)]);
//Path
path = d3.geo.path()
.projection(projection);
x = d3.scale.linear()
.domain([0, width])
.range([0, width]);
y = d3.scale.linear()
.domain([0, height])
.range([height, 0]);
svg = d3.select("#div_map").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);
g = svg.append("g");
// Zoom behavior
var zoom = d3.behavior.zoom()
.scaleExtent([1,15])
.on("zoom",function() {
g.selectAll("path.zoomable").attr("transform","translate("+d3.event.translate.join(",")+")scale("+d3.event.scale+")")
g.selectAll(".place").attr("transform", function(d) { p = projection(d.geometry.coordinates); return "translate(" + x(p[0]) + "," + y(p[1]) + ")"; });
}
);
svg.call(zoom);
d3.json("datos/ARcompleto.json.txt", function (error, ar) {
objMap = ar;
//Draw the map
provs = g.append("g")
.attr("id", "g_provincias")
.selectAll("path")
.data(topojson.feature(ar, ar.objects.provincias).features)
.enter().append("path")
.classed("zoomable", true)
.attr("d", path)
g.append("g")
.attr("id", "g_localidades")
.selectAll("path")
.data(topojson.feature(objMap, objMap.objects.localidades).features.filter(function (d) { return d.properties.LPROVINCIA == 'MENDOZA'; }))
.enter().append("path")
.attr("d", path)
.attr("class", "localidad")
.classed("place", true)
//.attr("transform", function(d) {return "translate(" + projection(d.geometry.coordinates.reverse()) + ")"; });
});
I can manage it with the Lars help. this is the new zoom function:
var zoom = d3.behavior.zoom()
.translate(projection.translate())
.scaleExtent([height, Infinity])
.scale(projection.scale())
.on("zoom", function() {
projection.translate(d3.event.translate).scale(d3.event.scale)
g.selectAll("path.zoomable").attr("d", path);
projection.translate(d3.event.translate).scale(d3.event.scale)
svg.selectAll(".place").attr("d", path);
});
Thanks a lot!