Bars in the grouped bar chart positioned correctly across x axis, however even thought I have a pre-defined range some of the bars are always beyond the chart with some y value equals to e.g. enormous -31199600.
CodeSandbox
// chart dimensions
const height = 600;
const colorScale = d3.scaleOrdinal().range(['#4FD8DD']);
const itemWidth = 140;
const barWidth = 3;
const barMargin = 5;
const xMargin = 80;
const yMargin = 150;
const svg = svgEl
.append('g')
.attr('class', 'grouped-bar-chart')
.attr('width', storeData.dataset.length * itemWidth + xMargin * 2)
.attr('height', height);
const xScale = d3
.scaleBand()
.domain(storeData.dataset.map((d) => d.city))
.range([0, storeData.dataset.length * itemWidth]);
const yScale = d3.scaleLinear().range([-120, height - 200]);
yScale.domain([d3.max(storeData.dataset[0].values, (d) => d.value), 0]);
// draw chart
svg
.selectAll('g.city')
.data(storeData.dataset, (d) => d.city)
.enter()
.append('g')
.classed('city', true)
.attr(
'transform',
(d) =>
`translate(${xMargin + xScale(d.city) + itemWidth / 2},${yMargin})`
)
.each(function (d) {
const city = d3.select(this);
for (let i = 0; i < d.values.length; i++) {
const y = yScale(d.values[i].value);
console.log(
'yScale(d.values[i].value) --->',
yScale(d.values[i].value)
);
console.log('yScale(0) --->', yScale(0));
const height = yScale(0) - y;
const x = (i - d.values.length / 2) * (barWidth + barMargin);
city
.append('rect')
.attr('x', x)
.attr('y', y)
.attr('width', barWidth)
.attr('height', height)
.style('fill', colorScale(i));
}
});
This code sets 5000 as the max value:
const maxValue = d3.max(storeData.dataset[0].values, (d) => d.value);
yScale.domain([maxValue, 0]);
Actually, max value is much higher (I get 400000000):
const maxValue = storeData.dataset.reduce((maxAll, item) => {
return item.values.reduce((maxItem, val) =>
Math.max(maxItem, val.value), maxAll);
}, 0);
console.log('MAX VALUE: ', maxValue);
Setting a correct value as domain upper limit for the yScale should solve the issue.
Related
I have a graph of lines paths .
Actually when I zoom it applies a transform attribute.
I would like to make a semantic zoom by zooming only on the x-axis.
Here's the code for zooming
private manageZoom(svgs: AllSvg, allAxis: AllAxis, dimension: Dimension): D3ZoomBehavior {
const zoom: D3ZoomBehavior = d3
.zoom()
.scaleExtent([1, 40])
.translateExtent([
[0, 0],
[dimension.width, dimension.height]
])
.on('zoom', zoomed.bind(null, allAxis));
svgs.svgContainer.call(zoom);
return zoom;
function zoomed({ xAxis, xAxisBottom, yAxis }: AllAxis, { transform }: any) {
svgs.sillons.attr('transform', transform);
xAxisBottom.axisContainer.call(xAxisBottom.axis.scale(transform.rescaleX(xAxisBottom.scale)) as any);
}
}
the sillons object is an array of paths + text + circles
I would that the lines get re-drawed in the right position as the x-axis get larger, but not zoom sillons on y-axis.
I have checked many posts but can't repoduce them to solve my issue. for example
When you set up something along the lines of
svg.call(
d3.zoom()
.on("zoom", zoom)
)
the zoom function can be just about anything that you want. The first argument of zoom is the zoom event itself. Let's denote it by evt. Then
evt.transform.k tells you the scale factor,
evt.transform.x tells you the horizontal translation, and
evt.transform.y tells you the vertical translation.
You don't have to use all of those, though. Rather, you can redraw your image however you want.
Here's a slightly cute example that rescales the image only horizontally.
let w = 500;
let h = 100;
let svg = d3
.select("#container")
.append("svg")
.attr("width", w)
.attr("height", h)
.style("border", "solid 1px black");
let n = 500;
let pts0 = d3.range(n).map((_) => [d3.randomNormal(w / 2, w / 20)(), 0]);
let pts1 = pts0.map((pt) => [w - pt[0], h]);
let g = svg.append("g");
let link_group = g.append("g");
link_group
.selectAll("path")
.data(d3.range(n))
.join("path")
.attr("d", (i) => d3.linkVertical()({ source: pts0[i], target: pts1[i] }))
.attr("fill", "none")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.1)
.attr("stroke-width", 1.5);
let all_pts = pts0.concat(pts1);
let circle_group = g.append("g");
let circles = circle_group
.attr("fill", "black")
.attr("fill-opacity", 0.2)
.selectAll("circle")
.data(all_pts)
.join("circle")
.attr("cx", (d) => d[0])
.attr("cy", (d) => d[1])
.attr("data-x", (d) => d[0])
.attr("r", 4);
svg.call(
d3
.zoom()
.scaleExtent([1 / 4, 20])
.duration(500)
.on("zoom", function (evt) {
let k = evt.transform.k;
link_group.selectAll("path").attr("d", function (i) {
let x00 = pts0[i][0];
let x01 = k * (x00 - w / 2) + w / 2;
let x10 = pts1[i][0];
let x11 = k * (x10 - w / 2) + w / 2;
return d3.linkVertical()({ source: [x01, 0], target: [x11, h] });
});
circle_group
.selectAll("circle")
.nodes()
.forEach(function (c) {
let x0 = c.getAttribute("data-x");
let k = evt.transform.k;
let x1 = k * (x0 - w / 2) + w / 2;
c.setAttribute("cx", x1);
});
})
);
<script src="https://d3js.org/d3.v7.min.js"></script>
<div id="container"></div>
I use d3js to draw a smooth curve. Then, I want to draw a point on the curve, but the point is random and I only have x value. I want get the function expression and get the y value with the x value. Is there any method to get the y value?
const line = d3.line()
.x(d => xScale(new Date(d.name)))
.y(d => yScale(d.value1))
.curve(d3.curveCatmullRom);
const series = svg.append('g')
.attr('transform', `translate(${grid.left},${grid.top})`)
.append('path')
.attr('d', line(data))
.attr('fill', 'transparent')
.attr('stroke-width', 2)
.attr('stroke', 'orange');
My current chart:
Here is a function that finds a point with specified x coordinate on a <path> (kind of binary search):
Note: The path should be monotonic on X (there must not be 2 points with the same x on the path)
const findPointAt = (path, x) => {
let from = 0;
let to = path.getTotalLength();
let current = (from + to) / 2;
let point = path.getPointAtLength(current);
while (Math.abs(point.x - x) > 0.5) {
if (point.x < x)
from = current;
else
to = current;
current = (from + to) / 2;
point = path.getPointAtLength(current);
}
return point;
}
const path = d3.select('path').node();
for (let x = 0; x <= 200; x += 50) {
const pos = findPointAt(path, x);
console.log(pos);
d3.select('svg').append('circle')
.attr('cx', pos.x)
.attr('cy', pos.y)
.attr('r', 3)
}
svg {
border: 1px solid gray;
}
path {
fill: none;
stroke: blue;
}
circle {
fill: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="200" height="150">
<path d="M 0,10 Q 40,0 90,80 C 120,120 150,70 220,20" />
</svg>
It's really a duplicate of this but I added a snippet as the post is several years old ...
const margin = 30;
const width = 400;
const height = 180;
const chartWidth = width - (margin * 2);
const chartHeight = height - (margin * 2);
const data = Array.from({length: 10}, (v, i) => {
return {
index: i,
value: Math.floor(Math.random() * 20) + 4
}
});
const svg = d3.select("#viz")
.append("svg")
.attr("width", width)
.attr("height", height);
const xScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.index))
.range([0, chartWidth]);
svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(${margin},${height - margin})`)
.call(d3.axisBottom(xScale));
const yScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.value))
.range([chartHeight, 0]);
svg.append("g")
.attr("class", "y-axis")
.attr("transform", `translate(${margin},${margin})`)
.call(d3.axisLeft(yScale));
const line = d3.line()
.x(d => xScale(d.index))
.y(d => yScale(d.value))
.curve(d3.curveCatmullRom);
const series = svg.append("g")
.attr("transform", `translate(${margin},${margin})`)
.append("path")
.attr("d", line(data))
.attr("fill", "transparent")
.attr("stroke-width", 2)
.attr("stroke", "orange");
const findYFromXLinearTime = (x, line) => {
const getXYAtLength = len => {
const pt = line.getPointAtLength(len);
return {x: pt.x, y: pt.y};
}
let l = 0;
while (getXYAtLength(l).x < x) l+=0.01;
return getXYAtLength(l).y;
}
const findYFromXLogTime = (x, line) => {
const error = 0.01;
const iterMax = 50;
let iter = 0;
let start = 0;
let end = line.getTotalLength();
let point = line.getPointAtLength((end + start) / 2);
while (x < point.x - error || x > point.x + error) {
// update middle
point = line.getPointAtLength((end + start) / 2);
// test
x < point.x ? end = (start + end) / 2 : start = (start + end ) / 2;
// update iteration
if (iterMax < ++ iter) break;
}
return point.y;
}
d3.select("#findY")
.on("click", evt => {
const x = document.getElementById("someX").value;
const y = findYFromXLogTime(xScale(x), series.node());
svg.append("circle")
.attr("cx", xScale(x) + margin)
.attr("cy", y + margin)
.attr("r", 4)
.attr("fill", "red")
.attr("stroke", "black")
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
<input id="someX" type="number">
<button id="findY" type="button">find Y</button>
<div id="viz"></div>
Link to the notebook.
I'm working on a small multiple line chart using d3.v5 on Observable, with the dataset structured like follows:
For visualization, the y scale takes num from the values array for the domain. There are several rows with unique key values, which I wanted to use to produce the small multiples. The image above shows the first key.
After visualizing the small multiple, I noticed that all the line charts are using the same y scale, which is not what I intended to do. This is what I currently have:
const y_scale = d3
.scaleLinear()
.domain([0, d3.max(series, d => d3.max(d.values, m => m.num))])
.range([width/2, width/2 - start_y - margin.bottom]);
Is there a way to adjust the domain so that each chart would have its own scale based on its own num values?
Edit 1: Notebook link added on top
The idiomatic D3 solution here would be using local variables. However, there are several different working alternatives.
For using local variables, we first declare them...
const localScale = d3.local();
const localLine = d3.local();
Then, we set the different scales in the "enter" selection:
var enter = my_group
.enter()
.append("g")
.attr("class", "chart_group")
.each(function(d) {
const yScale = localScale.set(this, d3
.scaleLinear()
.domain([0, d3.max(d.values, d => d.num)])
.range([panel_width / 2, panel_width / 2 - start_y - margin]));
localLine.set(this, d3
.line()
.x(d => x_scale(d.date))
.y(d => yScale(d.num)));
});
Finally, we get those scales:
sub_group
.select(".chart_line")
.attr("d", function(d) {
return localLine.get(this)(d)
})
Here is the whole cell, copy/paste this in your notebook, replacing your cell:
chart = {
const panels_per_row = 4;
const panel_width = (width - margin * 8) / panels_per_row;
const height =
margin + (panel_width + margin) * (parseInt(my_data.length / 2) + 1);
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
const start_x = 2;
const start_y = panel_width / 3 + margin;
const x_scale = d3
.scaleBand()
.domain(d3.set(series[0].values, d => d.date).values())
.range([0, panel_width]);
const localScale = d3.local();
const localLine = d3.local();
//join
var my_group = svg.selectAll('.chart_group').data(series, d => d.key);
//exit and remove
my_group.exit().remove();
//enter new groups
var enter = my_group
.enter()
.append("g")
.attr("class", "chart_group")
.each(function(d) {
const yScale = localScale.set(this, d3
.scaleLinear()
.domain([0, d3.max(d.values, d => d.num)])
.range([panel_width / 2, panel_width / 2 - start_y - margin]));
localLine.set(this, d3
.line()
.x(d => x_scale(d.date))
.y(d => yScale(d.num)));
});
//append elements to new group
enter.append("rect").attr("class", "group_rect");
enter.append("text").attr("class", "group_text");
enter.append("g").attr("class", "sub_chart_group");
//merge
my_group = my_group.merge(enter);
position_group_elements(my_group);
//join
var sub_group = my_group
.select(".sub_chart_group")
.selectAll('.sub_chart_elements_group')
.data(d => [d.values]); // data is wrapped in an array because this is a line/area chart
//exit and remove
sub_group.exit().remove();
//enter new groups
var sub_enter = sub_group
.enter()
.append("g")
.attr("class", "sub_chart_elements_group");
//append elements to new group
sub_enter.append("path").attr("class", "chart_line");
//merge
sub_group = sub_group.merge(sub_enter);
sub_group
.select(".chart_line")
.attr("d", function(d) {
return localLine.get(this)(d)
})
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-width", 1)
.attr("transform", "translate(" + start_x + "," + start_y + ")");
function position_group_elements(my_group) {
//position rectangle
my_group
.select(".group_rect")
.attr("x", function(d, i) {
//two groups per row so
var position = i % panels_per_row;
d.x_pos = position * (panel_width + margin) + margin;
d.y_pos =
parseInt(i / panels_per_row) * (panel_width + margin) + margin;
return d.x_pos;
})
.attr("y", d => d.y_pos)
.attr("fill", "#eee")
.attr("stroke", "#aaa")
.attr("stroke-width", 1)
.attr("width", panel_width)
.attr("height", panel_width);
//then position sub groups
my_group
.select(".sub_chart_group")
.attr("id", d => d.key)
.attr("transform", d => "translate(" + d.x_pos + "," + d.y_pos + ")");
}
return svg.node();
}
I am completely new to Java Script and D3. I'm trying to rotate the values in the bar chart by 90 degrees. However, I cannot rotate them individually. I've tried many different ways to approach this issue but have had no luck. Below is the code.
const render = data => {
const chartTitle = 'Top 10 Most Populous Countries';
const yAxisLabel = 'Population';
const xAxisLabel = 'Country';
const margin = {
top: 100,
right: 50,
bottom: 150,
left: 150
};
// Dimensions: 400 x 400
// used for the initial rendering
// width to height proportion
// its preserved as the chart is resized
const width = 1200 - margin.left - margin.right;
const height = 600 - margin.top - margin.bottom;
console.log(width);
console.log(height);
const xValues = d => d.country;
const yValues = d => d.population;
const yScaleMaxValue = Math.ceil((d3.max(data, yValues)));
console.log(yScaleMaxValue);
const yScaleMinValue = Math.ceil((d3.min(data, yValues)));
console.log(yScaleMinValue);
const xScale = d3.scaleBand().
paddingInner(0.2).
domain(data.map(xValues)).
range([0, width]);
const xAxis = d3.axisBottom(xScale);
const yScale = d3.scaleLinear()
.domain([0, yScaleMaxValue])
.range([height, 0])
;
const yAxisTickFormat = number => d3.format('.3s')(number)
.replace('G','B')
.replace('0.00','0');
const yAxis = d3.axisLeft(yScale).tickFormat(yAxisTickFormat);
const yValuesTextFormat = number => d3.format('.3s')(number);
const svg = d3.select('#responsivechart3').
append('svg').
attr('width', width + margin.left + margin.right).
attr('height', height + margin.top + margin.bottom).
call(responsivefy) // Call responsivefy to make the chart responsive
.append('g').
attr('transform', `translate(${margin.left}, ${margin.top})`);
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('class', 'rect')
.attr('x', d => xScale(xValues(d)))
.attr('y', d => yScale(yValues(d)))
.attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(yValues(d)))
;
svg.selectAll('text')
.data(data)
.enter()
.append('text')
.attr('class', 'bar-value')
.attr('x', d => xScale(xValues(d)))
.attr('y', d => yScale(yValues(d)))
.text(yValues)
.attr('transform', `rotate(-45)`);
svg.append('g').
call(yAxis);
svg.append('g')
.attr('transform', `translate(0, ${height})`)
.call(xAxis)
.selectAll('text')
.call(wrap, xScale.bandwidth());
svg.append('text')
.attr('class','chart-title')
.attr('y',-50)
.attr('x',100)
.attr('fill', 'black')
.text(chartTitle);
svg.append('text')
.attr('class','axis-label')
.attr('y',450)
.attr('x',400)
.attr('fill', 'black')
.text(xAxisLabel);
svg.append('text')
.attr('class','axis-label')
.attr('y',-100)
.attr('x',-250)
.attr('fill', 'black')
.text(yAxisLabel)
.attr('transform', `rotate(-90)`);
function responsivefy(svg) {
// Container is the DOM element, svg is appended.
// Then we measure the container and find its
// aspect ratio.
const container = d3.select(svg.node().parentNode),
width = parseInt(svg.style('width'), 10),
height = parseInt(svg.style('height'), 10),
aspect = width / height;
// Add viewBox attribute to set the value to initial size
// add preserveAspectRatio attribute to specify how to scale
// and call resize so that svg resizes on page load
svg.attr('viewBox', `0 0 ${width} ${height}`).
attr('preserveAspectRatio', 'xMinYMid').
call(resize);
d3.select(window).on('resize.' + container.attr('id'), resize);
function resize() {
const targetWidth = parseInt(container.style('width'));
svg.attr('width', targetWidth);
svg.attr('height', Math.round(targetWidth / aspect));
}
}
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width+25) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text
.append("tspan")
.attr("x", 0)
.attr("y", y)
.attr("dy", ++lineNumber * lineHeight + dy + "em")
.text(word);
}
}
});
}
};
data = d3.csv('data.csv').then(data => {
data.forEach(d => {
d.population = +d.population * 1000;
});
console.log(data);
render(data);
});
Any help on this would be deeply appreciated.
Ive also attached an image.
Rotated Values
I am trying to add a tooltip for my dual line chart graph.
However, instead of using timeScale or scaleLinear, I used scalePoint to graph my chart.
I am trying to achieve the following effect:
https://bl.ocks.org/mbostock/3902569
this.x = d3.scalePoint().range([ this.margin.left, this.width - this.margin.right ]);
this.xAxis = d3.axisBottom(this.x);
this.x.domain(
this.dataArray.map(d => {
return this.format(d[ 'year' ]);
}));
Here is my mouseover function,
function mousemove() {
//d3.mouse(this)[ 0 ]
//x.invert
var x0 = d3.mouse(this)[ 0 ],
i = bisectDate(data, x0, 1),
d0 = data[ i - 1 ],
d1 = data[ i ],
d = x0 - d0.year > d1.year - x0 ? d1 : d0;
console.log(x0);
// focus.attr("transform", "translate(" + x(format(d.year)) + "," + y(d.housing_index_change) + ")");
// focus.select("text").text(d.housing_index_change);
}
Since I am using scalePoint, there is obviously no invert function to map the coordinates to my data. and I am only retrieving the first element in the array and it is the only one that is being display regardless of the position of the mouse.
So my question is, how can I implement the invert functionality here while still using scalePoint?
Thank you :)
You are right, there is no invert for a point scale. But you can create your own function to get the corresponding domain of a given x position:
function scalePointPosition() {
var xPos = d3.mouse(this)[0];
var domain = xScale.domain();
var range = xScale.range();
var rangePoints = d3.range(range[0], range[1], xScale.step())
var yPos = domain[d3.bisect(rangePoints, xPos) -1];
console.log(yPos);
}
Step by step explanation
First, we get the x mouse position.
var xPos = d3.mouse(this)[0];
Then, based on your scale's range and domain...
var domain = xScale.domain();
var range = xScale.range();
...we create an array with all the steps in the point scale using d3.range:
var rangePoints = d3.range(range[0], range[1], xScale.step())
Finally, we get the corresponding domain using bisect:
var yPos = domain[d3.bisect(rangePoints, xPos) -1];
Check the console.log in this demo:
var data = [{
A: "groupA",
B: 10
}, {
A: "groupB",
B: 20
}, {
A: "groupC",
B: 30
}, {
A: "groupD",
B: 10
}, {
A: "groupE",
B: 17
}]
var width = 500,
height = 200;
var svg = d3.selectAll("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var color = d3.scaleOrdinal(d3.schemeCategory10)
.domain(data.map(function(d) {
return d.A
}));
var xScale = d3.scalePoint()
.domain(data.map(function(d) {
return d.A
}))
.range([50, width - 50])
.padding(0.5);
var yScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {
return d.B
}) * 1.1])
.range([height - 50, 10]);
var circles = svg.selectAll(".circles")
.data(data)
.enter()
.append("circle")
.attr("r", 8)
.attr("cx", function(d) {
return xScale(d.A)
})
.attr("cy", function(d) {
return yScale(d.B)
})
.attr("fill", function(d) {
return color(d.A)
});
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale);
svg.append("g").attr("transform", "translate(0,150)")
.attr("class", "xAxis")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(50,0)")
.attr("class", "yAxis")
.call(yAxis);
svg.append("rect")
.attr("opacity", 0)
.attr("x", 50)
.attr("width", width - 50)
.attr("height", height)
.on("mousemove", scalePointPosition);
function scalePointPosition() {
var xPos = d3.mouse(this)[0];
var domain = xScale.domain();
var range = xScale.range();
var rangePoints = d3.range(range[0], range[1], xScale.step())
var yPos = domain[d3.bisect(rangePoints, xPos) - 1];
console.log(yPos);
}
.as-console-wrapper { max-height: 20% !important;}
<script src="https://d3js.org/d3.v4.min.js"></script>