Image is not showing in appended d3 gantt chart - javascript

I am trying to append some image in Gantt chart horizontal bar but it's not showing. But tags getting appended inside rect tag.
here is the code
const start_date = "2020-01-15";
const end_date = "2020-05-05";
const MARGIN = {
left: 50,
right: 50,
top: 50,
bottom: 50
}
const svg = d3.select('.graph').append('svg')
// console.log(svg)
const width = 800
const height = 600
const ticks = ['task1', 'task2', 'task3', 'task4', 'task5', 'task6'];
const x = d3.scaleTime().domain([new Date(start_date), new Date(end_date)]).range([0, (width - (MARGIN.left + MARGIN.right))])
const y = d3.scaleBand()
.domain(ticks)
.range([0, 400])
// .range([0, 50,100, 150, 200, 250, 300, 350,height])
const X = v => {
const minDate = d3.min(v, d => new Date(d.initDate))
const maxDate = d3.max(v, d => new Date(d.endDate))
return d3.scaleUtc().domain([minDate, maxDate]).range([MARGIN.right, width - MARGIN.left]);
}
const x_axis = d3.axisTop(x).ticks(6);
const y_axis = d3
.axisLeft(y)
.ticks(6)
.tickFormat(function(d, i){return ticks[i]})
.tickSize(15);
svg.attr('width', width).attr('height', height).attr('viewBox', `0 0 700 800`)
let chart = svg.append("g").attr('class', 'chart-holder').attr('transform', `translate(50,50)`);
chart
.append("g")
.attr('class', 'x axis')
.attr("transform", "translate(0,50)")
.call(x_axis);
chart
.append("g")
.attr('class', 'y axis')
.attr("transform", "translate(0, 50)")
.call(y_axis);
chart
.selectAll('rect')
.data([...this.state.data])
.enter()
.append('rect')
.attr('height', 40)
.attr('x', function(d){
return x(new Date(d.initDate))
})
.attr('y', function(d, i){
return y(d.name);
})
.attr('width', function(d, i){
return x(new Date(d.endDate)) - x(new Date(d.initDate))
})
.attr('rx', 17)
.attr('ry', 17)
.attr('fill', "blue")
.attr('transform', `translate(50, 60)`)
.append('image')
.attr("src", "https://miro.medium.com/max/1000/1*tv9pIQPhwumDnYBfCoapYg.jpeg")
.attr("width", 30)
.attr("height", 30)
and things I have already tried
xlink:href instead of href
svg:image instead of image

You cannot append under . Replace with , then append and under
const items =
chart.selectAll('g.item').data(...).enter().append('g').classed('item', true);
items.append('rect')...
items.append('image')...

Related

d3js beeswarm with force simulation

