I'm using d3js to draw a chart which plots two data series as two lines.
However, parts of the bottom line (the blue line) are obscured:
Hiding either line by adding display: none in the browser's devel tools shows both lines fully rendered.
The rendered SVG looks like this (sorry for the picture, hard to copy the text):
Each path is created by its own D3 function because the vertical scales are different:
var theLineFcnA = d3.line()
.x(function (d) { return xScaleT(d.t); })
.y(function (d) { return yScaleA(d.v); });
var theLineFcnB = d3.line()
.x(function (d) { return xScaleT(d.t); })
.y(function (d) { return yScaleB(d.v); });
And called like this:
function plotLineA(plotData)
{
if (theSVG === null)
return;
// plot it
var theALine = theSVG.select('.lineChart').select("path.lineA");
theALine.data([plotData]);
theSVG.select("g.x.axis").call(xAxis);
theSVG.select("g.y.axisA").call(yAxisA);
theSVG.select("path.lineA").attr("d", theLineFcnA);
}
(there is a similar function for line B)
Any idea on how to fix this? I've fiddled around with various CSS properties on the line but not sure what else to do.
Many thanks
I suppose you set the width of the bottom curve (which should be the first path laid down) to be thicker than the that of the top curve. Here's an example:
let N = 12;
let n = 5;
let cur = 0;
let pts1 = d3.range(N).map(function (x) {
let step = 2 * d3.randomInt(0, 2)() - 1;
cur = cur + step;
return [x, cur];
});
cur = pts1[n - 1][1];
let pts2 = pts1.slice(0, n);
pts2 = pts2.concat(
d3.range(n, N).map(function (x) {
let step = 2 * d3.randomInt(0, 2)() - 1;
cur = cur + step;
return [x, cur];
})
);
let [ymin, ymax] = d3.extent(pts1.concat(pts2).map((d) => d[1]));
let width = 500;
let w = d3.min([800, width]);
let h = 0.625 * w;
let pad = 20;
let x_scale = d3
.scaleLinear()
.domain([0, N])
.range([pad, w - pad]);
let y_scale = d3
.scaleLinear()
.domain([ymin, ymax])
.range([h - pad, pad]);
let pts_to_path = d3
.line()
.x((d) => x_scale(d[0]))
.y((d) => y_scale(d[1]));
let svg = d3.select('#container')
.append("svg")
.attr("width", w)
.attr("height", h);
svg
.selectAll("path")
.data([pts1, pts2])
.join("path")
.attr("d", pts_to_path)
.attr("stroke", (_, i) => d3.schemeCategory10[i])
.attr("stroke-width", (_, i) => (i == 0 ? 6 : 2))
.attr("fill", "none")
svg
.append("g")
.attr("transform", `translate(0, ${h / 2})`)
.call(d3.axisBottom(x_scale));
svg
.append("g")
.attr("transform", `translate(${pad})`)
.call(d3.axisLeft(y_scale).ticks(4));
<script src="https://cdn.jsdelivr.net/npm/d3#7"></script>
<script src="https://cdn.jsdelivr.net/npm/#observablehq/plot#0.6"></script>
<div id="container"></div>
Related
Hi I have a line graph but the point indicated by the line aren't aligned with the x axis points, they seem to be in-between the points.
Anyone know that the cause could be and how to fix it?
I have added my code for this graph below. Please note that the code below was originally used for a bar chart. I am just trying to add lines to the bar chart. Therefore some bits of code may not seem relevant.
Thank you in advance!
let barWidth;
let labelLengths = [];
const response = _.cloneDeep(payloadResponse);
const d3ChartData = [];
const horizontal = options.horizontal
response.data.forEach(m => {
m.metric0 = parseFloat(m.metric0);
});
const total = _.sumBy(response.data, 'metric0')
const { percentValues } = options;
const metrics = [];
response.metrics.forEach(m => {
metrics.push({ key: Object.keys(m)[0], name: Object.values(m)[0] });
});
const dimensions = [];
response.dims.forEach(d => {
dimensions.push({ key: Object.keys(d)[0], name: Object.values(d)[0] });
});
response.data.forEach(row => {
const newobject = {};
newobject["category"] = row[dimensions[0].key];
newobject["values"] = row[metrics[0].key];//Y axis metrics
metrics.forEach((s, i) => {
row[metrics[i].key] = percentValues ? percentage(row[metrics[i].key] / total) : row[metrics[i].key]
newobject[s.name] = row[metrics[i].key]
})
d3ChartData.push(newobject)
})
// List of groups = species here = value of the first column called group -> show them on the X axis
const groups = d3.map(d3ChartData, function (d) { return d["category"] }).keys();
const valueGroups = d3.map(d3ChartData, function (d) { return d["values"] }).keys();//Y axis group
subgroups = d3.map(metrics, function (d) { return d["name"] }).keys();
subgroups = _.sortBy(subgroups);
const newdataset = d3ChartData;
const maxarray = [];
metrics.forEach((s, i) => {
maxarray.push((Math.max.apply(Math, response.data.map(function (o) {
return Math.round(o[metrics[i].key]);
}))))
})
// set the dimensions and margins of the graph
let max = Math.max.apply(Math, maxarray)
console.log("max","",max);
max = max * 1.1;
let longestLabel = Math.max.apply(Math, maxarray).toString().length;
if(longestLabel < 5)
{
longestLabel = 5;
}
let chartBottom = Math.max(...(groups.map(el => el.length)));
let longestValue = Math.max(...(valueGroups.map(el => Math.trunc(el).toString().length)));
let ySpacing = longestValue*10 >= 60 ? longestValue*10 == 70 ? longestValue*( (10 + Math.ceil(longestValue/3))+3) : longestValue*( (10 + Math.ceil(longestValue/3) )) : 80;
let margin = { top: 10, right: 20, bottom: horizontal ? ySpacing<150 ? ySpacing : 130 : chartBottom < 14 ? 100 : (chartBottom * 8), left: !horizontal ? ySpacing : chartBottom < 14 ? 100 : (chartBottom * 8)},
width = 340,
height = 310;
// append the svg object to the body of the page
let svg = d3.select('#result-chart-' + response.chartNo)
.append("svg")
.attr("id", "svg-chart-" + response.chartNo)
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
let sizeDimensions = {
margin: margin,
height: height,
width: width
}
let svgCanvas = d3.select('#svg-chart-' + response.chartNo)
let x, y;
// Add X axis
x = d3.scaleBand()
.domain(groups)
.range([0, width])
// Add Y axis
y = d3.scaleLinear()
.domain([0, max])
.range([height, 0]);
let makeYLines = () => d3.axisRight()
.scale(y);
svg.append('g')
.attr('transform', `translate(${width}, 0)`)
.attr("class", "gridlines")
.call(makeYLines().tickSize(-width, 0, 0).tickFormat(''));
svg.append("g")
.call(d3.axisLeft(y).tickSizeOuter(0))
.selectAll("text")
.attr("class", "axis-text")
//.attr("transform", function (d) {return !horizontal ? "rotate(0)" : "rotate(-30)"})
.attr("text-anchor", "end");
// Another scale for subgroup position?
let xSubgroup = d3.scaleBand()
.domain(subgroups)
.range([0, (!horizontal ? x.bandwidth() : y.bandwidth())])
.padding([0.05])
// color palette = one color per subgroup
let color = d3.scaleOrdinal()
.domain(subgroups)
.range(_config.theme)
let tooltip = d3.select('#chatbot-message-container')
.append("div")
.attr("class", "toolTip")
.style("border-color", _config.theme[0]);
//============================================
//line chart start
let line = d3.line()
.x(function(d) { return x(d.category)})
.y(function(d) { return y(d.values) })
svg.append("path")
.datum(newdataset)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", line)
let line2 = d3.line()
.x(function(d) { return x(d.category)})
.y(function(d) { return y(d.Leavers) })
svg.append("path")
.datum(newdataset)
.attr("fill", "none")
.attr("stroke", "red")
.attr("stroke-width", 1.5)
.attr("d", line2)
//============================================
//line chart end
//gets width of each bar
svg.selectAll('rect') // select all the text elements
.each(function(d){ barWidth = this.getBBox().width})
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickSizeOuter(0))
.selectAll("text")
.each(function(d){ labelLengths.push(this.getBBox().width)})
.attr("transform", function (d) {return horizontal ? percentValues ? "rotate(0)" : longestValue < 2 ? "rotate(0)" : "rotate(270)" : barWidth>Math.max(...labelLengths) ? "rotate(0)" : "rotate(270)"})
.attr("y", function (d) {return horizontal ? percentValues ? 8 : longestValue < 2 ? 8 : -4 : barWidth>Math.max(...labelLengths) ? 8 : -4})
.attr("x", function (d) {return horizontal ? percentValues ? 0 : longestValue < 2 ? 0 : -7 : barWidth>Math.max(...labelLengths) ? 0 : -7})
.attr("class", "axis-text x-axis-text")
.attr("text-anchor", function (d) {return horizontal ? percentValues ? "middle" : longestValue < 2 ? "middle" : "end" : barWidth>Math.max(...labelLengths) ? "middle" : "end"});
// Animation
svg.selectAll("rect")
.transition()
.duration(1000)
.ease(d3.easePoly)
.attr(!horizontal ? "y" : "x", function (d) {
return !horizontal ? y(d.value) : x(0);
})
.attr(!horizontal ? "height" : "width", function (d) {
return !horizontal ? (height - y(d.value)) : (x(d.value))
})
.delay(function (d, i) { return (i * 30) })
if (metrics.length > 1) {
buildLegend(svgCanvas, color, subgroups, sizeDimensions, 15, tooltip, 'result-chart-' + response.chartNo, response.chartNo);
} else {
buildAxisNames(svgCanvas, metrics, options, sizeDimensions);
}
UPDATE:
I found some sources mentioning using scalePoint, this worked perfectly for the line but broke the bars when reintroducing the code responsible for drawing them. I can only assume that this is because bar charts need to used scaleBand() ?
I then looked into changing the paddingInner/outer after finding this great resource on scaleBand() https://observablehq.com/#d3/d3-scaleband
Changing the padding as so seemed to work great for the line:
x = d3.scaleBand()
.domain(groups)
.range([0, width])
.paddingOuter(0.2)
.paddingInner(0.9)
But when adding the bars back in they were also changed:
The bars should actually look like this:
My next thought is to introduce a second x variable, eg: x2 and set a different paddingInner/outer for that so that both the line an d bars are using different x's. Is this the correct approach?
I understand that it may be difficult help/debug with this issue as I havened added a jsfiddle. This is simple due to me not having access to the data, I will see if I can hardcode some example data.
Until then I hope someone can help with this.
UPDATE: JsFiddle with code found here https://jsfiddle.net/MBanski/0ke5u3qy/1/
My issue is that the animation works fine when the images have no content to them as below, but once they are loaded I get the following error: Error: <image> attribute x: Expected length, "NaN".
Here is a code snippet:
var data = ['https://cdn.britannica.com/60/8160-050-08CCEABC/German-shepherd.jpg', 'https://cdn.britannica.com/60/8160-050-08CCEABC/German-shepherd.jpg', 'https://cdn.britannica.com/60/8160-050-08CCEABC/German-shepherd.jpg', 'https://cdn.britannica.com/60/8160-050-08CCEABC/German-shepherd.jpg', 'https://cdn.britannica.com/60/8160-050-08CCEABC/German-shepherd.jpg'];
var w = window,
d = document,
e = d.documentElement,
g = d.getElementsByTagName('body')[0],
wid = 400
y = 400;
var svg = d3.select("body").append("svg")
.attr("width", wid)
.attr("height", "400")
.on('mousemove', () => {
let x = event.x - 20;
d3.selectAll('.content')
.attr('x', (d, i) => fisheye(d, x))
})
.on('mouseleave', () => {
d3.selectAll('.content').transition().attr(
'x', (d, i) => xScale(i))
})
var chart = svg.append('g')
.classed('group', true)
let xScale = d3.scaleBand().domain(d3.range(5)).range([0, wid]).padding(0)
let rects = svg.selectAll('content')
.data(
d3.range(5)
//data //(uncomment this, and comment line above to try loading images)
)
rects.exit().remove();
rects.enter()
.append("svg:image")
.attr("xlink:href", d => d)
.attr("class", "content")
.attr("y", 0)
.attr("x", (d, i) => xScale(i))
.attr("width", "300px")
.style("opacity", 1)
.attr("stroke", "white")
.style('fill', 'rgb(81, 170, 232)')
.attr("height", 400);
let distortion = 10;
function fisheye(_, a) {
let x = xScale(_),
left = x < a,
range = d3.extent(xScale.range()),
min = range[0],
max = range[1],
m = left ? a - min : max - a;
if (m === 0) m = max - min;
return (left ? -1 : 1) * m * (distortion + 1) / (distortion + (m / Math.abs(x - a))) + a;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
I'm trying to make something that looks like this:
https://www.nytimes.com/newsgraphics/2013/09/13/fashion-week-editors-picks/index.html
I would really appreciate some help on getting that snippet to work and if possible as smoothly as this website link! That was made by the Bosstock himself and I'm a d3 newbie so I'm quite out of my depth.
I figured out my issue-
let xScale = d3.scaleBand().domain(data).range([0,wid]).padding(0)
The domain was d3.range(5) rather than my data variable.
Using D3, I want to take the data visualization type of a classical heatmap...
.. onto a compartmentalized version of several heatmap groups drawing data from a single data source.
Technically this should be one heatmap element drawing its data from a single source - separation and thus clustering/grouping is supposed to happen through sorting the data in the *.csv file (group one, group two, group three..) and the D3 *.JS file handling the styling.
While generating a single map:
// Build X scales and axis:
const x = d3.scaleBand()
.range([0, width])
.domain(myGroups)
.padding(0.00);
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x));
// Build Y scales and axis:
const y = d3.scaleBand()
.range([height, 0])
.domain(myVars)
.padding(0.00);
svg.append('g')
.call(d3.axisLeft(y));
assigning a color:
// Assign color scale
const myColor = d3.scaleLinear()
.range(['red', '#750606'])
.domain([1, 100]);
and fetching (sample) data:
// Read the data
d3.csv('https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/heatmap_data.csv', (data) => {
data.sort(function(a, b) {
return myVars.indexOf(b.variable) - myVars.indexOf(a.variable) || myGroups.indexOf(a.group) - myGroups.indexOf(b.group)
});
Has been working like a charm:
CodePen
I'm struggling to expand this basic structure onto the generation of multiple groups as described above. Expanding the color scheme, trying to build several additional X and Y axis that cover different ranges result in a complete break of the D3 element rendering the map unable to be displayed at all.
Can someone point me in the right direction on how to generate multiple heatmap groups without breaking the heatmap?
I was able to solve the compartmentalization using a row and column based procedure to construct the compartments:
// Dimensions
const numCategoryCols = 4;
const numCategoryRows = Math.ceil(grouped.length / numCategoryCols);
const numEntryCols = 3;
const numEntryRows = Math.ceil(grouped[0].values.length / numEntryCols);
const gridSize = 20;
const width = gridSize * numCategoryCols * numEntryCols;
const height = gridSize * numCategoryRows * numEntryRows;
const tooltipArrowSize = 8;
// Containers
const container = d3
.select("#" + containerId)
.classed("heatmap-grid", true)
.style("position", "relative");
const svg = container
.append("svg")
.style("display", "block")
.style("width", "100%")
.attr("viewBox", [0, 0, width, height])
.style("opacity", 0);
svg.transition()
.duration(3000)
.delay((d,i) => i*200)
.style("opacity", 1)
// Heatmap
const gCategory = svg
.selectAll(".category-g")
.data(grouped, (d) => d.key)
.join("g")
.attr("class", "category-g")
.attr("fill", (d) => color(d.key))
.attr("transform", (_, i) => {
const y = Math.floor(i / numCategoryCols);
const x = i % numCategoryCols;
return `translate(${gridSize * numEntryCols * x},${
gridSize * numEntryRows * y
})`;
});
const gEntry = gCategory
.selectAll(".entry-g")
.data((d) => d.values)
.join("g")
.attr("class", "entry-g")
.attr("transform", (_, i) => {
const y = Math.floor(i / numEntryCols);
const x = i % numEntryCols;
return `translate(${gridSize * x},${gridSize * y})`;
});
const entry = gEntry
.append("rect")
.attr("width", gridSize)
.attr("height", gridSize)
.attr("fill-opacity", (d) => d.Severity / 100)
.on("mouseenter", showTooltip)
.on("mouseleave", hideTooltip);
I'm attempting to make a stream graph in d3 that is both zoomable and the paths are hoverable.
From reading around I see that as of v4 d3 zoom consumes the event and therefore may be why my mouseover event no longer fires, however no amount of reordering or pointer-events: all I set seems to have an effect.
Is anybody able to help me understand what I have to do to get both my zoom and hover working in the following example? (There's also a codesandbox with the example)
const width = 500;
const height = 500;
let numberOfDataPoints = 5;
const numberOfLayers = 3;
let data = [];
for (let i = 0; i < numberOfDataPoints; i++) {
let point = [];
for (let j = 0; j < numberOfLayers; j++) {
point.push(Math.floor(Math.random() * Math.floor(120)));
}
data.push(point);
}
const x = d3
.scaleLinear()
.domain([0, numberOfDataPoints - 1])
.range([0, width]);
const y = d3.scaleLinear().range([height, 0]);
const area = d3
.area()
.x((d, i) => x(i))
.y0(d => y(d[0]))
.y1(d => y(d[1]));
const stack = d3
.stack()
.keys(d3.range(numberOfLayers))
.offset(d3.stackOffsetWiggle)
.order(d3.stackOrderNone);
let layers = stack(data);
y.domain([
d3.min(layers, l => d3.min(l, d => d[0])),
d3.max(layers, l => d3.max(l, d => d[1]))
]);
update();
const zoomContainer = d3
.select('svg')
.append('rect')
.attr('width', width)
.attr('height', height)
.attr('fill', 'none')
.style('pointer-events', 'all');
const zoom = d3
.zoom()
.scaleExtent([1, 8])
.on('zoom', zoomed);
zoomContainer.call(zoom);
function update() {
let path = d3
.select('svg')
.selectAll('path')
.data(layers);
path
.enter()
.append('path')
.attr('fill', 'red')
.merge(path)
.attr('d', d => area(d))
.on('mouseover', () => {
d3.select('svg')
.selectAll('path')
.attr('fill', 'red');
d3.select(d3.event.target).attr('fill', 'green');
});
}
function zoomed() {
let transform = d3.event.transform;
transform.y = 0;
let updatedScale = transform.rescaleX(x);
d3.select('svg')
.selectAll('path')
.attr('d', area.x((d, i) => updatedScale(i)));
}
The hypothesis for why this didn't work was correct (the zoom container consumes the event), and the fix is to just remove the zoomContainer and apply the zoom to the svg instead i.e.
const zoom = d3
.zoom()
.on('zoom', zoomed);
d3.select('svg').call(zoom);
I have included a legend to my Heat Map following this example, built with the threshold scale, and a Linear scale for its axis. The axis is created without issue, the problem is, I believe, in the code where I'm appending its rect elements.
Note: heatMap is a svg element.
The D3.js library version I'm using is D3.v5, in case this might be relevant to mention.
function (dataset,xScale,yScale,heatMap,w,h,p,colorScale,colorDomain)
{
let variance = dataset.monthlyVariance.map(function(val){
return val.variance;
});
const legendColors= [0,1.4,2.8,5.2,6.6,8,9.4,10.8,12.2,13.6].map((n)=> colorScale(n));
const minTemp = dataset.baseTemperature + Math.min.apply(null, variance);
const maxTemp = dataset.baseTemperature + Math.max.apply(null, variance);
const legendDomain= ((min,max,len)=>
{
let arr= [];
let step=(max-min)/len;
for (let i=0; i<len; i++)
{
let eq=min+i*step;
arr.push(eq.toFixed(2));
}
return arr;
})(minTemp,maxTemp,legendColors.length);
//The legend scaling threshold
let threshold= d3.scaleThreshold()
.domain(legendDomain)
.range(legendColors);
//Legend's axis scaling
let legendBar= d3.scaleLinear()
.domain([minTemp,maxTemp])
.range([0,w/3]);
**//Legend's axis**
let legendAxis= d3.axisBottom(legendBar)
.tickSize(6)
.tickValues(threshold.domain())
.tickFormat(d3.format(".1f"));
//append legend's element
let legendEle= heatMap.append("g")
.attr("id","legend")
.classed("legend",true)
.attr("transform", `translate(62,${h-p.bottom+60})`);
//Map legend
legendEle.append("g")
.selectAll("rect")
.data(threshold.range().map(function(color){
let d = threshold.invertExtent(color);
if(d[0] == null) d[0] = legendBar.domain()[0];
if(d[1] == null) d[1] = legendBar.domain()[1];
return d;
}))
.enter()
.append('rect')
.attr('x',(d)=> legendAxis(d[0]))
.attr('y', 0)
.attr('width', (d)=> legendAxis(d[1])-legendAxis(d[0]))
.attr('height', 20)
.attr("fill", function(d) { return threshold(d[0]); });
//append legend's axis
legendEle.append("g").call(legendAxis);
//rect grid map
heatMap.selectAll("rect")
.data(dataset.monthlyVariance)
.enter()
.append("rect")
.attr("class","cell")
.attr('data-month',function(d){
return d.month;
})
.attr('data-year',function(d){
return d.year;
})
.attr('data-temp',function(d){
return dataset.baseTemperature + d.variance;
})
.attr('x', (d)=> xScale(d.year))
.attr('y', (d)=> yScale(d.month))
.attr("width", function(d,i){
return 5;
})
.attr("height", function(d,i){
return yScale.bandwidth(d.month);
})
.attr("fill", (d)=> colorScale(dataset.baseTemperature + d.variance))
.attr("transform", `translate(62,0)`);
}