I am trying to implement a D3-delaunay on a linechart that has circles as points through which the line crosses. I think i am doing everything right, but i cant seem to see the overlay lines that show up. What am i doing wrong here? i am using the code on this site (https://observablehq.com/#didoesdigital/9-may-2020-d3-scatterplot-with-voronoi-tooltips) as a reference and to implement it on my linechart. If this reference is not suitable, than please provide another template that is similar to scatter plot which I can implement.
class Linechart extends React.Component {
constructor(props) {
super(props)
this.createLineChart = this.createLineChart.bind(this)
}
metricToPercent(metric) {
return (metric / 2 + 0.5) * 100;
};
scoreToDescrip(score) {
if (score >= 0.6) {
return "Good";
} else if (score >= 0) {
return "Average";
} else {
return "Poor";
}
};
componentDidMount() {
this.createLineChart()
}
componentDidUpdate() {
this.createLineChart()
}
createLineChart() {
var margin = {
top: 85,
right: 60,
bottom: 60,
left: 80
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var node = this.node
var divObj = d3.select(node)
var svgObj = divObj
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//Read the data
d3.json("https://raw.githubusercontent.com/QamarFarooq/data-for-testing/main/experience_scores.json").then(function(data) {
var dataArrayWithRemovedEmptyDates = []
// Remove all elements in the array that dont have companyReviewDate property
data.forEach(function(d, i) {
if (d.hasOwnProperty('companyReviewDate')) {
dataArrayWithRemovedEmptyDates.push(d)
}
})
// Transform `companyReviewDate` into an actual date
dataArrayWithRemovedEmptyDates.forEach(function(d) {
d.companyReviewDate = new Date(d.companyReviewDate);
})
// Remove all whitespace in CompanyBusinessName, it creates problems
data.forEach(function(d, i) {
d.companyBusinessName = d.companyBusinessName.split(' ').join('');
})
// group the data: I want to draw one line per group
var sumstat = d3.nest()
.key(function(d) {
return d.companyBusinessName;
})
.entries(dataArrayWithRemovedEmptyDates);
console.log(sumstat)
// Define the div for the tooltip
var tooltip = divObj
.append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden")
.style("background-color", "white")
.style("box-shadow", "0 0 4px #000000")
.style("padding", "10px")
const monthNames = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
var d = new Date();
tooltip.append("div")
.attr("class", "tooltipDate")
.html(monthNames[d.getMonth()] + " " + "(" + d.getFullYear() + ")")
.style("font-size", "20px")
.style("text-align", "center")
tooltip.append("div")
.attr("class", "tooltipName")
.style("text-align", "center")
.style("color", "grey")
tooltip.append("div")
.attr("class", "tooltipTitle")
.style("text-align", "center")
.html("Customer Sentiment")
.style("padding-top", "10px")
tooltip.append("div")
.attr("class", "tooltipScore")
.style("text-align", "center")
.style("color", 'DarkGrey')
.style("font-size", "20px")
tooltip.append("div")
.attr("class", "tooltipPerception")
.style("text-align", "center")
// Add title for linechart
svgObj.append("text")
.attr("text-anchor", "end")
.attr("font-size", 25)
.attr("x", 110)
.attr("y", -50)
.text("Customer Experience Score");
// Add X axis --> it is a date format
var x = d3.scaleTime()
.domain(d3.extent(data, function(d) {
return d.companyReviewDate;
}))
.range([0, width]);
svgObj.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("stroke-width", "0.3")
.style("opacity", "0.5")
.call(d3.axisBottom(x).tickSize(-height).tickFormat('').ticks(6))
// ticks
svgObj.append("g")
.style("opacity", "0.7")
.style("font", "14px times")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).ticks(5));
// Add Y axis
var y = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {
return +d.customerExperienceScore;
})])
.range([height, 0]);
svgObj.append("g")
.attr("stroke-width", "0.3")
.style("opacity", "0.5")
.call(d3.axisLeft(y).tickSize(-width).tickFormat('').ticks(5))
// ticks
svgObj.append("g")
.style("opacity", "0.7")
.style("font", "14px times")
.call(d3.axisLeft(y).ticks(5));
// Add X axis label:
svgObj.append("text")
.attr("text-anchor", "end")
.attr("font-size", 20)
.attr("x", width / 2 + margin.left)
.attr("y", height + 50)
.style("fill", d3.color("grey"))
.text("Company Review Date");
// Add Y axis label:
svgObj.append("text")
.attr("text-anchor", "end")
.attr("font-size", 20)
.attr("transform", "rotate(-90)")
.attr("x", -height / 2 + 40)
.attr("y", -margin.left + 25)
.style("fill", d3.color("grey"))
.text("Customer Experience Score")
// color palette
var key = sumstat.map(function(d) {
return d.key
}) // list of group names
var color = d3.scaleOrdinal()
.domain(key)
.range(['#e41a1c', '#377eb8', '#4daf4a'])
// Add one DOT in the legend for each name.
svgObj.selectAll(".dots")
.data(key)
.enter()
.append("circle")
.attr("cx", function(d, i) {
return 250 + i * 120
})
.attr("cy", -30)
.attr("r", 7)
.style("fill", function(d) {
return color(d)
})
// Add LABEL for legends of each dot.
svgObj.selectAll(".labels")
.data(key)
.enter()
.append("text")
.style("fill", d3.color("grey"))
.attr("x", function(d, i) {
return 270 + i * 120
})
.attr("y", -28)
.text(function(d) {
return d
})
.attr("text-anchor", "left")
.style("alignment-baseline", "middle")
// Highlight individual line and show tooltip
var highlightAndShowTooltip = function(d) {
// this means i am on top of dot circle
if (d.key == null) {
console.log("I am on top of a dot circle of key " + d.companyBusinessName)
//show tooltip
tooltip.style("visibility", "visible")
//Data for Tooltip
tooltip.selectAll(".tooltipName")
.html(d.key)
var score = 12 //this will be dynamic, for now i just set it to 12 to test it out
tooltip.selectAll(".tooltipScore")
.html("<span style='color: #cb9f9e;'>" + score + "</span> / 100")
// first every group turns grey
svgObj.selectAll(".line")
.transition().duration(200)
.style("opacity", "0.5")
svgObj.selectAll(".dot")
.transition().duration(200)
.style("opacity", "0.5")
// Second the hovered line takes its color
svgObj.selectAll("." + d.companyBusinessName)
.transition().duration(200)
.style("stroke", color(d.companyBusinessName))
.style("opacity", "1")
svgObj.selectAll("." + d.companyBusinessName)
.transition().duration(200)
.style("stroke", color(d.companyBusinessName))
.style("opacity", "1")
}
// this means i am on top of line
else if (d.companyBusinessName == null) {
var selected_line = d.key
console.log("i am on top of line " + d.key)
// first every group turns grey
svgObj.selectAll(".line")
.transition().duration(200)
.style("opacity", "0.5")
svgObj.selectAll(".dot")
.transition().duration(200)
.style("opacity", "0.5")
// Second the hovered line takes its color
svgObj.selectAll("." + selected_line)
.transition().duration(200)
.style("stroke", color(selected_line))
.style("opacity", "1")
svgObj.selectAll("." + selected_line)
.transition().duration(200)
.style("stroke", color(selected_line))
.style("opacity", "1")
}
}
// UnHighlight and hide tooltip
var doNotHighlightAndHideTooltip = function(d) {
//hide tooltip
tooltip.style("visibility", "hidden")
//return other lines back to normal opacity
svgObj.selectAll(".line")
.transition().duration(200).delay(50)
.style("stroke", function(d) {
return (color(d.key))
})
.style("opacity", "1")
svgObj.selectAll(".dot")
.transition().duration(200).delay(50)
.style("stroke", function(d) {
return (color(d.companyBusinessName))
})
.style("opacity", "1")
}
// keep showing tooltip as cursor moves along line
var keepShowingTooltip = function(d) {
tooltip.style("top", (d3.event.pageY - 10) + "px").style("left", (d3.event.pageX + 10) + "px")
}
// Draw the line
svgObj.selectAll(".line")
.data(sumstat)
.enter()
.append("path")
.attr("class", function(d) {
return "line " + d.key
}) // 2 class for each line: 'line' and the group name
.attr("fill", "none")
.attr("stroke", function(d) {
return color(d.key)
})
.attr("stroke-width", 4.5)
.attr("d", function(d) {
return d3.line()
.curve(d3.curveMonotoneX)
.x(function(d) {
return x(d.companyReviewDate);
})
.y(function(d) {
return y(+d.customerExperienceScore);
})
(d.values)
});
// This is the experiment with voronoi
// Draw dots on points
svgObj.selectAll(".dot")
//i am using the raw data array, NOT the nested array
.data(dataArrayWithRemovedEmptyDates)
.enter()
.append("circle")
.attr("class", function(d, i) {
return i;
})
.style("fill", "white")
.style("stroke-width", "3px")
.style("stroke", function(d) {
return color(d.companyBusinessName)
})
.attr("cx", function(d) {
return x(d.companyReviewDate);
})
.attr("cy", function(d) {
return y(d.customerExperienceScore);
})
.attr("r", 5.5)
var voronoi = d3.Delaunay
.from(dataArrayWithRemovedEmptyDates, d => x(d.x), d => y(d.y))
.voronoi([margin.left, margin.top, width - margin.right, height - margin.bottom]); // ensures voronoi is limited to the
//Create the voronoi grid
svgObj.append("g")
.attr("class", "voronoiWrapper")
.selectAll("path")
.data(dataArrayWithRemovedEmptyDates)
.enter()
.append("path")
.attr("opacity", 0.5)
.attr("stroke", "#ff1493") // Hide overlay
.attr("fill", "none")
.style("pointer-events", "all")
.attr("d", (d, i) => voronoi.renderCell(i))
.on("mouseover", highlightAndShowTooltip)
.on("mouseout", doNotHighlightAndHideTooltip);
})
}
render() {
return <div ref = {
node => this.node = node
}
className = "example_div" > < /div>
}
}
ReactDOM.render( <Linechart / >, document.querySelector('body'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/d3-delaunay#5.3.0/dist/d3-delaunay.min.js"></script>
You didn't look at your data structure well enough: you don't use d.x and d.y, you have your own names for those:
.from(dataArrayWithRemovedEmptyDates, d => x(d.companyReviewDate), d => y(d.customerExperienceScore))
I also removed the margins from the Voronoi. The reasoning is that svgObj is not an svg for you, but a g with a transform. That means that you already applied the margins correctly, so if you add them here, you apply them twice. All this yields the following result:
class Linechart extends React.Component {
constructor(props) {
super(props)
this.createLineChart = this.createLineChart.bind(this)
}
metricToPercent(metric) {
return (metric / 2 + 0.5) * 100;
};
scoreToDescrip(score) {
if (score >= 0.6) {
return "Good";
} else if (score >= 0) {
return "Average";
} else {
return "Poor";
}
};
componentDidMount() {
this.createLineChart()
}
componentDidUpdate() {
this.createLineChart()
}
createLineChart() {
var margin = {
top: 85,
right: 60,
bottom: 60,
left: 80
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var node = this.node
var divObj = d3.select(node)
var svgObj = divObj
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//Read the data
d3.json("https://raw.githubusercontent.com/QamarFarooq/data-for-testing/main/experience_scores.json").then(function(data) {
var dataArrayWithRemovedEmptyDates = []
// Remove all elements in the array that dont have companyReviewDate property
data.forEach(function(d, i) {
if (d.hasOwnProperty('companyReviewDate')) {
dataArrayWithRemovedEmptyDates.push(d)
}
})
// Transform `companyReviewDate` into an actual date
dataArrayWithRemovedEmptyDates.forEach(function(d) {
d.companyReviewDate = new Date(d.companyReviewDate);
})
// Remove all whitespace in CompanyBusinessName, it creates problems
data.forEach(function(d, i) {
d.companyBusinessName = d.companyBusinessName.split(' ').join('');
})
// group the data: I want to draw one line per group
var sumstat = d3.nest()
.key(function(d) {
return d.companyBusinessName;
})
.entries(dataArrayWithRemovedEmptyDates);
console.log(sumstat)
// Define the div for the tooltip
var tooltip = divObj
.append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden")
.style("background-color", "white")
.style("box-shadow", "0 0 4px #000000")
.style("padding", "10px")
const monthNames = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
var d = new Date();
tooltip.append("div")
.attr("class", "tooltipDate")
.html(monthNames[d.getMonth()] + " " + "(" + d.getFullYear() + ")")
.style("font-size", "20px")
.style("text-align", "center")
tooltip.append("div")
.attr("class", "tooltipName")
.style("text-align", "center")
.style("color", "grey")
tooltip.append("div")
.attr("class", "tooltipTitle")
.style("text-align", "center")
.html("Customer Sentiment")
.style("padding-top", "10px")
tooltip.append("div")
.attr("class", "tooltipScore")
.style("text-align", "center")
.style("color", 'DarkGrey')
.style("font-size", "20px")
tooltip.append("div")
.attr("class", "tooltipPerception")
.style("text-align", "center")
// Add title for linechart
svgObj.append("text")
.attr("text-anchor", "end")
.attr("font-size", 25)
.attr("x", 110)
.attr("y", -50)
.text("Customer Experience Score");
// Add X axis --> it is a date format
var x = d3.scaleTime()
.domain(d3.extent(data, function(d) {
return d.companyReviewDate;
}))
.range([0, width]);
svgObj.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("stroke-width", "0.3")
.style("opacity", "0.5")
.call(d3.axisBottom(x).tickSize(-height).tickFormat('').ticks(6))
// ticks
svgObj.append("g")
.style("opacity", "0.7")
.style("font", "14px times")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).ticks(5));
// Add Y axis
var y = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {
return +d.customerExperienceScore;
})])
.range([height, 0]);
svgObj.append("g")
.attr("stroke-width", "0.3")
.style("opacity", "0.5")
.call(d3.axisLeft(y).tickSize(-width).tickFormat('').ticks(5))
// ticks
svgObj.append("g")
.style("opacity", "0.7")
.style("font", "14px times")
.call(d3.axisLeft(y).ticks(5));
// Add X axis label:
svgObj.append("text")
.attr("text-anchor", "end")
.attr("font-size", 20)
.attr("x", width / 2 + margin.left)
.attr("y", height + 50)
.style("fill", d3.color("grey"))
.text("Company Review Date");
// Add Y axis label:
svgObj.append("text")
.attr("text-anchor", "end")
.attr("font-size", 20)
.attr("transform", "rotate(-90)")
.attr("x", -height / 2 + 40)
.attr("y", -margin.left + 25)
.style("fill", d3.color("grey"))
.text("Customer Experience Score")
// color palette
var key = sumstat.map(function(d) {
return d.key
}) // list of group names
var color = d3.scaleOrdinal()
.domain(key)
.range(['#e41a1c', '#377eb8', '#4daf4a'])
// Add one DOT in the legend for each name.
svgObj.selectAll(".dots")
.data(key)
.enter()
.append("circle")
.attr("cx", function(d, i) {
return 250 + i * 120
})
.attr("cy", -30)
.attr("r", 7)
.style("fill", function(d) {
return color(d)
})
// Add LABEL for legends of each dot.
svgObj.selectAll(".labels")
.data(key)
.enter()
.append("text")
.style("fill", d3.color("grey"))
.attr("x", function(d, i) {
return 270 + i * 120
})
.attr("y", -28)
.text(function(d) {
return d
})
.attr("text-anchor", "left")
.style("alignment-baseline", "middle")
// Highlight individual line and show tooltip
var highlightAndShowTooltip = function(d) {
// this means i am on top of dot circle
if (d.key == null) {
console.log("I am on top of a dot circle of key " + d.companyBusinessName)
//show tooltip
tooltip.style("visibility", "visible")
//Data for Tooltip
tooltip.selectAll(".tooltipName")
.html(d.key)
var score = 12 //this will be dynamic, for now i just set it to 12 to test it out
tooltip.selectAll(".tooltipScore")
.html("<span style='color: #cb9f9e;'>" + score + "</span> / 100")
// first every group turns grey
svgObj.selectAll(".line")
.transition().duration(200)
.style("opacity", "0.5")
svgObj.selectAll(".dot")
.transition().duration(200)
.style("opacity", "0.5")
// Second the hovered line takes its color
svgObj.selectAll("." + d.companyBusinessName)
.transition().duration(200)
.style("stroke", color(d.companyBusinessName))
.style("opacity", "1")
svgObj.selectAll("." + d.companyBusinessName)
.transition().duration(200)
.style("stroke", color(d.companyBusinessName))
.style("opacity", "1")
}
// this means i am on top of line
else if (d.companyBusinessName == null) {
var selected_line = d.key
console.log("i am on top of line " + d.key)
// first every group turns grey
svgObj.selectAll(".line")
.transition().duration(200)
.style("opacity", "0.5")
svgObj.selectAll(".dot")
.transition().duration(200)
.style("opacity", "0.5")
// Second the hovered line takes its color
svgObj.selectAll("." + selected_line)
.transition().duration(200)
.style("stroke", color(selected_line))
.style("opacity", "1")
svgObj.selectAll("." + selected_line)
.transition().duration(200)
.style("stroke", color(selected_line))
.style("opacity", "1")
}
}
// UnHighlight and hide tooltip
var doNotHighlightAndHideTooltip = function(d) {
//hide tooltip
tooltip.style("visibility", "hidden")
//return other lines back to normal opacity
svgObj.selectAll(".line")
.transition().duration(200).delay(50)
.style("stroke", function(d) {
return (color(d.key))
})
.style("opacity", "1")
svgObj.selectAll(".dot")
.transition().duration(200).delay(50)
.style("stroke", function(d) {
return (color(d.companyBusinessName))
})
.style("opacity", "1")
}
// keep showing tooltip as cursor moves along line
var keepShowingTooltip = function(d) {
tooltip.style("top", (d3.event.pageY - 10) + "px").style("left", (d3.event.pageX + 10) + "px")
}
// Draw the line
svgObj.selectAll(".line")
.data(sumstat)
.enter()
.append("path")
.attr("class", function(d) {
return "line " + d.key
}) // 2 class for each line: 'line' and the group name
.attr("fill", "none")
.attr("stroke", function(d) {
return color(d.key)
})
.attr("stroke-width", 4.5)
.attr("d", function(d) {
return d3.line()
.curve(d3.curveMonotoneX)
.x(function(d) {
return x(d.companyReviewDate);
})
.y(function(d) {
return y(+d.customerExperienceScore);
})
(d.values)
});
// This is the experiment with voronoi
// Draw dots on points
svgObj.selectAll(".dot")
//i am using the raw data array, NOT the nested array
.data(dataArrayWithRemovedEmptyDates)
.enter()
.append("circle")
.attr("class", function(d, i) {
return i;
})
.style("fill", "white")
.style("stroke-width", "3px")
.style("stroke", function(d) {
return color(d.companyBusinessName)
})
.attr("cx", function(d) {
return x(d.companyReviewDate);
})
.attr("cy", function(d) {
return y(d.customerExperienceScore);
})
.attr("r", 5.5)
var voronoi = d3.Delaunay
.from(dataArrayWithRemovedEmptyDates, d => x(d.companyReviewDate), d => y(d.customerExperienceScore))
.voronoi([0, 0, width, height]); // ensures voronoi is limited to the
console.log(voronoi, dataArrayWithRemovedEmptyDates);
//Create the voronoi grid
svgObj.append("g")
.attr("class", "voronoiWrapper")
.selectAll("path")
.data(dataArrayWithRemovedEmptyDates)
.enter()
.append("path")
.attr("opacity", 0.5)
.attr("stroke", "#ff1493") // Hide overlay
.attr("fill", "none")
.style("pointer-events", "all")
.attr("d", (d, i) => {
return voronoi.renderCell(i);
})
.on("mouseover", highlightAndShowTooltip)
.on("mouseout", doNotHighlightAndHideTooltip);
})
}
render() {
return <div ref = {
node => this.node = node
}
className = "example_div" > < /div>
}
}
ReactDOM.render( <Linechart / >, document.querySelector('body'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/d3-delaunay#5.3.0/dist/d3-delaunay.min.js"></script>
Edit: due to sizing, probably easiest to view the snippet full screen. Also, use buttons to see what each bars actual color is, which may be helpful.
I am working on an interactive, comparative histogram in D3, however I am struggling with setting the colors appropriately. See below for what I have so far.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedName: 'none'
}
this.chartProps = {
myLightGrey: '#EEE',
myMidGrey: '#888',
myDarkGrey: '#333',
chartWidth: 800, // Dont Change These, For Viewbox
chartHeight: 480, // Dont Change These, For Viewbox
padding: 80,
margin: {top: 20, right: 20, bottom: 30, left: 40},
logoRadius: 800 / 36,
svgID: "nbaStatHistograms"
};
}
// Helper Functions
generateStatData() {
var data = [{"Pos":"PG","teamAbb":"GSW","PTS":37,"REB":6,"AST":3,"STL":0,"BLK":0,"TOV":2,"X3PM":6,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":29,"REB":13,"AST":3,"STL":1,"BLK":3,"TOV":6,"X3PM":4,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":14,"REB":2,"AST":1,"STL":0,"BLK":1,"TOV":0,"X3PM":2,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":29,"REB":2,"AST":8,"STL":4,"BLK":0,"TOV":6,"X3PM":2,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":25,"REB":8,"AST":6,"STL":0,"BLK":1,"TOV":2,"X3PM":4,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":21,"REB":4,"AST":3,"STL":0,"BLK":1,"TOV":2,"X3PM":3,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":30,"REB":4,"AST":5,"STL":3,"BLK":1,"TOV":2,"X3PM":4,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":29,"REB":4,"AST":5,"STL":1,"BLK":0,"TOV":1,"X3PM":3,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":22,"REB":8,"AST":5,"STL":1,"BLK":1,"TOV":4,"X3PM":4,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":20,"REB":5,"AST":8,"STL":2,"BLK":0,"TOV":2,"X3PM":3,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":31,"REB":11,"AST":6,"STL":1,"BLK":2,"TOV":4,"X3PM":4,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":18,"REB":2,"AST":3,"STL":0,"BLK":2,"TOV":1,"X3PM":2,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":27,"REB":6,"AST":8,"STL":1,"BLK":0,"TOV":5,"X3PM":3,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":28,"REB":6,"AST":4,"STL":0,"BLK":2,"TOV":3,"X3PM":3,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":29,"REB":2,"AST":0,"STL":2,"BLK":0,"TOV":4,"X3PM":4,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":31,"REB":5,"AST":6,"STL":2,"BLK":1,"TOV":1,"X3PM":7,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":19,"REB":7,"AST":4,"STL":0,"BLK":0,"TOV":1,"X3PM":2,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":15,"REB":5,"AST":6,"STL":0,"BLK":0,"TOV":3,"X3PM":1,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":21,"REB":8,"AST":4,"STL":3,"BLK":0,"TOV":2,"X3PM":3,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":24,"REB":8,"AST":5,"STL":0,"BLK":3,"TOV":2,"X3PM":2,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":27,"REB":3,"AST":2,"STL":0,"BLK":1,"TOV":1,"X3PM":5,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":22,"REB":0,"AST":11,"STL":2,"BLK":0,"TOV":3,"X3PM":5,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":25,"REB":7,"AST":7,"STL":0,"BLK":2,"TOV":4,"X3PM":4,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":15,"REB":2,"AST":2,"STL":1,"BLK":0,"TOV":2,"X3PM":3,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":16,"REB":5,"AST":4,"STL":2,"BLK":0,"TOV":1,"X3PM":2,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":21,"REB":8,"AST":6,"STL":1,"BLK":3,"TOV":1,"X3PM":1,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":13,"REB":6,"AST":2,"STL":1,"BLK":2,"TOV":2,"X3PM":3,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":22,"REB":8,"AST":8,"STL":2,"BLK":1,"TOV":5,"X3PM":2,"fullName":"Stephen-Curry"},{"Pos":"SG","teamAbb":"GSW","PTS":28,"REB":5,"AST":3,"STL":1,"BLK":0,"TOV":1,"X3PM":6,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":22,"REB":4,"AST":9,"STL":1,"BLK":0,"TOV":2,"X3PM":3,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":29,"REB":5,"AST":4,"STL":2,"BLK":2,"TOV":4,"X3PM":3,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":23,"REB":1,"AST":2,"STL":1,"BLK":0,"TOV":1,"X3PM":4,"fullName":"Klay-Thompson"},{"Pos":"SF","teamAbb":"GSW","PTS":21,"REB":7,"AST":8,"STL":0,"BLK":0,"TOV":2,"X3PM":0,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":15,"REB":4,"AST":5,"STL":0,"BLK":0,"TOV":2,"X3PM":3,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":9,"REB":6,"AST":5,"STL":4,"BLK":0,"TOV":4,"X3PM":2,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":24,"REB":3,"AST":3,"STL":1,"BLK":2,"TOV":3,"X3PM":2,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":13,"REB":7,"AST":3,"STL":0,"BLK":1,"TOV":1,"X3PM":3,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":35,"REB":5,"AST":5,"STL":0,"BLK":1,"TOV":2,"X3PM":4,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":27,"REB":5,"AST":3,"STL":2,"BLK":2,"TOV":4,"X3PM":1,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":17,"REB":4,"AST":1,"STL":0,"BLK":0,"TOV":1,"X3PM":1,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":39,"REB":11,"AST":7,"STL":3,"BLK":0,"TOV":7,"X3PM":4,"fullName":"Stephen-Curry"},{"Pos":"SG","teamAbb":"GSW","PTS":23,"REB":10,"AST":1,"STL":2,"BLK":1,"TOV":2,"X3PM":1,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":24,"REB":5,"AST":6,"STL":1,"BLK":0,"TOV":6,"X3PM":3,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":21,"REB":5,"AST":2,"STL":2,"BLK":0,"TOV":4,"X3PM":3,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":9,"REB":4,"AST":2,"STL":0,"BLK":1,"TOV":1,"X3PM":3,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":33,"REB":7,"AST":4,"STL":2,"BLK":0,"TOV":2,"X3PM":4,"fullName":"Stephen-Curry"},{"Pos":"SG","teamAbb":"GSW","PTS":29,"REB":4,"AST":1,"STL":1,"BLK":1,"TOV":4,"X3PM":5,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":27,"REB":4,"AST":6,"STL":1,"BLK":0,"TOV":1,"X3PM":3,"fullName":"Stephen-Curry"},{"Pos":"SG","teamAbb":"GSW","PTS":24,"REB":4,"AST":2,"STL":2,"BLK":2,"TOV":0,"X3PM":4,"fullName":"Klay-Thompson"},{"Pos":"SG","teamAbb":"GSW","PTS":21,"REB":5,"AST":2,"STL":1,"BLK":1,"TOV":1,"X3PM":5,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":28,"REB":5,"AST":7,"STL":0,"BLK":0,"TOV":5,"X3PM":3,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":29,"REB":7,"AST":5,"STL":1,"BLK":1,"TOV":6,"X3PM":3,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":20,"REB":3,"AST":2,"STL":0,"BLK":0,"TOV":0,"X3PM":4,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":23,"REB":6,"AST":10,"STL":3,"BLK":0,"TOV":4,"X3PM":3,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":25,"REB":2,"AST":7,"STL":0,"BLK":4,"TOV":1,"X3PM":0,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":27,"REB":3,"AST":5,"STL":0,"BLK":0,"TOV":0,"X3PM":3,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":30,"REB":2,"AST":3,"STL":2,"BLK":0,"TOV":0,"X3PM":5,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":24,"REB":3,"AST":7,"STL":1,"BLK":0,"TOV":4,"X3PM":2,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":19,"REB":5,"AST":4,"STL":0,"BLK":0,"TOV":1,"X3PM":2,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":31,"REB":5,"AST":11,"STL":1,"BLK":0,"TOV":2,"X3PM":5,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":19,"REB":7,"AST":5,"STL":2,"BLK":1,"TOV":7,"X3PM":1,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":22,"REB":3,"AST":1,"STL":1,"BLK":0,"TOV":0,"X3PM":4,"fullName":"Klay-Thompson"},{"Pos":"SF","teamAbb":"GSW","PTS":35,"REB":11,"AST":10,"STL":0,"BLK":2,"TOV":3,"X3PM":3,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":22,"REB":4,"AST":3,"STL":0,"BLK":0,"TOV":3,"X3PM":4,"fullName":"Klay-Thompson"},{"Pos":"SF","teamAbb":"GSW","PTS":36,"REB":10,"AST":7,"STL":1,"BLK":5,"TOV":2,"X3PM":3,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":21,"REB":4,"AST":5,"STL":1,"BLK":0,"TOV":3,"X3PM":2,"fullName":"Klay-Thompson"},{"Pos":"SF","teamAbb":"GSW","PTS":28,"REB":9,"AST":5,"STL":0,"BLK":3,"TOV":4,"X3PM":2,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":24,"REB":3,"AST":3,"STL":1,"BLK":0,"TOV":4,"X3PM":4,"fullName":"Klay-Thompson"},{"Pos":"SF","teamAbb":"GSW","PTS":36,"REB":11,"AST":7,"STL":0,"BLK":2,"TOV":5,"X3PM":4,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":25,"REB":5,"AST":0,"STL":0,"BLK":0,"TOV":4,"X3PM":5,"fullName":"Klay-Thompson"},{"Pos":"SF","teamAbb":"GSW","PTS":36,"REB":11,"AST":8,"STL":1,"BLK":3,"TOV":2,"X3PM":4,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":17,"REB":10,"AST":2,"STL":0,"BLK":0,"TOV":2,"X3PM":4,"fullName":"Klay-Thompson"},{"Pos":"SF","teamAbb":"GSW","PTS":22,"REB":8,"AST":2,"STL":0,"BLK":2,"TOV":4,"X3PM":2,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":29,"REB":2,"AST":5,"STL":2,"BLK":0,"TOV":3,"X3PM":3,"fullName":"Klay-Thompson"},{"Pos":"SF","teamAbb":"GSW","PTS":33,"REB":7,"AST":7,"STL":2,"BLK":4,"TOV":1,"X3PM":3,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":16,"REB":1,"AST":1,"STL":1,"BLK":2,"TOV":2,"X3PM":2,"fullName":"Klay-Thompson"},{"Pos":"SF","teamAbb":"GSW","PTS":18,"REB":6,"AST":1,"STL":2,"BLK":2,"TOV":3,"X3PM":0,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":15,"REB":5,"AST":4,"STL":3,"BLK":0,"TOV":3,"X3PM":1,"fullName":"Klay-Thompson"},{"Pos":"SF","teamAbb":"GSW","PTS":25,"REB":7,"AST":3,"STL":2,"BLK":5,"TOV":4,"X3PM":3,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":24,"REB":7,"AST":2,"STL":0,"BLK":0,"TOV":0,"X3PM":4,"fullName":"Klay-Thompson"},{"Pos":"SF","teamAbb":"GSW","PTS":21,"REB":6,"AST":4,"STL":0,"BLK":3,"TOV":2,"X3PM":2,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":15,"REB":0,"AST":1,"STL":0,"BLK":1,"TOV":2,"X3PM":3,"fullName":"Klay-Thompson"},{"Pos":"SF","teamAbb":"GSW","PTS":27,"REB":4,"AST":6,"STL":0,"BLK":0,"TOV":3,"X3PM":4,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":24,"REB":3,"AST":2,"STL":0,"BLK":0,"TOV":2,"X3PM":3,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":38,"REB":4,"AST":3,"STL":0,"BLK":0,"TOV":0,"X3PM":10,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":20,"REB":5,"AST":9,"STL":1,"BLK":1,"TOV":1,"X3PM":2,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":21,"REB":0,"AST":5,"STL":0,"BLK":0,"TOV":3,"X3PM":5,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":32,"REB":5,"AST":8,"STL":1,"BLK":0,"TOV":3,"X3PM":6,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":25,"REB":12,"AST":6,"STL":0,"BLK":4,"TOV":2,"X3PM":1,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":25,"REB":5,"AST":4,"STL":2,"BLK":1,"TOV":1,"X3PM":3,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":29,"REB":9,"AST":5,"STL":1,"BLK":0,"TOV":3,"X3PM":4,"fullName":"Stephen-Curry"},{"Pos":"SG","teamAbb":"GSW","PTS":28,"REB":3,"AST":4,"STL":0,"BLK":0,"TOV":1,"X3PM":6,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":45,"REB":6,"AST":3,"STL":3,"BLK":0,"TOV":2,"X3PM":8,"fullName":"Stephen-Curry"},{"Pos":"SG","teamAbb":"GSW","PTS":10,"REB":4,"AST":3,"STL":1,"BLK":1,"TOV":2,"X3PM":2,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":32,"REB":4,"AST":9,"STL":2,"BLK":0,"TOV":6,"X3PM":5,"fullName":"Stephen-Curry"},{"Pos":"SG","teamAbb":"GSW","PTS":19,"REB":5,"AST":1,"STL":0,"BLK":2,"TOV":0,"X3PM":3,"fullName":"Klay-Thompson"},{"Pos":"SF","teamAbb":"GSW","PTS":40,"REB":4,"AST":4,"STL":1,"BLK":1,"TOV":5,"X3PM":6,"fullName":"Kevin-Durant"},{"Pos":"SF","teamAbb":"GSW","PTS":26,"REB":6,"AST":6,"STL":2,"BLK":0,"TOV":3,"X3PM":2,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":12,"REB":5,"AST":2,"STL":0,"BLK":0,"TOV":2,"X3PM":0,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":24,"REB":6,"AST":9,"STL":1,"BLK":0,"TOV":1,"X3PM":2,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":25,"REB":6,"AST":5,"STL":0,"BLK":1,"TOV":1,"X3PM":3,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":26,"REB":3,"AST":0,"STL":2,"BLK":0,"TOV":0,"X3PM":2,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":23,"REB":4,"AST":8,"STL":2,"BLK":0,"TOV":3,"X3PM":4,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":32,"REB":5,"AST":8,"STL":3,"BLK":1,"TOV":3,"X3PM":4,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":17,"REB":4,"AST":2,"STL":1,"BLK":1,"TOV":2,"X3PM":3,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":30,"REB":9,"AST":4,"STL":1,"BLK":0,"TOV":6,"X3PM":6,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":19,"REB":8,"AST":7,"STL":1,"BLK":0,"TOV":0,"X3PM":0,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":38,"REB":4,"AST":2,"STL":0,"BLK":1,"TOV":3,"X3PM":7,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":19,"REB":1,"AST":8,"STL":2,"BLK":0,"TOV":6,"X3PM":5,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":26,"REB":7,"AST":5,"STL":0,"BLK":2,"TOV":4,"X3PM":4,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":8,"REB":1,"AST":1,"STL":2,"BLK":0,"TOV":1,"X3PM":2,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":32,"REB":6,"AST":7,"STL":0,"BLK":0,"TOV":1,"X3PM":8,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":14,"REB":4,"AST":14,"STL":2,"BLK":2,"TOV":5,"X3PM":2,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":9,"REB":2,"AST":4,"STL":0,"BLK":0,"TOV":0,"X3PM":0,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":25,"REB":5,"AST":9,"STL":3,"BLK":0,"TOV":2,"X3PM":5,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":28,"REB":10,"AST":11,"STL":2,"BLK":3,"TOV":4,"X3PM":6,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":25,"REB":5,"AST":3,"STL":1,"BLK":1,"TOV":3,"X3PM":7,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":49,"REB":4,"AST":5,"STL":2,"BLK":0,"TOV":1,"X3PM":8,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":20,"REB":9,"AST":2,"STL":1,"BLK":1,"TOV":2,"X3PM":1,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":7,"REB":4,"AST":3,"STL":1,"BLK":2,"TOV":1,"X3PM":0,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":14,"REB":2,"AST":5,"STL":0,"BLK":0,"TOV":3,"X3PM":1,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":17,"REB":2,"AST":3,"STL":1,"BLK":0,"TOV":0,"X3PM":0,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":27,"REB":6,"AST":3,"STL":2,"BLK":0,"TOV":3,"X3PM":3,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":23,"REB":5,"AST":6,"STL":2,"BLK":0,"TOV":6,"X3PM":3,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":33,"REB":4,"AST":6,"STL":0,"BLK":0,"TOV":5,"X3PM":6,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":20,"REB":6,"AST":3,"STL":0,"BLK":0,"TOV":1,"X3PM":4,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":24,"REB":6,"AST":4,"STL":2,"BLK":0,"TOV":5,"X3PM":5,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":31,"REB":6,"AST":7,"STL":0,"BLK":2,"TOV":1,"X3PM":0,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":16,"REB":1,"AST":5,"STL":2,"BLK":0,"TOV":0,"X3PM":2,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":21,"REB":5,"AST":5,"STL":1,"BLK":1,"TOV":3,"X3PM":2,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":33,"REB":6,"AST":1,"STL":1,"BLK":1,"TOV":5,"X3PM":3,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":12,"REB":4,"AST":2,"STL":1,"BLK":0,"TOV":3,"X3PM":2,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":20,"REB":7,"AST":8,"STL":0,"BLK":0,"TOV":4,"X3PM":4,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":24,"REB":9,"AST":4,"STL":1,"BLK":2,"TOV":1,"X3PM":4,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":18,"REB":5,"AST":4,"STL":0,"BLK":0,"TOV":3,"X3PM":2,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":17,"REB":4,"AST":8,"STL":2,"BLK":0,"TOV":2,"X3PM":3,"fullName":"Stephen-Curry"},{"Pos":"SF","teamAbb":"GSW","PTS":10,"REB":6,"AST":6,"STL":0,"BLK":2,"TOV":1,"X3PM":0,"fullName":"Kevin-Durant"},{"Pos":"SG","teamAbb":"GSW","PTS":25,"REB":3,"AST":0,"STL":0,"BLK":0,"TOV":3,"X3PM":5,"fullName":"Klay-Thompson"},{"Pos":"PG","teamAbb":"GSW","PTS":22,"REB":9,"AST":7,"STL":0,"BLK":1,"TOV":3,"X3PM":3,"fullName":"Stephen-Curry"}];
return data;
}
round(number, precision) {
var factor = Math.pow(10, precision);
return Math.round(number * factor) / factor;
}
updateButtonColors(button, parent, self) {
const {myLightGrey, myMidGrey, myDarkGrey} = self.chartProps;
parent.selectAll("rect")
.attr("fill", myLightGrey)
parent.selectAll("text")
.attr("fill", myDarkGrey)
button.select("rect")
.attr("fill", myDarkGrey)
button.select("text")
.attr("fill", myLightGrey)
}
xScaleFunc(self) {
const {chartWidth, margin } = self.chartProps;
const statData = self.generateStatData();
// looks to be working correctly here
return d3.scaleLinear()
.domain(d3.extent(statData, stat => stat.AST)).nice()
.range([margin.left, chartWidth - margin.right]);
}
yScaleFunc(self, maxY) {
const { chartHeight, margin } = self.chartProps;
const statData = self.generateStatData();
return d3.scaleLinear()
.domain([0, maxY]).nice()
.range([chartHeight - margin.bottom, margin.top])
}
// Draw Graph Functions
drawHistogram() {
// 0. Setup | Scales and Other Constants
// =======================================
const { chartHeight, chartWidth, margin, myDarkGrey } = this.chartProps;
const { xScaleFunc, yScaleFunc } = this;
const xScale = xScaleFunc(this);
const statData = this.generateStatData()
const histogram = d3.histogram()
.domain(xScale.domain())
.thresholds(xScale.ticks(10))
// ======
// 1. Draw The Histogram For Each Guy
// ===================================
const histogramBars = d3.select('g.histogramBars');
const allPlayers = [... new Set(statData.map(row => row.fullName))]
// loop to find max Y for yScale
let maxY = 0;
for(var i = 0; i < allPlayers.length; i++) {
const playerStats = statData
.filter(row => row.fullName == allPlayers[i])
.map(row => row.AST)
const playerBinnedData = histogram(playerStats)
let thisYmax = d3.max(playerBinnedData, d => d.length)
maxY = (thisYmax > maxY) ? thisYmax : maxY;
}
const yScale = yScaleFunc(this, maxY);
// loop to create histogram for each player
const colors = ['yellow', 'red', 'blue'];
for(var i = 0; i < allPlayers.length; i++) {
const playerStats = statData
.filter(row => row.fullName == allPlayers[i])
.map(row => row.AST)
const playerBinnedData = histogram(playerStats)
const playerBars = histogramBars
.append("g")
.selectAll(`.rect-${allPlayers[i]}`)
.data(playerBinnedData)
.enter()
.append("rect")
.attr("class", `rect-${allPlayers[i]}`)
.attr("x", d => xScale(d.x0) + 1)
.attr("width", d => Math.max(0, xScale(d.x1) - xScale(d.x0) - 1))
.attr("y", d => yScale(d.length))
.attr("height", d => yScale(0) - yScale(d.length))
.attr("fill", colors[i])
.attr("stroke", myDarkGrey)
.attr("stroke-width", 2)
.attr("opacity", 0.5);
}
const xAxis = g => g
.attr("transform", `translate(0,${chartHeight - margin.bottom})`)
.call(d3.axisBottom(xScale).tickSizeOuter(0))
.call(g => g.append("text")
.attr("x", chartWidth - margin.right)
.attr("y", -4)
.attr("fill", "#000")
.attr("font-weight", "bold")
.attr("text-anchor", "end")
.text('X Axis Name Goes Here'))
histogramBars.append("g")
.call(xAxis);
const yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(yScale))
.call(g => g.select(".domain").remove())
.call(g => g.select(".tick:last-of-type text").clone()
.attr("x", 4)
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text('# of Games'))
histogramBars.append("g")
.call(yAxis);
}
drawNameHoverButtons() {
// Setup Constants
// ==================
const { myLightGrey, myMidGrey, myDarkGrey } = this.chartProps;
const { updateButtonColors } = this;
const bWidth= 75;
const bHeight= 25;
const bSpace= 5;
const statData = this.generateStatData()
const allPlayers = [... new Set(statData.map(row => row.fullName))]
const nameHoverButtons = d3.select('g.nameHoverButtons')
// ====
// Add buttons
// ==================
const self = this;
const buttonGroup = nameHoverButtons.selectAll("g.nameButton")
.data(allPlayers)
.enter()
.append("g")
.attr("class", "button")
.style("cursor", "pointer")
buttonGroup.append("rect")
.attr("class","buttonRect")
.attr("width",bWidth)
.attr("height",bHeight)
.attr("x", (d,i) => 500 + (bWidth+bSpace)*i)
.attr("y", 0)
.attr("rx", 4) // rx and ry give the buttons rounded corners
.attr("ry", 4)
.attr("fill", myLightGrey)
.attr("stroke", myDarkGrey)
.attr("stroke-width", "0.1em")
// adding text to each toggle button group
buttonGroup.append("text")
.attr("class","buttonText")
.attr("font-family", "arial")
.attr("font-size", "0.6em")
.attr("x", (d,i) => 500 + (bWidth+bSpace)*i + bWidth/2)
.attr("y", 10)
.attr("text-anchor","middle")
.attr("dominant-baseline","central")
.attr("fill", '#222')
.text(d => d)
buttonGroup
.on("click", function(d,i) {
updateButtonColors(d3.select(this), d3.select(this.parentNode), self)
self.setState({selectedName: allPlayers[i]})
})
.on("mouseover", function(d, i) {
if (d3.select(this).select("rect").attr("fill") != myDarkGrey) {
d3.select(this)
.select("rect")
.attr("fill", myMidGrey); // lol almost here keep trying
}
d3.selectAll(`g.histogramBars rect`)
.attr('opacity', 0.1)
d3.selectAll(`.rect-${allPlayers[i]}`)
.attr('z-index', 3)
.attr('opacity', 1)
})
.on("mouseout", function(d, i) {
if (d3.select(this).select("rect").attr("fill") != myDarkGrey) {
d3.select(this)
.select("rect")
.attr("fill", myLightGrey);
}
d3.selectAll(`g.histogramBars rect`)
.attr('opacity', 0.5)
d3.selectAll(`.rect-${allPlayers[i]}`)
.attr('z-index', 1)
.attr('opacity', 0.5)
})
}
drawStatButtons() {
}
// When component mounts, get
componentDidMount() {
const { chartHeight, chartWidth, svgID } = this.chartProps;
d3.select(`#${svgID}`)
.attr('width', '100%')
.attr('height', '100%')
.attr('viewBox', "0 0 " + chartWidth + " " + chartHeight)
.attr('preserveAspectRatio', "xMaxYMax");
this.drawHistogram()
this.drawNameHoverButtons()
}
render() {
const { svgID } = this.chartProps;
return (
<div ref="scatter">
<svg id={svgID}>
<g className="histogramBars" />
<g className="nameHoverButtons" />
<g className="points" />
</svg>
</div>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.8.0-alpha.1/d3-tip.js"></script>
<div id='root'>
FUCKING WORK
</div>
My issue I think is a simple one, but hard to describe typing, but I will try.
The 3 main colors used in this graph are red, blue and yellow. However, if you look in column 3, you will see blue, darker blue, and purple. This is because of the order of my for loop, where the histogram for yellow is plotted first, then the histogram for red, then the histogram for blue bars. Since the blue bars are plotted last, the color with by far the most emphasis is blue, even though I've used an opacity of 0.5.
For example, if you were to change the line in my code where I create the color array, by reordering the colors, you'll see a new emphasis placed on whichever color is plotted last.
Instead of blue, dark blue, purple, I would like to see the yellow, red and blue in each bar column. If it is the case for a particular histogram column that yellow has the lowest y value (say y=3), then red (y=5), then blue (y=8), I would like the color of emphasis for the bottom part of the bar (0-3) to be yellow, for the middle part of the bar (3-5) to be red, and for the top part of the bar to be blue (5-8).
Please let me know if I can elaborate more on what I'm looking for. Otherwise, appreciate in advance any help I can get with this!