I try to do a beeswarm plot with different radius; inspired by this code
The issue I have, is that my point are offset regarding my x axis:
The point on the left should be at 31.7%. I don't understand why, so I would appreciate if you could guide me. This could be improved by changing the domain of x scale, but this can't match the exact value; same issue if I remove the d3.forceCollide()
Thank you,
Data are available here.
Here is my code:
$(document).ready(function () {
function tp(d) {
return d.properties.tp60;
}
function pop_mun(d) {
return d.properties.pop_mun;
}
var margin = {top: 20, right: 20, bottom: 20, left: 40},
width = 1280 - margin.right - margin.left,
height = 300 - margin.top - margin.bottom;
var svg = d3.select("body")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var z = d3.scaleThreshold()
.domain([.2, .3, .4, .5, .6, .7])
.range(["#35ff00", "#f1a340", "#fee0b6",
"#ff0000", "#998ec3", "#542788"]);
var loading = svg.append("text")
.attr("x", (width) / 2)
.attr("y", (height) / 2)
// .attr("dy", ".35em")
.style("text-anchor", "middle")
.text("Simulating. One moment please…");
var formatPercent = d3.format(".0%"),
formatNumber = d3.format(".0f");
d3.json('static/data/qp_full.json').then(function (data) {
features = data.features
//1 create scales
var x = d3.scaleLinear()
.domain([0, d3.max(features, tp)/100])
.range([0, width - margin.right])
var y = d3.scaleLinear().domain([0, 0.1]).range([margin.left, width - margin.right])
var r = d3.scaleSqrt().domain([0, d3.max(features, pop_mun)])
.range([0, 25]);
//2 create axis
var xAxis = d3.axisBottom(x).ticks(20)
.tickFormat(formatPercent);
svg.append("g")
.attr("class", "x axis")
.call(xAxis);
var nodes = features.map(function (node, index) {
return {
radius: r(node.properties.pop_mun),
color: '#ff7f0e',
x: x(node.properties.tp60 / 100),
y: height + Math.random(),
pop_mun: node.properties.pop_mun,
tp60: node.properties.tp60
};
});
function tick() {
for (i = 0; i < nodes.length; i++) {
var node = nodes[i];
node.cx = node.x;
node.cy = node.y;
}
}
setTimeout(renderGraph, 10);
function renderGraph() {
// Run the layout a fixed number of times.
// The ideal number of times scales with graph complexity.
// Of course, don't run too long—you'll hang the page!
const NUM_ITERATIONS = 1000;
var force = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody().strength(-3))
.force('center', d3.forceCenter(width / 2, height/2))
.force('x', d3.forceX(d => d.x))
.force('y', d3.forceY(d => d.y))
.force('collide', d3.forceCollide().radius(d => d.radius))
.on("tick", tick)
.stop();
force.tick(NUM_ITERATIONS);
force.stop();
svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => d.radius)
.style("fill", d => z(d.tp60/100))
.on("mouseover", function (d, i) {
d3.select(this).style('fill', "orange")
console.log(i.tp60,i)
svg.append("text")
.attr("id", "t")
.attr("x", function () {
return d.x - 50;
})
.attr("y", function () {
return d.y - 50;
})
.text(function () {
return [x.invert(i.x), i.tp60]; // Value of the text
})
})
.on("mouseout", function (d, i) {
d3.select("#t").remove(); // Remove text location
console.log(i)
d3.select(this).style('fill', z(i.tp60/100));
});
loading.remove();
}
})
})

Responsive D3.js line chart

