Related
I'm trying to create an interactive bar chart of the top Forbes 100 companies, with buttons to change between sales and profit.
The first issue I'm having is with the domain:
x.domain([0, d3.max(data, d => d[xValue])])
Error says "data not defined"
but I defined it here:
d3.csv("data/data_clean.csv").then(data => {
data.forEach(d => {
d.sales_usd_billion = Number(d.sales_usd_billion)
d.profit_usd_billion = Number(d.profit_usd_billion)
})
data snapshot:
rank,company,country,sales_usd_billion,sales_unit,profit_usd_billion,profit_unit,assets_usd_billion,market_usd_billion,sales_usd,profit_usd,assets_usd
1,Berkshire Hathaway,United States,276.09,B,89.8,B,958.78,741.48,276.09,89.8,958.78
2,ICBC,China,208.13,B,54.03,B,5518.51,214.43,208.13,54.03,5518.51
3,Saudi Arabian Oil Company (Saudi Aramco),Saudi Arabia,400.38,B,105.36,B,576.04,2292.08,400.38,105.36,576.04
4,JPMorgan Chase,United States,124.54,B,42.12,B,3954.69,374.45,124.54,42.12,3954.69
5,China Construction Bank,China,202.07,B,46.89,B,4746.95,181.32,202.07,46.89,4746.95
6,Amazon,United States,469.82,B,33.36,B,420.55,1468.4,469.82,33.36,420.55
7,Apple,United States,378.7,B,100.56,B,381.19,2640.32,378.7,100.56,381.19
8,Agricultural Bank of China,China,181.42,B,37.38,B,4561.05,133.38,181.42,37.38,4561.05
9,Bank of America,United States,96.83,B,31,B,3238.22,303.1,96.83,31,3238.22
10,Toyota Motor,Japan,281.75,B,28.15,B,552.46,237.73,281.75,28.15,552.46
11,Alphabet,United States,257.49,B,76.03,B,359.27,1581.72,257.49,76.03,359.27
12,Microsoft,United States,184.9,B,71.19,B,340.39,2054.37,184.9,71.19,340.39
13,Bank of China,China,152.43,B,33.57,B,4192.84,117.83,152.43,33.57,4192.84
14,Samsung Group,South Korea,244.16,B,34.27,B,358.88,367.26,244.16,34.27,358.88
FULL CODE:
//Forbes companies bar chart
//set up chart area
const MARGIN = { LEFT: 250, RIGHT: 10, TOP: 50, BOTTOM: 100 }
const WIDTH = 1000 - MARGIN.LEFT - MARGIN.RIGHT
const HEIGHT = 1100 - MARGIN.TOP - MARGIN.BOTTOM
const svg = d3.select("#chart-area").append("svg")
.attr("width", WIDTH + MARGIN.LEFT + MARGIN.RIGHT)
.attr("height", HEIGHT + MARGIN.TOP + MARGIN.BOTTOM)
const g = svg.append("g")
.attr("transform", `translate(${MARGIN.LEFT}, ${MARGIN.TOP})`)
// X label
g.append("text")
.attr("class", "x axis-label")
.attr("x", WIDTH / 2)
.attr("y", HEIGHT + 50)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
// Y label
const yLabel = g.append("text")
.attr("class", "y axis-label")
.attr("x", - (HEIGHT / 2))
.attr("y", -200)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.attr("transform", "rotate(-90)")
.text("Company")
//scales
const x = d3.scaleLinear()
.range([0, WIDTH])
const y = d3.scaleBand()
.range([HEIGHT, 0])
//axis generators
const xAxisCall = d3.axisBottom()
const yAxisCall = d3.axisLeft()
//axis groups
const xAxisGroup = g.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0, ${HEIGHT})`)
const yAxisGroup = g.append("g")
.attr("class", "y axis")
//event listeners
$("#var-select").on("change", update)
d3.csv("data/data_clean.csv").then(data => {
data.forEach(d => {
d.sales_usd_billion = Number(d.sales_usd_billion)
d.profit_usd_billion = Number(d.profit_usd_billion)
})
update()
})
function update() {
const t = d3.transition().duration(750)
//filter based on selections
const xValue = $("#var-select").val()
x.domain([0, d3.max(data, d => d[xValue])])
y.domain(data.map(d => d.company))
data.sort(function(a, b) {
return b.rank - a.rank;
})
//update axes
xAxisCall.scale(x)
xAxis.transition(t).call(xAxisCall)
yAxisCall.scale(y)
yAxis.transition(t).call(yAxisCall)
//***Tooltips */
//*** --- */
rects.enter().append("rect")
.attr("y", d => y(d.company) +3)
.attr("x", 0)
.attr("width", d => x(d[value]))
.attr("height", d => 4)
You need to pass data in your update method and change function definition as function update(data). Its a simple scope problem, I would suggest that try debugging the code and then ask for help here.To learn more about debugging, follow a javascript debugging tutorial
In this Vue component, am trying to create an interactive bar chart hover is seems to be recreating the group element every time the data is updated. If someone can tell me where the problem is because am stuck since I've tried both the general update pattern as well as nest updated pattern.
export default {
name: "StatisticsUI",
props:["reps"],
mounted(){
this.setOptions(),
this.genChart()
},
updated(){
this.genChart()
},
methods:{
......
genChart(){
const data = [
["CCP",2],
["ZPA",1],
["ERA",3],
["POS",4],
]
const svg = d3.select("svg")
const width = svg.attr("width")
const height = svg.attr("height")
const margin ={left: 50, right:50, top:50, bottom:50}
const innerWidth = width - margin.left - margin.right
const innerHeight = height - margin.top - margin.bottom
const g = svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
const yAxis = g.append("g").attr("class", "y-axis")
const xAxis = g.append("g").attr("class", "x-axis")
const xValue = d => d[0]
const yValue = d => d[1]
const xScale = d3.scaleBand().domain(data.map(xValue)).range([0, innerWidth]).padding(0.2);
const yScale = d3.scaleLinear().domain([0, d3.max(data, yValue)]).range([innerHeight, 0]);
yAxis.call(d3.axisLeft(yScale));
xAxis
.call(d3.axisBottom(xScale))
.attr("transform", `translate(0, ${innerHeight})`)
let rect = g.selectAll("rect").data(data);
rect.exit().remove()
rect
.enter()
.append("rect")
.merge(rect)
.attr("fill", "#69b3a2")
.attr("x", (d) => xScale(xValue(d)))
.attr("width", xScale.bandwidth())
.attr("height", 0)
.attr("y", innerHeight)
.transition()
.duration(1000)
.delay((d, i) => i * 50)
.ease(d3.easeBounce)
.attr("y", (d) => yScale(yValue(d)))
.attr("height", function (d) {
return innerHeight - yScale(yValue(d));
});
}
}
}
So I have this code that works fine for the format date:
formatDate(date){
var aux= utcParse("%Y-%m-%dT%H:%M:%S.%LZ")(date);
return aux;
};
The chart looks like this:
However in the x-axis, the whole datetime object is rendered. I just wanted to show the day and the month in this format "day/month".
This is what I tried:
formatDate(date){
var aux= utcParse("%Y-%m-%dT%H:%M:%S.%LZ")(date);
var formated = timeFormat("%d-%m")(aux);
return formated;
};
However when I do this the data does not render. Before it even raised an error.
Please note that I am using a time axis for x:
// Add X axis --> it is a date format
var x = scaleTime()
.domain(extent(data_render, function(d) { return d[0]; }))
.range([ 0, width ]);
This is the whole source code of the React component.
import React, { Component } from 'react';
import { scaleLinear, scaleBand, scaleTime, scaleOrdinal } from 'd3-scale';
import { select, selectAll, pointer} from 'd3-selection';
import { line, curveMonotoneX, area } from 'd3-shape';
import { extent, max } from 'd3-array';
import { transition} from 'd3-transition';
import { axisBottom, axisLeft,axisRight } from "d3-axis";
import { timeParse, timeFormat , utcParse} from 'd3-time-format';
export default class MyLineChart extends Component {
constructor(props)
{
super(props);
this.state={"isLoaded":false, "circleHover":false};
this.lineRef = React.createRef();
this.formatDate=this.formatDate.bind(this);
this.circleTooltip=this.circleTooltip.bind(this);
}
circleTooltip(circle){
circle
.append("text")
.text("circle")
}
formatDate(date){
var aux= utcParse("%Y-%m-%dT%H:%M:%S.%LZ")(date);
//var formated = timeFormat("%d-%m")(aux);
//console.log(formated);
return aux;
//return timeFormat("%d-%m")(utcParse("%Y-%m-%dT%H:%M:%S.%LZ")(date));
};
componentDidMount(){
const node = this.lineRef.current;
const { data, title, aggr} = this.props;
var data_render = [];
data.segments.forEach(
(obj) => {
data_render.push([this.formatDate(obj.end), obj[title][aggr]]);
}
)
console.log(data_render);
// set the dimensions and margins of the graph
var margin = {top: 10, right: 30, bottom: 30, left: 60},
width = 460 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = select(node)
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Add X axis --> it is a date format
var x = scaleTime()
.domain(extent(data_render, function(d) { return d[0]; }))
.range([ 0, width ]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(axisBottom(x));
// Add Y axis
var y = scaleLinear()
.domain([0, max(data_render, function(d) { return d[1]; })])
.range([ height, 0 ]).nice();
svg.append("g")
.attr("transform", "translate("+0+",0)")
.call(axisRight(y));
//Add the area
svg.append("path")
.datum(data_render)
.attr("fill", "#69b3a2")
.attr("fill-opacity", .3)
.attr("stroke", "none")
.attr("d", area()
.x(function(d) { return x(d[0]) })
.y0( height )
.y1(function(d) { return y(d[1]) }).curve(curveMonotoneX)
)
// Add the line
svg.append("path")
.datum(data_render)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", line()
.x(function(d) { return x(d[0]) })
.y(function(d) { return y(d[1]) }).curve(curveMonotoneX)
)
// Add the circles
svg.selectAll("myCircles")
.data(data_render)
.enter()
.append("circle")
.attr("fill", "red")
.attr("stroke", "none")
.attr("cx", function(d) { return x(d[0]) })
.attr("cy", function(d) { return y(d[1]) })
.attr("r", 5).style("opacity",1)
svg.selectAll("myText")
.data(data_render)
.enter()
.append("text")
.attr("x", function(d){return x(d[0])})
.attr("y", function(d){return y(d[1])})
.text(function(d){return d[0]+' '+d[1]})
.style("font-size","6px")
.style("opacity",1);
//Add the title
svg.append("text")
.attr("x", width/2)
.attr("y", margin.top)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.text(title);
this.setState({...this.state, isLoaded:true})
}
render() {
return (
<div>
<svg className="lineChart" ref={this.lineRef} />
</div>
);
}
}
How can I format the axis to just show the day and the month in the x-axis?
You want the values in data_render, like d[0], to be Date objects, which is what utcParse("%Y-%m-%dT%H:%M:%S.%LZ")(date) returns.
Then for your axis, you want something like d3.axisBottom(x).tickFormat(d3.timeFormat("%d-%m")).
Got this piece of code:
export default function Chart({ data, changeData}) {
console.log(data);
const ref = useRef();
const createGraph = (data) => {
var margin = { top: 20, right: 20, bottom: 30, left: 40 },
width = 1360 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scaleBand().range([0, width]).padding(0.1);
var y = d3.scaleLinear().range([height, 0]);
var svg = d3
.select(ref.current)
.append("svg")
.attr("id", "chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
x.domain(
data.map(function (d) {
return d.name;
})
);
y.domain([
0,
d3.max(data, function (d) {
return d.number;
}),
]);
// append the rectangles for the bar chart
const bars = svg.selectAll().data(data).enter().append("rect");
bars
.attr("class", "bar")
.attr("x", function (d) {
return x(d.name);
})
.attr("width", x.bandwidth())
.attr("y", function (d) {
return y(d.number);
})
.attr("fill", "pink")
.attr("height", function (d) {
return height - y(d.number);
})
.on("mouseenter", function (actual, i) {
d3.select(this).attr("opacity", 0.5);
d3.select(this)
.transition()
.duration(300)
.attr("opacity", 0.6)
.attr("x", (a) => x(a.name) - 5)
.attr("width", x.bandwidth() + 10);
})
.on("mouseleave", function (actual, i) {
d3.select(this).attr("opacity", 1);
d3.select(this)
.transition()
.duration(300)
.attr("opacity", 1)
.attr("x", (a) => x(a.name))
.attr("width", x.bandwidth());
});
bars
.append("text")
.attr("class", "value")
.attr("x", (a) => x(a.name) + x.bandwidth() / 2)
.attr("y", (a) => y(a.number) + 30)
.attr("fill", "blue")
.attr("text-anchor", "middle")
.text((a) => `${a.number}%`);
d3.selectAll("bars").append("text").attr("class", "divergence");
// add the x Axis
svg
.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// add the y Axis
svg.append("g").call(d3.axisLeft(y));
};
useEffect(() => {
createGraph(data);
}, [data]);
return (
<React.Fragment>
<Filter>
<h2>By</h2>
<span>Popularity</span>
<span onClick={() => changeData()}>Following</span>
</Filter>
<div style={{ marginLeft: "100px" }} ref={ref}></div>
</React.Fragment>
);
}
How does one update the data with every render?
I tried creating a function that removes the SVG and adds a new one but the positions are messed up after first render, also I tried using exit() and remove() in useEffect but no results.
The result that I'm trying to achieve is that I have a graph and the data is fetched and passed into this component and whenever I press a button, the data will change. I'm not looking at any animations right now, I just want to see how I can change the data .
It's a bit late but in case anyone happens on this question while searching.
useEffect(() => {
*// your d3 code here*
instead of using .enter().append('rect')
use .join('rect')
}, [data]) *// runs the d3 code every time the data changes*
more about .join() https://observablehq.com/#d3/selection-join
In the return statement specify the svg and g tags.
Instead of ref on the div, do useRef on the svg tag
Instead of .append(g), do .select('.x-axis') etc
This is so that you do not append new g every time the useEffect is run
<svg ref={d3Container}>
<g className="chart">
<g className="x-axis" />
<g className="y-axis" />
</g>
</svg>
I learnt the above after watching the 1st and 3rd video in this series https://muratorium.com/using-react-hooks-with-d3
Have spent the last 2 days looking through stackoverflow and online examples as to why my charts aren't displaying properly.
I'm sure I'm missing something in terms of the scaling portion of the code. If I copy the dark part at the bottom of the x-Axis on the chart to notepad it gives me all of the x-axis elements.
Can anyone point me in the right direction?
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.8.0/d3.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded',function(){
req.open("GET",'https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json',true);
req.send();
req.onload=function(){
json=JSON.parse(req.responseText);
document.getElementsByClassName('title')[0].innerHTML=json.name;
dataset=json.data;
const w = 500;
const h = 300;
const padding = 10;
// create an array with all date names
const dates = dataset.map(function(d) {
return d[0];
});
const xScale = d3.scaleBand()
.rangeRound([padding, w-padding])
.padding([.02])
.domain(dates);
console.log("Scale Bandwidth: " + xScale.bandwidth());
const yScale = d3.scaleLinear()
.rangeRound([h-padding, padding])
.domain(0,d3.max(dataset, (d)=>d[1]));
console.log("Dataset Max Height: " + d3.max(dataset, (d)=>d[1]));
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
const svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.append("g")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("width",(d,i)=>xScale.bandwidth())
.attr("height",(d,i)=>(h-yScale(d[1])))
.attr("x", (d,i)=>xScale(d[0]))
.attr("y", (d,i)=>yScale(d[1]))
.attr("fill", "navy")
.attr("class", "bar");
};
});
</script>
<h1 class="title">Title Will Go Here</h1>
</body>
D3 now uses Promises instead of asynchronous callbacks to load data. Promises simplify the structure of asynchronous code, especially in modern browsers that support async and await.
Changes in D3 5.0
Also, you are right in that your yScale is broken. Linear scales need a range and a domain, each being passed a 2 value array.
const yScale = d3.scaleLinear()
.range([h - padding, padding])
.domain([0, d3.max(dataset, (d) => d[1])]);
document.addEventListener('DOMContentLoaded', async function() {
const res = await d3.json("https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json");
//console.log(res.data)
const dataset = res.data
const w = 500;
const h = 300;
const padding = 10;
// create an array with all date names
const dates = dataset.map(function(d) {
return d[0];
});
const max = d3.max(dataset, function(d) { return d[1]} )
const xScale = d3.scaleBand()
.rangeRound([0, w])
.padding([.02])
.domain(dates);
console.log("Scale Bandwidth: " + xScale.bandwidth());
const yScale = d3.scaleLinear()
.range([h - padding, padding])
.domain([0, d3.max(dataset, (d) => d[1])]);
console.log("Dataset Max Height: " + d3.max(dataset, (d) => d[1]));
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
const svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.append("g")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("width", (d, i) => xScale.bandwidth())
.attr("height", (d, i) => (h - yScale(d[1])) )
.attr("x", (d, i) => xScale(d[0]))
.attr("y", (d, i) => yScale(d[1]))
.attr("fill", "navy")
.attr("class", "bar");
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.8.0/d3.min.js"></script>
Codepen