I am building a stacked bar chart using d3 v4.4.4 in react.js v15.3.0. Below is the data I am using to build my stacked bar chart:
[
{ timestamp: "2006", source1: "20", source2: "20", source3: "20", source4: '20'},
{ timestamp: "2007", source1: "70", source2: "50", source3: "10", source4: '70'},
{ timestamp: "2008", source1: "80", source2: "50", source3: "60", source4: '40'},
{ timestamp: "2009", source1: "30", source2: "20", source3: "40", source4: '50'},
{ timestamp: "2010", source1: "70", source2: "20", source3: "90", source4: '20'}
]
I have gotten the stacked bar chart to show up seen here, but it is not showing the first object in the array of data. Instead the stacked bar chart duplicates the last object in the array of data and uses for both the first and last stacked bar in the chart.
const stack = d3.stack().keys(keys)
const layers = stack(chartDataWorkingCopy)
this was the result of logging layers to the console
As you can see from the screenshot above the first and last arrays are the same, and the first object (i.e. { timestamp: "2006", source1: "20", source2: "20", source3: "20", source4: '20'}) from the original data I used to build the chart is not there.
Can someone explain to me why d3 is skipping over the first object in the array and duplicating the last when building the stacked bar chart?
Below is the source code for the stacked bar chart component:
import React, { PropTypes } from 'react'
import * as d3 from 'd3'
import _ from 'lodash'
class StackedBarChart extends React.Component {
static propTypes = {
chartData: PropTypes.array,
barWidth: PropTypes.number,
barOffset: PropTypes.number,
chartHeight: PropTypes.number,
chartWidth: PropTypes.number
}
static defaultProps = {
chartData: [
{ timestamp: "2006", source1: "20", source2: "20", source3: "20", source4: '20'},
{ timestamp: "2007", source1: "70", source2: "50", source3: "10", source4: '70'},
{ timestamp: "2008", source1: "80", source2: "50", source3: "60", source4: '40'},
{ timestamp: "2009", source1: "30", source2: "20", source3: "40", source4: '50'},
{ timestamp: "2010", source1: "70", source2: "20", source3: "90", source4: '20'}
],
barWidth: 10,
barOffset: 5,
chartHeight: 200,
chartWidth: 300
}
componentDidMount() {
this.renderChart()
}
componentDidUpdate(prevProps){
if(!_.isEqual(this.props, prevProps)){
d3.select('svg').remove()
this.renderChart()
}
}
splitTheDifference(data) {
let str = String(data)
const points = str.split(',')
return (points[1] - points[0])
}
renderChart() {
const { chartData, barWidth, barOffset, chartHeight, chartWidth} = this.props
let chartDataWorkingCopy = [...chartData]
// keep the bars from going off the page
const maxBars = (chartWidth / (barWidth + 2))
if (chartDataWorkingCopy.length > maxBars) {
chartDataWorkingCopy = chartDataWorkingCopy.slice(-Math.floor(maxBars))
}
const keys = _.remove(_.keys(_.extend.apply({}, chartDataWorkingCopy)), (d) => {
return d !== 'timestamp'
})
const xScale = d3.scaleBand().range([0, chartWidth]).padding(0.1)
const yScale = d3.scaleLinear().range([chartHeight, 0])
const color = ['hsl(8, 82%, 50%)', 'hsl(76, 96%, 50%)', 'hsl(178, 99%, 50%)', 'hsl(302, 100%, 50%)', 'hsl(58, 98%, 50%)', 'hsl(144, 100%, 50%)']
const xAxis = d3.axisBottom(xScale).tickFormat(d3.timeFormat("%b"))
const yAxis = d3.axisLeft(yScale)
const width = chartWidth
const height = chartHeight
let svg = d3.select('.barChartContainer').append("svg")
.attr('width', width)
.attr('height', height)
.append('g')
const stack = d3.stack().keys(keys)
const layers = stack(chartDataWorkingCopy)
xScale.domain(chartDataWorkingCopy.map(function(d) { return d.timestamp }))
yScale.domain([0, d3.max(layers[layers.length - 1], function(d) {
return d[0] + d[1]
}) ]).nice()
const layer = svg.selectAll('.layer')
.data(layers)
.enter().append('g')
.attr('class', 'layer')
.style('fill', function(d, i) { return color[i] })
layer.selectAll("rect")
.data(function(d) {
return d
})
.enter().append("rect")
.attr("x", function(d, i) {
return i * 12
})
.attr("y", function(d) { return yScale(d[1]) })
.attr("height", function(d) { return yScale(d[0]) - yScale(d[1]) })
.attr("width", 10)
.on('mouseover', (d) => {
d3.select('#tooltip')
.classed('hidden', false)
.style('position', 'absolute')
.style('background', '#333333')
.style('color', '#fff')
.style('padding', 10)
.style('left', `${d3.event.pageX}px`)
.style('top', `${d3.event.pageY - 80}px`)
.select('#value')
.text(this.splitTheDifference(d))
})
.on('mouseout', (d) => {
d3.select('#tooltip')
.classed('hidden', true)
})
}
render(){
return(
<div className={ 'barChartContainer' }>
<div id='tooltip' className='hidden'>
<p id='value'></p>
</div>
</div>
)
}
}
export default StackedBarChart
Well, d3.stack() is not skipping the first object in your data array.
The problem is that this code...
const keys = _.remove(_.keys(_.extend.apply({}, chartDataWorkingCopy)), (d) => {
return d !== 'timestamp'
})
... is not only getting the keys, but also modifying your chartDataWorkingCopy array, which still points to your chartData array.
Right now, you may think that you're cloning your original array here:
let chartDataWorkingCopy = [...chartData];
But, unfortunately, you're not. You cannot clone an array of objects with the spread operator. The documentation says:
Typically the spread operators in ES2015 goes one level deep while copying an array. Therefore, they are unsuitable for copying multidimensional arrays. It's the same case with Object.assign() and Object spread operators. Look at the example below for a better understanding.
So, in your code, any change to chartDataWorkingCopy also changes chartData.
Solution 01
You can get the keys without modifying the array, using vanilla JS or D3, you don't need lodash for this. For instance, this is a very simple way to get the keys you want using only D3 and without modifying the source array:
const keys = d3.keys(chartDataWorkingCopy[0]).filter(d => d != "timestamp");
Solution 02
If you want to keep your lodash function, use a method that really copies a deep array, like this:
let chartDataWorkingCopy = JSON.parse(JSON.stringify(chartData));
In this demo, I'm keeping your code (with lodash), just changing the code that makes a copy of the original array. Now the values are correct:
var chartData = [
{ timestamp: "2006", source1: "20", source2: "20", source3: "20", source4: '20'},
{ timestamp: "2007", source1: "70", source2: "50", source3: "10", source4: '70'},
{ timestamp: "2008", source1: "80", source2: "50", source3: "60", source4: '40'},
{ timestamp: "2009", source1: "30", source2: "20", source3: "40", source4: '50'},
{ timestamp: "2010", source1: "70", source2: "20", source3: "90", source4: '20'}
];
let chartDataWorkingCopy = JSON.parse(JSON.stringify(chartData));
const keys = _.remove(_.keys(_.extend.apply({}, chartDataWorkingCopy)), (d) => {
return d !== 'timestamp'
})
const stack = d3.stack().keys(keys)
const layers = stack(chartData);
console.log(layers);
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
PS: Copying an array with JSON.parse(JSON.stringify(array)) only works if you don't have actual date objects in that array. Right now, "2006", "2007", "2008" and so on are only strings, so this method works.
Related
I have the following code:
import { scaleTime } from "d3-scale";
import { timeFormat } from "d3-time-format";
const scale = scaleTime().domain([1625747708797, 1625747709671])
.range([0, 1317]);
const ticks = scale.ticks();
const tickFormat = scale.tickFormat(1, '%L');
console.log(ticks.map(tickFormat));
The console output is:
["800", "900", "000", "100", "200", "300", "400", "500", "600"]
Why it doesn't sort the list from start to end:
["000", "100", ...., "800"]
Imagine your dataset was instead over many years and you ran:
const tickFormat = scale.tickFormat(1, '%m');
ticks.map(tickFormat);
You'd get an array of strings of month numbers ([01-12]), but they'd be out of order because your data is in time order (years, then months, etc).
here i state with data
state = {
Response: [
{
"id": "15071",
"name": "John",
"salary": "53",
"age": "23",
"department": "admin"
},
{
"id": "15072",
"name": "maxr",
"salary": "53",
"age": "23",
"department": "admin"
},
{
"id": "15073",
"name": "Josef",
"salary": "53",
"age": "23",
"department": "admin"
},
{
"id": "15074",
"name": "Ye",
"salary": "53",
"age": "23",
"department": "admin"
}
]
i am displaying these records in the table. In table u will see 10 records and there will be a button on top of table so if append button is pressed then 10 records has to be added on every button press and the data has to be same but it has to be appended using the below logic i am trying to set the state by pushing 10 records and trying to append it for ex if i have 1,2,3,4,5,6,7,8,9,10 if i press append 1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10 has to be apeended
appendEmployees() {
var reLoadCount = 1;
for (let i = 0; i < 10; i++) {
const myObj = {
id: 0,
name: '',
salary: 0,
department: ''
};
myObj.id = +this.setState.employee[i].id + (reLoadCount * 10);
myObj.name = this.setState.employee[i].name;
myObj.salary = this.setState.employee[i].salary;
myObj.department = this.setState.employee[i].department;
this.setState.employee.push(myObj);
}
reLoadCount++;
}
am i doing some thing wrong here
If I get this right you're trying to add 10 duplicates of the objects in the this.state.employee array, the only difference between these new objects and the existing ones is their id.
If that is the case, here is how you can do that:
appendEmployees() {
this.setState(prevState => {
// Get the biggest ID number.
const maxId = Math.max(...prevState.employee.map(e => parseInt(e.id)));
// create 10 new employees copies of the first 10.
const newEmployees = prevState.employee.slice(0, 10).map((e, i) => ({
...e,
id: (maxId + i + 1)
}));
// return/update the state with a new array for "employee" that is a concatenation of the old array and the array of the 10 new ones.
return {
employee: [...prevState.employee, ...newEmployees]
}
});
}
I've added some comments to the example to explain what it does.
The important thing is this.setState which is the function used to update the state, here, I've used it with a function as the first parameter (it works with objects as well), I've did that because it is the preferred way of generating a new state that is derived from the old state.
I have two json observables I need to join as they originally come separated from another source so this is just an example as I'm trying to understand rxJS too.
var obs = Rx.Observable.from([
{ id: "1", url: "whatever.jpg"},
{ id: "2", url: "whatever2.jpg"},
{ id: "3", url: "whatever3.jpg"}
])
var list = Rx.Observable.from([
{ first: "Gary", last: "Simon", age: "34"},
{ first: "Jane", last: "Simon", age: "34"},
{ first: "Juan", last: "Simon", age: "34"}
])
// I tried this
var newObs = Rx.Observable.merge(list,obs)
// and this
var newObs = Rx.Observable.forkJoin(list,obs)
The point is I need a new json that looks roughly like this:
[{
"first": "Gary",
"last": "Simon",
"age": "34"
},
{
"id": "1",
"url": "whatever.jpg"
}]
How should I do that with RxJS?
Use zip:
Observable.zip(obs, list).subscribe(([obsItem, listItem]) => console.log(obsItem, listItem));
Notes:
The widely used convention is to add $ as a suffix to the observable names. e.g. obs$, list$
If the length of input observables is not the same. The result will take the length of the shortest one. e.g. obs has 3 items, list has 5 items, then the result observable will only emits 3 items and skip the last 2 items of list.
First, please excuse my poor english.
I'm working on a project with a grid-map and an external csv.
The grid-map is not composed of rectangles or hexagons but only with the centroid of any kind of symbol that will be use at the end.
So I have a Topojson file with centroids "ID" and centroids "Coordinates".
The external CSV is composed of several columns, the first one with the same centroids "ID" and the other one with value for different year.
"ID","C2001","C2002","C2003","C2004","C2005","C2006","C2007","C2008","C2009","C2010","C2000"
6050,"-5.55753","-5.55914","-5.75444","-5.76307","-5.81660","-5.99361","-6.02150","-6.15979","-5.73530","-6.30509","-5.52990"
6051,"-5.55753","-5.55914","-5.75444","-5.76307","-5.81660","-5.99361","-6.02150","-6.15979","-5.73530","-6.30509","-5.52990"
Here is my code
var width = 960,
height = 600;
var options = [
{date: "2000", selected: "+d.C2000"},
{date: "2001", selected: "+d.C2001"},
{date: "2002", selected: "+d.C2002"},
{date: "2003", selected: "+d.C2003"},
{date: "2004", selected: "+d.C2004"},
{date: "2005", selected: "+d.C2005"},
{date: "2006", selected: "+d.C2006"},
{date: "2007", selected: "+d.C2007"},
{date: "2008", selected: "+d.C2008"},
{date: "2009", selected: "+d.C2009"},
{date: "2010", selected: "+d.C2010"},
];
var color = d3.scale.threshold()
.domain([-1985, -1400, -1000, -700, -300, -100, -25, -0])
.range(["#7f0000", "#b30000", "#d7301f", "#ef6548", "#fc8d59", "#fdbb84", "#fdd49e", "#fee8c8", "#fff7ec"]);
var path = d3.geo.path()
.projection(null)
.pointRadius(1.5);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
queue()
.defer(d3.json, "CO2_light.json")
.defer(d3.csv, "tdd_CO2_emissions.csv")
.await(ready);
function ready(error, centroid, CO2) {
var rateById = {};
console.log(rateById); //To
//CO2.forEach(function(d) { rateById[+d.ID] = +d.C2000; }); WORKING FINE = value in rateById
CO2.forEach(function(d) { rateById[+d.ID] = options[0].selected; }); //NOT WORKING = inside rateById "+d.C2000" instead the value
svg.selectAll("path")
.data(topojson.feature(centroid, centroid.objects.CENTROID).features)
.enter().append("path")
.attr("class", "centerGrid")
.attr("d", path)
.style("fill", function(d) { return color(rateById[+d.properties.ID]); });
d3.select(".loading").remove();
For the moment I leave the button for my next problem and I'm focus on this two line below
//CO2.forEach(function(d) { rateById[+d.ID] = +d.C2000; }); WORKING FINE = value in rateById
CO2.forEach(function(d) { rateById[+d.ID] = options[0].selected; }); //NOT WORKING = inside rateById "+d.C2000" instead the value
If I use the first line I get a nice grid-map (see image) but if I'm trying to access at value of a specific year from the options array with the second line and do console.log(rateById); I get this
Object
6050: "+d.C2000"
6051: "+d.C2000"
6712: "+d.C2000"
Instead of this
Object
6050: -6.30509
6051: -6.30509
6712: -7.0441
Fixed here: http://jsfiddle.net/z7sLdyu2/2/
2 changes to do in your code:
First, your options select should not contain javascript code to be executed (+d.), but only the year values:
var options = [
{date: "2000", selected: "C2000"},
{date: "2001", selected: "C2001"},
{date: "2002", selected: "C2002"},
{date: "2003", selected: "C2003"},
{date: "2004", selected: "C2004"},
{date: "2005", selected: "C2005"},
{date: "2006", selected: "C2006"},
{date: "2007", selected: "C2007"},
{date: "2008", selected: "C2008"},
{date: "2009", selected: "C2009"},
{date: "2010", selected: "C2010"}
];
Then in the loop, assign the rateById value by accessing the data d property for the selected year like this:
rateById[+d.ID] = +d[options[0].selected];
P.S.: I had to remove part of your code in your jsfiddle, as the centroids json was not found in your version of the jsfiddle, leading to errors not related to the problem of this question.
I have a array of objects for use in D3 e.g
var cities = [
{ city: "London", country: "United Kingdom", index: 280 },
{ city: "Geneva", country: "Switzerland", index: 259 },
{ city: "New York City", country: "United States", index: 237 },
{ city: "Singapore", country: "Singapore", index: 228 },
{ city: "Paris", country: "France", index: 219 },
{ city: "San Francisco", country: "United States", index: 218 },
{ city: "Copenhagen", country: "Denmark", index: 217 },
{ city: "Sydney", country: "Australia", index: 215 },
{ city: "Hong Kong", country: "Hong Kong", index: 214 },
{ city: "Brisbane", country: "Australia", index: 208 }
}
I would like to order the objects in ascending order based on their cities.index property. So that I can display them as such in D3.js. Im sure there is a way of doing this in D3 but I am yet to figure it out when dealing with an array of objects.
Any help?
You can pass an anonymous function to the Javascript Array.prototype.sort to sort by index. D3 has a function d3.ascending (v 3.x) that makes it easy to sort ascending:
cities.sort(function(x, y){
return d3.ascending(x.index, y.index);
})
And here's the output:
[
{"city":"Brisbane","country":"Australia","index":208},
{"city":"Hong Kong","country":"Hong Kong","index":214},
{"city":"Sydney","country":"Australia","index":215},
{"city":"Copenhagen","country":"Denmark","index":217},
{"city":"San Francisco","country":"United States","index":218},
{"city":"Paris","country":"France","index":219},
{"city":"Singapore","country":"Singapore","index":228},
{"city":"New York City","country":"United States","index":237},
{"city":"Geneva","country":"Switzerland","index":259},
{"city":"London","country":"United Kingdom","index":280}
]
Just sort the array before you use it in D3, as Travis J mentioned in a comment. There's no reason to use D3 to sort (d3.ascending is just a comparison wrapper anyway).
Also, note that you have a } where you want a ] at the end of your declaration.
You can access each object's properties as so:
cities.sort(function(a, b){
return a["index"]-b["index"];
});
As #Casey Falk said d3.sort is an elegant method in d3 library. I code the following to solve my problem:
const axisGrid = svg.append("g")
const photoGroup = axisGrid.append("g")
.selectAll("g")
.data(photo_data)
.enter()
.append("g")
const photos = photoGroup.append("image");
photos.on("mouseenter", function(curItem) {
// selection.sort(compare) https://github.com/d3/d3-selection/blob/v3.0.0/README.md#selection_sort
photoGroup.sort((a, b) => {
if (a.photo == curItem.photo) { // make the current image element on top
return 1
} else {
return -1
}
})