I am new to using D3 charts and am having an issue making my multi line chart responsive. I have reviewed some examples on how to make D3 responsive, but I can not make it work on my graph. If someone could assist me in making the code I currently have responsive I would really appreciate it.
Here is the code:
const svg = d3.select("#lineChart").attr("height", 400).attr("width", 850);
const width = +svg.attr("width");
const height = +svg.attr("height");
let hasMoreThan0 = false;
const render = (data) => {
const xValue = (d) => d.dateTest;
const yValue = (d) => d.total;
const margin = { top: 60, right: 40, bottom: 88, left: 105 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const xScale = d3
.scaleTime()
.domain(d3.extent(data, xValue))
.range([0, innerWidth])
.nice();
let yScale;
let totalArray = [];
data.forEach((element) => {
totalArray.push(element.total);
});
const reducer = (accumulator, currentValue) => accumulator + currentValue;
let tc = totalArray.reduce(reducer);
data.forEach((d) => {
if (tc == 0) {
yScale = d3
.scaleLinear()
.domain([0, 51])
.range([innerHeight, 0])
.nice();
} else {
yScale = d3
.scaleLinear()
.domain(d3.extent(data, yValue))
.range([innerHeight, 0])
.nice();
}
});
const g = svg
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const xAxis = d3
.axisBottom(xScale)
.tickPadding(15)
.ticks(d3.timeDay)
.tickFormat(d3.timeFormat("%a %d %B %y"));
const yAxis = d3.axisLeft(yScale).tickSize(-innerWidth).tickPadding(10);
const yAxisG = g.append("g").call(yAxis);
yAxisG.selectAll(".domain").remove();
yAxisG
.append("text")
.attr("class", "axis-label")
.attr("y", -60)
.attr("x", -innerHeight / 2)
.attr("fill", "black")
.attr("transform", `rotate(-90)`)
.attr("text-anchor", "middle");
const xAxisG = g
.append("g")
.call(xAxis)
.attr("transform", `translate(0,${innerHeight})`);
xAxisG.select(".domain").remove();
xAxisG
.append("text")
.attr("class", "axis-label")
.attr("y", 80)
.attr("x", innerWidth / 2)
.attr("fill", "black");
const areaGenerator = d3
.area()
.x((d) => xScale(xValue(d)))
.y0(innerHeight)
.y1((d) => yScale(yValue(d)))
.curve(d3.curveMonotoneX);
g.append("path")
.attr("class", "area-path")
.attr("d", areaGenerator(data));
const lineGenerator = d3
.line()
.x((d) => xScale(xValue(d)))
.y((d) => yScale(yValue(d)))
.curve(d3.curveMonotoneX);
g.append("path")
.attr("class", "line-path")
.attr("d", lineGenerator(data));
// g.append("text").attr("class", "title").attr("y", -10).text(title);
svg.append("line").attr("class", "line-dashet");
svg.append("line").attr("class", "line-dashet");
svg
.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", function (d) {
return xScale(xValue(d)) + 105;
})
.attr("cy", function (d) {
return yScale(yValue(d)) + 60;
})
.attr("r", 4)
.on("mouseout", function (e) {
let toolTip = document.getElementsByClassName("tooltip")[0];
toolTip.classList.remove("tooltip-open");
})
.on("mouseover", function (e) {
let selectPoint = d3.select(this);
let xLineAnim = selectPoint._groups[0][0].cx.animVal.value;
let yLineAnim = selectPoint._groups[0][0].cy.animVal.value;
let selectPointData = selectPoint._groups[0][0].__data__;
let dateFormated = formatDate(selectPointData.dateTest, a, "-");
let toolTip = document.getElementsByClassName("tooltip")[0];
toolTip.style.top = yLineAnim - 55 + "px";
toolTip.style.left = xLineAnim + 5 + "px";
toolTip.innerHTML =
"<div><span>Day: </span>" +
dateFormated +
"</div><div><span>Total: </span>" +
selectPointData.total +
"</div>";
toolTip.classList.add("tooltip-open");
svg
.select(".line-dashet")
.attr("x1", xLineAnim)
.attr("y1", margin.top)
.attr("x2", xLineAnim)
.attr("y2", innerHeight + margin.top);
});
};
getTimeData();

D3.js bar chart, bars extending from top to bottom, instead of bottom to top

