I'm trying to visualize a stock price chart, using reactJS and D3JS(v5).
Fetching data from Alphavantage API
Framework = ReactJS (which, honestly, introduces an unnecessary layer of complexity)
D3JS for viz
Roughly three parts to this:
global function parseData(). Parses data from the fetch into a
format that d3 can read. Verified working, but included for
completeness.
componentDidMount() call within class App. Promise chain: has fetch call, then parseData(), then setState(), and finally
drawChart()
drawChart() local function within class App: contains all D3 logic. Will be separated out into its own component later. Works if I pass it data from a local json (commented out; some sample rows provided below), but not when I try to pass it
data from fetch.
Code so far: this is all in App.js, no child components:
import React, { Component } from 'react';
import './App.css';
import * as d3 from "d3";
//import testTimeData from "./data/testTimeData"
function parseData(myInput) {
// processes alpha vantage data into a format for viz
// output an array of objects,
// where each object is {"Date":"yyyy-mm-dd", "a":<float>}
let newArray = []
for (var key in myInput) {
if (myInput.hasOwnProperty(key)) {
const newRow = Object.assign({"newDate": new Date(key)}, {"Date": key}, myInput[key])
newArray.push(newRow)
}
}
//console.log(newArray)
// 2. Generate plotData for d3js
let newArray2 = []
for (var i = 0; i < newArray.length; i++) {
let newRow = Object.assign({"Date": newArray[i]["Date"]}, {"a":parseFloat(newArray[i]["4. close"])})
newArray2.unshift(newRow)
}
return newArray2
}
class App extends Component {
constructor() {
super()
this.state = {
ticker:"",
plotData:[]
}
this.drawChart = this.drawChart.bind(this)
}
// setState() in componentDidMount()
// fetch monthly data
// hardcode ticker = VTSAX for the moment
// and call this.drawChart()
componentDidMount() {
const ticker = "VTSAX"
const api_key = "EEKL6B77HNZE6EB4"
fetch("https://www.alphavantage.co/query?function=TIME_SERIES_MONTHLY&symbol="+ticker+"&apikey="+api_key)
.then(console.log("fetching..."))
.then(response => response.json())
.then(data => parseData(data["Monthly Time Series"]))
.then(data => console.log(data))
.then(data =>{this.setState({plotData:data, ticker:ticker})})
.then(this.drawChart());
}
drawChart() {
const stockPlotData = this.state.plotData
console.log("stockPlotData.length=", stockPlotData.length)
var svg = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 300)
var margin = {left:50, right:30, top:30, bottom: 30}
var width = svg.attr("width") - margin.left - margin.right;
var height = svg.attr("height") - margin.bottom - margin.top;
var x = d3.scaleTime().rangeRound([0, width]);
var y = d3.scaleLinear().rangeRound([height, 0]);
var parseTime = d3.timeParse("%Y-%m-%d");
x.domain(d3.extent(stockPlotData, function(d) { return parseTime(d.date); }));
y.domain([0,
d3.max(stockPlotData, function(d) {
return d.a;
})]);
var multiline = function(category) {
var line = d3.line()
.x(function(d) { return x(parseTime(d.date)); })
.y(function(d) { return y(d[category]); });
return line;
}
var g = svg.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
var categories = ['a'];
for (let i in categories) {
var lineFunction = multiline(categories[i]);
g.append("path")
.datum(stockPlotData)
.attr("class", "line")
.style("stroke", "blue")
//.style("stroke", color(i))
.style("fill", "None")
.attr("d", lineFunction);
}
// append X Axis
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickFormat(d3.timeFormat("%Y-%m-%d")));
// append Y Axis
g.append("g")
.call(d3.axisLeft(y));
}
render(){
return (<div>{this.state.ticker}</div>)
}
}
export default App;
Outputs from console.log() calls:
data => console.log(data) within the promise chain of componentDidMount() correctly displays the fetched data
But console.log("stockPlotData.length=", stockPlotData.length) call in the drawChart() function returns stockPlotData.length= 0. Did I call this.setState() wrongly?
The page renders "VTSAX" correctly in the render(){return(...)} call at the bottom, indicating that this.setState updated the ticker variable correctly.
Some test data rows:
[
{"Date":"2016-01-15", "a": 220 },
{"Date":"2016-01-16", "a": 250},
{"Date":"2016-01-17", "a": 130},
{"Date":"2016-01-18", "a": 180},
{"Date":"2016-01-19", "a": 200},
]
That's a lot of code to read, but based on a quick glance, you probably want to call this.drawChart() inside of a .then instead of after your promise chain, because it's gonna fire before your promises resolve.
Related
Learning Javascript and D3.
Trying to create a graph where it draws each line in a series, one at a time or on a delayed interval.
What I've got so far (relevant code at the end of this post)
https://jsfiddle.net/jimdholland/5n6xrLk0/180/
My data is structures with each series as one row, with 12 columns. When you read it in, the object approximately looks like
mydata = [
{NumDays01: "0", NumDays02: "0", NumDays03: "0", NumDays04: "0", NumDays05: "0",Numdays06: 30}
1: {NumDays01: "0", NumDays02: "0", NumDays03: "0", NumDays04: "0",...
]
I can get it to create line, but it draws all the paths at once. Likewise, I've tried a loop, but that still draws them all at once. Also tried d3.timer and similar results. But I'm having a hard time understanding the timer callback stuff and creating those functions correctly.
I haven't found another example to really study other than a chart from FlowingData which has a lot of bells and whistles out of my league.
https://flowingdata.com/2019/02/01/how-many-kids-we-have-and-when-we-have-them/
Any help or tips would be appreciated.
var svg = d3.select("#chart").select("svg"),
margin = { top: 30, right: 20, bottom: 10, left: 40 },
width = parseInt(d3.select("#chart").style('width'), 10) - margin.left - margin.right;
d3.tsv("https://s3.us-east-2.amazonaws.com/jamesdhollandwebfiles/data/improvementTest.tsv", function(error, data) {
if (error) throw error;
console.log(data);
myData = data;
var t = d3.timer(pathMaker);
}); // #end d3.tsv()
function pathMaker() {
var peeps = myData[rowToRun];
var coords = [];
var lineToRemove = [];
for (var nameIter in dayList) {
coords.push({ x: x(nameIter), y: y(peeps[dayList[nameIter]])})
}
var improvementPath = g.append("path")
.datum(coords)
.attr("id", "path" + peeps.caseid)
.attr("d", lineMaker)
.attr("stroke", "#90c6e4")
.attr("fill", "none")
.attr("stroke-width", 2);
var total_length = improvementPath.node().getTotalLength();
var startPoint = pathStartPoint(improvementPath);
improvementPath = improvementPath
.attr("stroke-dasharray", total_length + " " + total_length)
.attr("stroke-dashoffset", total_length)
.transition() // Call Transition Method
.duration(4000) // Set Duration timing (ms)
.ease(d3.easeLinear) // Set Easing option
.attr("stroke-dashoffset", 0); // Set final value of dash-offset for transition
rowToRun += 1;
if (rowToRun == 5) {rowToRun = 0;}
}
//Get path start point for placing marker
function pathStartPoint(Mypath) {
var d = Mypath.attr("d"),
dsplitted = d.split(/M|L/)[1];
return dsplitted;
}
there are a few problems. I tried to configure your code as little as I could to make it work. If you need further explanations, please let me know
d3.tsv("https://s3.us-east-2.amazonaws.com/jamesdhollandwebfiles/data/improvementTest.tsv", function(error, data) {
if (error) throw error;
console.log(data);
myData = data;
pathMaker()
}); // #end d3.tsv()
function pathMaker() {
var peeps = myData[rowToRun];
var coords_data = [];
var lineToRemove = [];
for (let i = 0; i < myData.length; i++) {
var coords = [];
for (var nameIter in dayList) {
coords.push({ x: x(nameIter), y: y(myData[i][dayList[nameIter]])})
}
coords_data.push(coords)
}
console.log(coords_data)
var improvementPath = g.selectAll("path")
.data(coords_data)
.enter()
.append("path")
.attr("d", lineMaker)
.attr("stroke", "#90c6e4")
.attr("fill", "none")
.attr("stroke-width", 2);
improvementPath = improvementPath.each(function (d,i) {
var total_length = this.getTotalLength();
var startPoint = pathStartPoint(improvementPath);
const path = d3.select(this)
.attr("stroke-dasharray", total_length + " " + total_length)
.attr("stroke-dashoffset", total_length)
.transition() // Call Transition Method
.duration(4000) // Set Duration timing (ms)
.delay(i*4000)
.ease(d3.easeLinear) // Set Easing option
.attr("stroke-dashoffset", 0); // Set final value of dash-offset for transition
})
rowToRun += 1;
if (rowToRun == 5) {rowToRun = 0;}
}
I have the following script for rendering a simple barchart in D3.js. I have been able to render charts ok up until a point.
I have this data below which I am struggling to insert into my chart, there is no specific key I can call upon and I'm really confused how I would insert all of these into my chart.
Object { "food-environmental-science": 0, "art-media-research": 0, .....}
I have a seperate file for the HTML (only a snippet):
var barchart1 = barchart("#otherchart");
function clickScatter(d){
var unitOfAssessment = d.UoAString;
click = d.environment.topicWeights
renderTopicWeights(click)
}
function renderTopicWeights(clickedPoint){
barchart1.loadAndRenderDataset(clickedPoint)
}
When I call upon the loadAndRenderDataset function, console gives me a data.map is not a function error.
function barchart(targetDOMelement) {
//=================== PUBLIC FUNCTIONS =========================
//
barchartObject.appendedMouseOverFunction = function (callbackFunction) {
console.log("appendedMouseOverFunction called", callbackFunction)
appendedMouseOverFunction = callbackFunction;
render();
return barchartObject;
}
barchartObject.appendedMouseOutFunction = function (callbackFunction) {
appendedMouseOutFunction = callbackFunction;
render();
return barchartObject;
}
barchartObject.loadAndRenderDataset = function (data) {
dataset=data.map(d=>d); //create local copy of references so that we can sort etc.
render();
return barchartObject;
}
barchartObject.overrideDataFieldFunction = function (dataFieldFunction) {
dataField = dataFieldFunction;
return barchartObject;
}
barchartObject.overrideKeyFunction = function (keyFunction) {
//The key function is used to obtain keys for GUP rendering and
//to provide the categories for the y-axis
//These valuse should be unique
GUPkeyField = yAxisCategoryFunction = keyFunction;
return barchartObject;
}
barchartObject.overrideMouseOverFunction = function (callbackFunction) {
mouseOverFunction = callbackFunction;
render();
return barchartObject;
}
barchartObject.overrideMouseOutFunction = function (callbackFunction) {
mouseOutFunction = callbackFunction;
render(); //Needed to update DOM
return barchartObject;
}
barchartObject.overrideTooltipFunction = function (toolTipFunction) {
tooltip = toolTipFunction;
return barchartObject;
}
barchartObject.overrideMouseClickFunction = function (fn) {
mouseClick2Function = fn;
render(); //Needed to update DOM if they exist
return barchartObject;
}
barchartObject.render = function (callbackFunction) {
render(); //Needed to update DOM
return barchartObject;
}
barchartObject.setTransform = function (t) {
//Set the transform on the svg
svg.attr("transform", t)
return barchartObject;
}
barchartObject.yAxisIndent = function (indent) {
yAxisIndent=indent;
return barchartObject;
}
//=================== PRIVATE VARIABLES ====================================
//Width and height of svg canvas
var svgWidth = 900;
var svgHeight = 450;
var dataset = [];
var xScale = d3.scaleLinear();
var yScale = d3.scaleBand(); //This is an ordinal (categorical) scale
var yAxisIndent = 400; //Space for labels
var maxValueOfDataset; //For manual setting of bar length scaling (only used if .maxValueOfDataset() public method called)
//=================== INITIALISATION CODE ====================================
//Declare and append SVG element
var svg = d3
.select(targetDOMelement)
.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight)
.classed("barchart",true);
//Declare and add group for y axis
var yAxis = svg
.append("g")
.classed("yAxis", true);
//Declare and add group for x axis
var xAxis = svg
.append("g")
.classed("xAxis", true);
//===================== ACCESSOR FUNCTIONS =========================================
var dataField = function(d){return d.datafield} //The length of the bars
var tooltip = function(d){return d.key + ": "+ d.datafield} //tooltip text for bars
var yAxisCategoryFunction = function(d){return d.key} //Categories for y-axis
var GUPkeyField = yAxisCategoryFunction; //For 'keyed' GUP rendering (set to y-axis category)
//=================== OTHER PRIVATE FUNCTIONS ====================================
var maxValueOfDataField = function(){
//Find the maximum value of the data field for the x scaling function using a handy d3 max() method
//This will be used to set (normally used )
return d3.max(dataset, dataField)
};
var appendedMouseOutFunction = function(){};
var appendedMouseOverFunction = function(){};
var mouseOverFunction = function (d,i){
d3.select(this).classed("highlight", true).classed("noHighlight", false);
appendedMouseOverFunction(d,i);
}
var mouseOutFunction = function (d,i){
d3.select(this).classed("highlight", false).classed("noHighlight", true);
appendedMouseOutFunction(d,i);
}
var mouseClick2Function = function (d,i){
console.log("barchart click function = nothing at the moment, d=",d)
};
function render () {
updateScalesAndRenderAxes();
GUP_bars();
}
function updateScalesAndRenderAxes(){
//Set scales to reflect any change in svgWidth, svgHeight or the dataset size or max value
xScale
.domain([0, maxValueOfDataField()])
.range([0, svgWidth-(yAxisIndent+10)]);
yScale
.domain(dataset.map(yAxisCategoryFunction)) //Load y-axis categories into yScale
.rangeRound([25, svgHeight-40])
.padding([.1]);
//Now render the y-axis using the new yScale
var yAxisGenerator = d3.axisLeft(yScale);
svg.select(".yAxis")
.transition().duration(1000).delay(1000)
.attr("transform", "translate(" + yAxisIndent + ",0)")
.call(yAxisGenerator);
//Now render the x-axis using the new xScale
var xAxisGenerator = d3.axisTop(xScale);
svg.select(".xAxis")
.transition().duration(1000).delay(1000)
.attr("transform", "translate(" + yAxisIndent + ",20)")
.call(xAxisGenerator);
};
function GUP_bars(){
//GUP = General Update Pattern to render bars
//GUP: BIND DATA to DOM placeholders
var selection = svg
.selectAll(".bars")
.data(dataset, GUPkeyField);
//GUP: ENTER SELECTION
var enterSel = selection //Create DOM rectangles, positioned # x=yAxisIndent
.enter()
.append("rect")
.attr("x", yAxisIndent)
enterSel //Add CSS classes
.attr("class", d=>("key--"+GUPkeyField(d)))
.classed("bars enterSelection", true)
.classed("highlight", d=>d.highlight)
enterSel //Size the bars
.transition()
.duration(1000)
.delay(2000)
.attr("width", function(d) {return xScale(dataField(d));})
.attr("y", function(d, i) {return yScale(yAxisCategoryFunction(d));})
.attr("height", function(){return yScale.bandwidth()});
enterSel //Add tooltip
.append("title")
.text(tooltip)
//GUP UPDATE (anything that is already on the page)
var updateSel = selection //update CSS classes
.classed("noHighlight updateSelection", true)
.classed("highlight enterSelection exitSelection", false)
.classed("highlight", d=>d.highlight)
updateSel //update bars
.transition()
.duration(1000)
.delay(1000)
.attr("width", function(d) {return xScale(dataField(d));})
.attr("y", function(d, i) {return yScale(yAxisCategoryFunction(d));})
.attr("height", function(){return yScale.bandwidth()});
updateSel //update tool tip
.select("title") //Note that we already created a <title></title> in the Enter selection
.text(tooltip)
//GUP: Merged Enter & Update selections (so we don't write these twice)
var mergedSel = enterSel.merge(selection)
.on("mouseover", mouseOverFunction)
.on("mouseout", mouseOutFunction)
.on("click", mouseClick2Function)
//GUP EXIT selection
var exitSel = selection.exit()
.classed("highlight updateSelection enterSelection", false)
.classed("exitSelection", true)
.transition()
.duration(1000)
.attr("width",0)
.remove()
};
return barchartObject;'
}
Any help would be much appreciated, and I appolguise if what I'm asking is not clear. Thanks
format your input data into object like {key:"",value:""} and pass this into d3 so that they can understand and render chart.
var input = {'a':1,"b":2};
function parseData(input){
return Object.keys(input).reduce(function(output, key){
output.push({"key":key,"value":input[key]});
return output;
},[])
}
console.log(parseData(input));
// [{"key":"a","value":1},{"key":"b","value":2}]
jsFiddle demo - https://jsfiddle.net/1kdsoyg2/1/
been stuck on this all morning. I've been following this example
I've been trying to get this to work in React but sadly no luck. I was able to read the csv in before and console log the data so I know it's finding the right file. What am I missing here?
class Linegraph extends Component {
componentDidMount() {
var parseDate = d3.timeParse("%m/%d/%Y");
var margin = { left: 50, right: 20, top: 20, bottom: 50 };
var width = 960 - margin.left - margin.right;
var height = 500 - margin.top - margin.bottom;
var max = 0;
var xNudge = 50;
var yNudge = 20;
var minDate = new Date();
var maxDate = new Date();
d3.csv("MOCK_DATA.csv")
.row(function(d) {
return {
month: parseDate(d.month),
price: Number(d.price.trim().slice(1))
};
})
.get(function(error, rows) {
max = d3.max(rows, function(d) {
return d.price;
});
minDate = d3.min(rows, function(d) {
return d.month;
});
maxDate = d3.max(rows, function(d) {
return d.month;
});
.................
(note: this question is not a duplicate of the existing Q/A about the new d3.fetch module because it uses a row function, not covered on those Q/A)
Since you are using D3 v5, you have to change the XmlHttpRequest pattern of v4 to the new Promises pattern of v5 (see the documentation here).
So, your code should be:
d3.csv("MOCK_DATA.csv", function(d) {
return {
month: parseDate(d.month),
price: Number(d.price.trim().slice(1))
};
})
.then(function(rows) {
//etc...
});
Pay attention to the fact that the row function goes inside the d3.csv function, as the second argument.
What I want to do :
I would like to create a pie chart with the javascript library d3.js. I get data from my server with an AJAX call, and then create my piechart with it.
What I've done :
For this I created 2 react component : one which implements the ajax call and pass his state to the other component which create the pie chart with the data in props.
First component :
import React from 'react';
import Pie from './pie.jsx';
class RingChart extends React.Component {
loadPieChart() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: (data) => {
let d3data = Object.keys(data)
.filter(key => key!= 'pourcentage')
.map(key => { return {name: key, value: data[key]} });
this.setState({dataPie: d3data, percent: data['pourcentage']});
},
error: (xhr, status, err) => {
console.error(this.props.url, status, err.toString());
}
});
}
constructor(props) {
super(props);
this.state = {dataPie: [], percent: 0};
}
componentDidMount() {
this.loadPieChart();
setInterval(this.loadCommentsFromServer, this.props.pollInterval);
}
render() {
return (
<div className="chart">
<Pie size="400" data={this.state.dataPie} percent={this.state.percent}></Pie>
</div>
);
}
}
export default RingChart;
Here my second component which create my pie chart :
import React from 'react';
class Pie extends React.Component {
constructor(props) {
super(props);
}
componentDidUpdate() {
let size = this.props.size;
const data = this.props.data;
let percent = this.props.percent;
console.log(data);
var margin = {top: 20, right: 20, bottom: 20, left: 20};
let width = size - margin.left - margin.right;
let height = width - margin.top - margin.bottom;
var chart = d3.select(".ring")
.append('svg')
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + ((width/2)+margin.left) + "," + ((height/2)+margin.top) + ")");
var radius = Math.min(width, height) / 2;
var color = d3.scale.ordinal()
.range(["#313F46", "#4DD0E1"]);
var arc = d3.svg.arc()
.outerRadius(radius)
.innerRadius(radius - 20);
var pie = d3.layout.pie()
.sort(null)
.startAngle(1.1*Math.PI)
.endAngle(3.1*Math.PI)
.value(function(d) { return d.value; });
var g = chart.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("className", "arc");
function tweenPie(b) {
var i = d3.interpolate({startAngle: 1.1*Math.PI, endAngle: 1.1*Math.PI}, b);
return function(t) { return arc(i(t)); };
}
g.append("path")
.attr("fill", function(d, i) { return color(i); })
.transition()
.ease("exp")
.duration(600)
.attrTween("d", tweenPie);
g.append("text")
.attr("dy", ".35em")
.style("text-anchor", "middle")
.style("color", "#263238")
.style("font-size", "55px")
.attr("className", "text-pie")
.text(function(d) { return percent+'%'; });
}
render() {
return (<div className="ring" style={{textAlign:'center'}}></div>)
}
}
export default Pie;
My problem :
Nothing is created ... The console.log(data) in my second component return firstly an empty array and secondly my array with the correct values. It's weird that nothing is created, but even if it was created, my component Pie is call twice.
How can I call my component only once ?
Why my component is not created ?
How can I do for that my pie chart will be update automatically when new values appears on my server ?
thank's a lot, I'm discovering react.js, a lot of basic's notions is unknow for me.
EDIT : Ok now my component is created, I had forgotten the ".ring" in my line : var chart = d3.select("ring") -_-.
But as I said before, now two component are created (one empty, and one correct). Sometimes I have two components created correctly. It depends on the AJAX call ... How can I solve the problem of the async AJAX call ?
React basically takes the props and state of a component and when these changes it updates the component (it executes the render function again and then diffs this with your actual DOM).
You tap into the lifecycle with the ComponentDidUpdate method. This will be called every time React updates a component. Therefore, the first time it's called your array is empty (your ajax call hasn't returned). Then your call returns, which results in the array being filled. React updates your component and you get the updated data. The fact you see the log twice is simply because, according to React, your component should be updated: Once with an empty array, and once with the results in the filled array.
To see if you actually create two components, check out either the constructor or the ComponentDidMount methods, logging in there should only be called once per intended creation of the component.
To summarize: it's no problem that the ComponentDidUpdate call is called more than once.
I'm working on a bar chart. Strangely enough, the bar chart works fine for unique data, but when updated with data that is exactly the same, the graph does not update. It is not a scale/spacing problem, as the rect's representing the data aren't being generated at all.
Here is a JSFiddle demonstrating my problem.
More information
The bar chart is given data in the following form:
data.push({
temperature: 10,
humidity: 20,
light: 30
});
The bar chart creates a separate bar for each attribute. The attributes are represented by this type object, to help with spacing and naming the bars:
// type objects
typeObject = {
temperature: {
string: "temperature", //type name
class: "temp", //css class for styling
initDis: 0, //defines where to place first bar
dis: 3, //defines space between the next temp bar
base: tempBase, //the <g> element base
color: "#A6E22E" //color
},
humidity: {
...
},
...
};
And, every time data is added, these type objects are iterated through and passed into the following function, to make a d3 'update pattern':
var createBarsForCategory = function (type) {
var bars;
//`data` has been updated to an array containing newly added data
bars = type.base.selectAll("rect")
.data(data, function(d) {
return d[type.string];
});
//general update transition
bars.transition().duration(500).attr("x", function(d, i) {
return i * (m.bar.w + m.bar.space) * type.dis + m.bar.w * type.initDis;
});
// enter
bars.enter()
.insert("rect")
.attr("class", type.class)
.attr("width", m.bar.w)
.attr("height", function (d) {
return 0;
})
.attr("x", function (d, i) {
return i * (m.bar.w + m.bar.space) * type.dis + m.bar.w * type.initDis;
})
.attr("y", function (d) {
return scales.yscale(0);
})
.transition().delay(100).duration(1000)
.attr("height", function (d) {
return scales.yscale(0) - scales.yscale(d[type.string]);
})
.attr("y", function (d) {
return scales.yscale(d[type.string]);
})
;
// remove
bars.exit().remove();
};
Thank you in advance for your time.
I have updated your fiddle http://jsfiddle.net/pyLh7tcn/33/ with the following code on lines 173-178:
bars = type.base.selectAll("rect")
.data(data);
Now both random and uniform data sets are plotting properly.
Hope this helps.