Barchart image
D3.js bar chart, bars extending from top to bottom, instead of bottom to top.
I am not sure what attributes i should be changing to correct this.
I have posted my code and an image of the resulting chart.
...
const marketCataRender = marketCataData => {
const marketCataSVG = d3.select('.marketCataChart').append('svg')
marketCataSVG.attr('class', 'marketCataSVG')
.attr('height', marketCataHeight)
.attr('width', marketCataWidth);
// x y values
const xValue = d => d.loc_start_str;
const yValue = d => d.total_matched;
// x y scales
const xScale = d3.scaleBand()
.domain(marketCataData.map(xValue))
.range(\[0, innerWidth\]);
const yScale = d3.scaleLinear()
.domain(d3.extent(marketCataData, yValue))
.range(\[innerHeight, 0\])
.nice();
// x y axis
const xAxis = d3.axisBottom(xScale)
const yAxis = d3.axisLeft(yScale)
// set chart group to make it easier to transform
const g = marketCataSVG.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// x y axis groups
const xAxisG = g.append('g')
.call(xAxis)
.attr('transform', `translate(0, ${innerHeight})`)
.selectAll('text')
.style('text-anchor', 'end')
.attr('transform', `rotate(-90)`)
.attr('x', -7)
const yAxisG = g.append('g')
.call(yAxis)
// Apply bar chart rectangle to chart
const marketCataRect = g.selectAll('rect')
marketCataRect.data(marketCataData)
.enter().append('rect')
.attr('x', d => xScale(xValue(d)))
.attr('height', d => yScale(yValue(d)))
.attr('width', xScale.bandwidth());
}][1]
...
You haven't declared the Y coordinates for your rectangles.
You need to scale the y coordinate of your rectangles.
const marketCataRect = g.selectAll('rect')
marketCataRect.data(marketCataData)
.enter().append('rect')
.attr('x', d => xScale(d.loc_start_str) )
.attr('y', d => yScale(d.total_matched) ) // set y
.attr('height', d => marketCataHeight - yScale(d.total_matched)) // find height by subtracting y value from height of the chart.
.attr('width', xScale.bandwidth());
example here: https://bl.ocks.org/d3noob/8952219
Try to always provide a Minimal, Complete, and Verifiable example. https://stackoverflow.com/help/mcve
I tried to do this by taking your code and adding dummy data etc. and then modifying it
The result is this (Demo here- https://codepen.io/Alexander9111/pen/gObEZym):
HTML:
<div class="marketCataChart"></div>
<script src="https://d3js.org/d3.v5.min.js"></script>
Javascript:
const marketCataHeight = 800;
const marketCataWidth = 2000;
const innerWidth = 1500;
const innerHeight = 500;
const margin = {
top: 30,
left: 30,
bottom: 30,
right: 30
};
const marketCataRender = marketCataData => {
const marketCataSVG = d3.select('.marketCataChart').append('svg')
marketCataSVG.attr('class', 'marketCataSVG')
.attr('height', marketCataHeight)
.attr('width', marketCataWidth);
// x y values
const xValue = d => d.loc_start_str;
const yValue = d => d.total_matched;
// x y scales
const xScale = d3.scaleBand();
xScale.domain(marketCataData.map(xValue))
.range([0, innerWidth]);
const yScale = d3.scaleLinear();
yScale.domain(d3.extent(marketCataData, yValue))
.range([innerHeight, 0])
.nice();
// x y axis
//const xAxis = d3.axisBottom(xScale)
const xAxis = d3.axisTop(xScale) //change to axisTop
const yAxis = d3.axisLeft(yScale)
// set chart group to make it easier to transform
const g = marketCataSVG.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// x y axis groups
const xAxisG = g.append('g')
.call(xAxis)
.attr('class', 'x-axis')
.attr('transform', `translate(0, ${0})`) // no longer need to translate by innerHeight as the x-axis is on the top
.selectAll('text')
.style('text-anchor', 'middle')
.attr('transform', `rotate(-0)`) //-90
.attr('x', -7)
const yAxisG = g.append('g')
.call(yAxis)
.attr('class', 'y-axis');
// Apply bar chart rectangle to chart
const marketCataRect = g.selectAll('rect');
marketCataRect.data(marketCataData)
.enter().append('rect')
.attr('x', d => xScale(xValue(d)))
.attr('height', d => yScale(yValue(d)))
.attr('width', xScale.bandwidth());
//Optional - add chart border:
g.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', innerWidth)
.attr('height', innerHeight)
.attr('stroke', 'black')
.attr('stroke-width', '1px')
.attr('fill', 'none')
.attr('class', 'chart-boarder');
};
const marketCataData = [
{loc_start_str: "example0", total_matched: 0},
{loc_start_str: "example1", total_matched: 100},
{loc_start_str: "example2", total_matched: 200},
{loc_start_str: "example3", total_matched: 300},
{loc_start_str: "example4", total_matched: 400},
]
marketCataRender(marketCataData);
Most important lines were: const xAxis = d3.axisTop(xScale) and const xAxisG.attr('transform', `translate(0, ${0})`)

Arranging graphs on one screen

I wan to four graphs side-by-side linearly but they keep overlapping. I am using d3.js and I would like to draw these four graphs side by side.
I tried drawing each graph in its own svg tag and then combine them but it doesn't work.
<script>
const svg = d3.select("svg");
const width = +svg.attr("width");
const height = +svg.attr("height");
var TelescopeData = [
{ Average: 2000, TelescopeName: "1 meter" },
{ Average: 3000, TelescopeName: "1.9 meter" },
{ Average: 4000, TelescopeName: "Lesedi" }
];
var padding = { top: 40, right: 20, bottom: 30, left: 75 };
const innerWidth = width - padding.left - padding.right;
const innerHeight = height - padding.top - padding.bottom;
var colors = ["red", "black", "green"];
var yScale = d3
.scaleLinear()
.domain([0, d3.max(TelescopeData, d => d.Average)])
.range([innerHeight, 0])
.nice();
var xScale = d3
.scaleBand()
.domain(
TelescopeData.map(d => {
return d.TelescopeName;
})
)
.range([0, innerWidth])
.padding(0.4);
//xAxis
const xAxis = svg
.append("g")
.classed("xAxis", true)
.attr(
"transform",
`translate(${padding.left},${innerHeight + padding.top})`
)
.call(d3.axisBottom(xScale));
//yAxis
const yAxis = svg
.append("g")
.classed("yAxis", true)
.attr("transform", `translate(${padding.left},${padding.top})`)
.call(d3.axisLeft(yScale));
// now adding the data
const rectGrp = svg
.append("g")
.attr("transform", `translate(${padding.left},${padding.top})`);
rectGrp
.selectAll("rect")
.data(TelescopeData)
.enter()
.append("rect")
.attr("width", xScale.bandwidth())
.attr("height", (d, i) => {
return innerHeight - yScale(d.Average);
})
.attr("x", d => {
return xScale(d.TelescopeName);
})
.attr("y", function(d) {
return yScale(d.Average);
})
.attr("fill", (d, i) => {
return colors[i];
});
rectGrp
.append("text")
.attr("y", -20)
.attr("x", 50)
.text("Quarterly Average");
</script>
I expect to see the code attached here to be used for drawing 3 other graphs side-side with the first one
Are you wanting to have four graphs shown side by side on the screen next to each other?
That is what your question seems to imply, without having an example of what you want the output to look like. The other option would be, how to merge four graphs into one graph, which is an entirely different question.
Here is a very simple HTML document that will put the four graphs side by side on the screen. I think your problem was probably that you were selecting the element to draw to using the SVG selector instead of assigning an id to each different element and then selecting each element id.
<html>
<head>
<title>Example</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
function drawGraph(elementId) {
const svg = d3.select(elementId);
const width = +svg.attr("width");
const height = +svg.attr("height");
let TelescopeData = [
{ Average: 2000, TelescopeName: "1 meter" },
{ Average: 3000, TelescopeName: "1.9 meter" },
{ Average: 4000, TelescopeName: "Lesedi" }
];
let padding = { top: 40, right: 20, bottom: 30, left: 75 };
const innerWidth = width - padding.left - padding.right;
const innerHeight = height - padding.top - padding.bottom;
let colors = ["red", "black", "green"];
let yScale = d3
.scaleLinear()
.domain([0, d3.max(TelescopeData, d => d.Average)])
.range([innerHeight, 0])
.nice();
let xScale = d3
.scaleBand()
.domain(
TelescopeData.map(d => {
return d.TelescopeName;
})
)
.range([0, innerWidth])
.padding(0.4);
//xAxis
const xAxis = svg
.append("g")
.classed("xAxis", true)
.attr("transform",`translate(${padding.left},${innerHeight + padding.top})`)
.call(d3.axisBottom(xScale));
//yAxis
const yAxis = svg
.append("g")
.classed("yAxis", true)
.attr("transform", `translate(${padding.left},${padding.top})`)
.call(d3.axisLeft(yScale));
// now adding the data
const rectGrp = svg
.append("g")
.attr("transform", `translate(${padding.left},${padding.top})`);
rectGrp
.selectAll("rect")
.data(TelescopeData)
.enter()
.append("rect")
.attr("width", xScale.bandwidth())
.attr("height", (d, i) => {
return innerHeight - yScale(d.Average);
})
.attr("x", d => {
return xScale(d.TelescopeName);
})
.attr("y", function(d) {
return yScale(d.Average);
})
.attr("fill", (d, i) => {
return colors[i];
});
rectGrp
.append("text")
.attr("y", -20)
.attr("x", 50)
.text("Quarterly Average");
}
drawGraph("#testsvg1");
drawGraph("#testsvg2");
drawGraph("#testsvg3");
drawGraph("#testsvg4");
</script>
</head>
<body>
<svg id="#testsvg1" width="200" height="200"></svg>
<svg id="#testsvg2" width="200" height="200"></svg>
<svg id="#testsvg3" width="200" height="200"></svg>
<svg id="#testsvg4" width="200" height="200"></svg>
</body>
</html>

D3 combination of bar and area chart

I am wondering is it possible to achieve the combination of area and bar chart in the way shown in the screenshot below?
Along with making the area in between clickable for some other action.
It would be really helpful if you can guide me to some of the examples to get an idea how to achieve the same.
I posted a codepen here. That creates a bar chart, and then separate area charts between each bar chart.
const BarChart = () => {
// set data
const data = [
{
value: 48,
label: 'One Rect'
},
{
value: 32,
label: 'Two Rect'
},
{
value: 40,
label: 'Three Rect'
}
];
// set selector of container div
const selector = '#bar-chart';
// set margin
const margin = {top: 60, right: 0, bottom: 90, left: 30};
// width and height of chart
let width;
let height;
// skeleton of the chart
let svg;
// scales
let xScale;
let yScale;
// axes
let xAxis;
let yAxis;
// bars
let rect;
// area
let areas = [];
function init() {
// get size of container
width = parseInt(d3.select(selector).style('width')) - margin.left - margin.right;
height = parseInt(d3.select(selector).style('height')) - margin.top - margin.bottom;
// create the skeleton of the chart
svg = d3.select(selector)
.append('svg')
.attr('width', '100%')
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
xScale = d3.scaleBand().padding(0.15);
xAxis = d3.axisBottom(xScale);
yScale = d3.scaleLinear();
yAxis = d3.axisLeft(yScale);
svg.append('g')
.attr('class', 'x axis')
.attr('transform', `translate(0, ${height})`);
svg.append('g')
.attr('class', 'y axis');
svg.append('g')
.attr('class', 'x label')
.attr('transform', `translate(10, 20)`)
.append('text')
.text('Value');
xScale
.domain(data.map(d => d.label))
.range([0, width])
.padding(0.3);
yScale
.domain([0, 75])
.range([height, 0]);
xAxis
.scale(xScale);
yAxis
.scale(yScale);
rect = svg.selectAll('rect')
.data(data);
rect
.enter()
.append('rect')
.style('fill', d => '#00BCD4')
.attr('y', d => yScale(d.value))
.attr('height', d => height - yScale(d.value))
.attr('x', d => xScale(d.label))
.attr('width', xScale.bandwidth());
// call the axes
svg.select('.x.axis')
.call(xAxis);
svg.select('.y.axis')
.call(yAxis);
// rotate axis text
svg.select('.x.axis')
.selectAll('text')
.attr('transform', 'rotate(45)')
.style('text-anchor', 'start');
if (parseInt(width) >= 600) {
// level axis text
svg.select('.x.axis')
.selectAll('text')
.attr('transform', 'rotate(0)')
.style('text-anchor', 'middle');
}
data.forEach(
(d, i) => {
if (data[i + 1]) {
areas.push([
{
x: d.label,
y: d.value
},
{
x: data[i + 1].label,
y: data[i + 1].value
}
]);
}
}
);
areas = areas.filter(
d => Object.keys(d).length !== 0
);
areas.forEach(
a => {
const area = d3.area()
.x((d, i) => {
return i === 0 ?
xScale(d.x) + xScale.bandwidth() :
xScale(d.x);
})
.y0(height)
.y1(d => yScale(d.y));
svg.append('path')
.datum(a)
.attr('class', 'area')
.style('fill', d => '#B2EBF2')
.attr('d', area)
.on('click', d => {
console.log('hello click!');
});
}
)
}
return { init };
};
const myChart = BarChart();
myChart.init();
#bar-chart {
height: 500px;
width: 100%;
}
<script src="https://unpkg.com/d3#5.2.0/dist/d3.min.js"></script>
<div id="bar-chart"></div>
After creating the bar chart, I repackage the data to make it conducive to creating an area chart. I created an areas array where each item is going to be a separate area chart. I'm basically taking the values for the first bar and the next bar, and packaging them together.
data.forEach(
(d, i) => {
if (data[i + 1]) {
areas.push([
{
x: d.label,
y: d.value
},
{
x: data[i + 1].label,
y: data[i + 1].value
}
]);
}
}
);
areas = areas.filter(
d => Object.keys(d).length !== 0
);
I then iterate through each element on areas and create the area charts.
The only tricky thing here, I think, is getting the area chart to span from the end of the first bar to the start of the second bar, as opposed to from the end of the first bar to the end of the second bar. To accomplish this, I added a rectangle width from my x-scale to the expected x value of the area chart when the first data point is being dealt with, but not the second.
I thought of this as making two points on a line: one for the first bar and one for the next bar. D3's area function can shade all the area under a line. So, the first point on my line should be the top-right corner of the first bar. The second point should be the top-left corner of the next bar.
Attaching a click event at the end is pretty straightforward.
areas.forEach(
a => {
const area = d3.area()
.x((d, i) => {
return i === 0 ?
xScale(d.x) + xScale.bandwidth() :
xScale(d.x);
})
.y0(height)
.y1(d => yScale(d.y));
svg.append('path')
.datum(a)
.attr('class', 'area')
.style('fill', d => '#B2EBF2')
.attr('d', area)
.on('click', d => {
console.log('hello click!');
});
}
)
In the example below, I have combined a simple bar chart (like in this famous bl.lock) with some polygons in between. I guess it could also be achieved with a path.
const data = [
{ letter: "a", value: 9 },
{ letter: "b", value: 6 },
{ letter: "c", value: 3 },
{ letter: "d", value: 8 }
];
const svg = d3.select("#chart");
const margin = { top: 20, right: 20, bottom: 30, left: 40 };
const width = +svg.attr("width") - margin.left - margin.right;
const height = +svg.attr("height") - margin.top - margin.bottom;
const xScale = d3.scaleBand()
.rangeRound([0, width]).padding(0.5)
.domain(data.map(d => d.letter));
const yScale = d3.scaleLinear()
.rangeRound([height, 0])
.domain([0, 10]);
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(yScale));
g.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", d => xScale(d.letter))
.attr("y", d => yScale(d.value))
.attr("width", xScale.bandwidth())
.attr("height", d => height - yScale(d.value));
// Add polygons
g.selectAll(".area")
.data(data)
.enter().append("polygon")
.attr("class", "area")
.attr("points", (d,i,nodes) => {
if (i < nodes.length - 1) {
const dNext = d3.select(nodes[i + 1]).datum();
const x1 = xScale(d.letter) + xScale.bandwidth();
const y1 = height;
const x2 = x1;
const y2 = yScale(d.value);
const x3 = xScale(dNext.letter);
const y3 = yScale(dNext.value);
const x4 = x3;
const y4 = height;
return `${x1},${y1} ${x2},${y2} ${x3},${y3} ${x4},${y4} ${x1},${y1}`;
}
})
.on("click", (d,i,nodes) => {
const dNext = d3.select(nodes[i + 1]).datum();
const pc = Math.round((dNext.value - d.value) / d.value * 100.0);
alert(`${d.letter} to ${dNext.letter}: ${pc > 0 ? '+' : ''}${pc} %`);
});
.bar {
fill: steelblue;
}
.area {
fill: lightblue;
}
.area:hover {
fill: sandybrown;
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg width="400" height="300" id="chart"></svg>

Categories