D3 chart is duplicating on React.js component - reactjs

This is on a react application, what might be going wrong? A new svg is appended everytime, instead of the one that exists updating...
I'm not sure what other details I should add, I already included all the info that you may need to help me, please let me know if there's anything else I should add. Thank you.
This is my component, I'm passing the data through props.
export const FourDirectionsTimeChart = ({data}) => {
useEffect(() => {
const width = document.getElementById("container").clientWidth;
const height = document.getElementById("container").clientHeight;
const R = (width + height) / 8;
const CX = width / 2;
const CY = height / 2;
const smallR = R * 0.1;
const circleColor = "bisque";
const itemColor = "#3F4200";
let svg = d3.select("#container")
.append("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", `0 0 ${width} ${height}`)
let mainCircle = svg.append("circle")
.attr("fill", circleColor)
.attr("r", R)
.attr("cx", CX)
.attr("cy", CY);
let centerCircle = svg.append("circle")
.attr("fill", "white")
.attr("r", smallR)
.attr("cx", CX)
.attr("cy", CY);
const timePercentage = (time) => {
const percentage = (time * 100 / 23) / 100;
return percentage;
};
function timeToRadius(time) {
return (smallR + timePercentage(time) * (R - smallR))
}
// Times concentric circles ---
for (let i = 0; i <= 23; i += 4) {
svg.append("circle")
.attr("fill", "none")
.attr("cx", CX)
.attr("cy", CY)
.attr("stroke-dasharray", "4 20")
.attr("stroke", "gray")
.attr("stroke-width", 1)
.attr("r", timeToRadius(i))
}
// Cardinal points ---
const textTime = 25;
const fontSize = R * 0.25;
svg.append("text")
.attr("dx", getPosition(0, textTime).x)
.attr("dy", getPosition(0, textTime).y)
.text("N")
.attr("fill", "black")
.attr("font-size", fontSize)
.attr("text-anchor", "middle")
.style("font-family", "serif")
svg.append("text")
.attr("dx", getPosition(180, textTime).x)
.attr("dy", getPosition(180, textTime).y)
.text("S")
.attr("fill", "black")
.attr("font-size", fontSize)
.attr("text-anchor", "middle")
.style("font-family", "serif")
.attr("alignment-baseline", "hanging")
svg.append("text")
.attr("dx", getPosition(-90, textTime).x)
.attr("dy", getPosition(-90, textTime).y)
.text("E")
.attr("fill", "black")
.attr("font-size", fontSize)
.attr("text-anchor", "start")
.attr("alignment-baseline", "middle")
.style("font-family", "serif");
svg.append("text")
.attr("dx", getPosition(90, textTime).x)
.attr("dy", getPosition(90, textTime).y)
.text("O")
.attr("fill", "black")
.attr("font-size", fontSize)
.attr("text-anchor", "end")
.attr("alignment-baseline", "middle")
.style("font-family", "serif")
// Ships positions ---
function getPosition(degrees, time) {
const getRadians = (degrees) => degrees * Math.PI / 180 + Math.PI / 2;
let x = (smallR + timePercentage(time) * (R - smallR)) * Math.cos(getRadians(degrees)) + CX;
let y = (smallR + timePercentage(time) * (R - smallR)) * Math.sin(getRadians(degrees)) * -1 + CY;
return { x, y };
}
// Data mapping ---
let parsedData = [];
(() => {
data?.forEach((x) => {
parsedData.push({
id: x.Patente,
course: x.Rumbo,
speed: x.Velocidad,
time: new Date(x.Fecha_gps).getHours()
})
});
let position;
parsedData.map(d => {
position = getPosition(d.course, d.time);
svg.append("circle")
.attr("fill", () => {
if (d.speed == 0) return "brown";
else return "brown";
})
.attr("r", R * 0.015)
.attr("cx", position.x)
.attr("cy", position.y)
.attr("opacity", 0.4);
});
})();
}, [data]);
return (
<div id="container" style={{ width: '100%', height: '100%' }}></div>
)
}

What do you think the line let svg = d3.select("#container").append("svg") does? It selects the element with ID "container" and appends a new SVG element to it.
You can essentially do one of two things:
Select and remove all existing SVG elements in the container and then draw the chart from scratch on update: d3.select("#container svg").remove(), then d3.select("#container").append("svg");
Split the initialization (append svg, set size, draw axes) logic from the logic that might need to be run multiple times (update the values) and make sure to call only the second part on update. This is more complex, but also efficient, especially in an already DOM-heavy ecosystem like React.
I've taken your code and added an example of how I would implement the second way below:
Note that to access R, smallR, etc, I've had to divide the initialization logic over two useEffect hooks.
const {
useEffect,
useState
} = React;
const FourDirectionsTimeChart = () => {
const [data, setData] = useState([]);
const [constants, setConstants] = useState({});
// Initialization logic, run only once
useEffect(() => {
const width = document.getElementById("container").clientWidth;
const height = document.getElementById("container").clientHeight;
const R = (width + height) / 8;
const CX = width / 2;
const CY = height / 2;
const smallR = R * 0.1;
setConstants({
R,
CX,
CY,
smallR
});
const circleColor = "bisque";
const itemColor = "#3F4200";
const svg = d3.select("#container")
.append("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", `0 0 ${width} ${height}`);
const mainCircle = svg.append("circle")
.attr('id', 'mainCircle')
.attr("fill", circleColor);
const centerCircle = svg.append("circle")
.attr('id', 'innerCircle')
.attr("fill", "white");
// Times concentric circles ---
for (let i = 0; i <= 23; i += 4) {
svg.append("circle")
.attr("class", "concentric")
.datum(i)
.attr("fill", "none")
.attr("stroke-dasharray", "4 20")
.attr("stroke", "gray")
.attr("stroke-width", 1);
}
// Cardinal points ---
svg.append("text")
.attr("id", "N")
.text("N")
.attr("fill", "black")
.attr("text-anchor", "middle")
.style("font-family", "serif")
svg.append("text")
.attr("id", "S")
.text("S")
.attr("fill", "black")
.attr("text-anchor", "middle")
.style("font-family", "serif")
.attr("alignment-baseline", "hanging")
svg.append("text")
.attr("id", "E")
.text("E")
.attr("fill", "black")
.attr("text-anchor", "start")
.attr("alignment-baseline", "middle")
.style("font-family", "serif");
svg.append("text")
.attr("id", "O")
.text("O")
.attr("fill", "black")
.attr("text-anchor", "end")
.attr("alignment-baseline", "middle")
.style("font-family", "serif")
}, []);
// Drawing logic, run whenever `constants` changes (which is only once)
useEffect(() => {
const { R, smallR, CX, CY } = constants;
const textTime = 25;
const fontSize = R * 0.25;
const svg = d3.select("#container svg");
const mainCircle = svg.select("#mainCircle")
.attr("r", R)
.attr("cx", CX)
.attr("cy", CY);
const centerCircle = svg.select("#innerCircle")
.attr("r", smallR)
.attr("cx", CX)
.attr("cy", CY);
// Times concentric circles ---
function timeToRadius(time) {
return (smallR + timePercentage(time) * (R - smallR));
}
svg.selectAll(".concentric")
.attr("cx", CX)
.attr("cy", CY)
.attr("r", d => timeToRadius(d))
// Cardinal points ---
svg.select("text#N")
.attr("font-size", fontSize)
.attr("dx", getPosition(0, textTime).x)
.attr("dy", getPosition(0, textTime).y);
svg.select("text#S")
.attr("font-size", fontSize)
.attr("dx", getPosition(180, textTime).x)
.attr("dy", getPosition(180, textTime).y);
svg.select("text#E")
.attr("font-size", fontSize)
.attr("dx", getPosition(-90, textTime).x)
.attr("dy", getPosition(-90, textTime).y);
svg.select("text#O")
.attr("font-size", fontSize)
.attr("dx", getPosition(90, textTime).x)
.attr("dy", getPosition(90, textTime).y);
}, [constants]);
const timePercentage = (time) => {
const percentage = (time * 100 / 23) / 100;
return percentage;
};
function timeToRadius(time) {
const {
smallR,
R
} = constants;
return (smallR + timePercentage(time) * (R - smallR));
}
const getRadians = (degrees) => degrees * Math.PI / 180 + Math.PI / 2;
function getPosition(degrees, time) {
const {
smallR,
R,
CX,
CY
} = constants;
const x = (smallR + timePercentage(time) * (R - smallR)) * Math.cos(getRadians(degrees)) + CX;
const y = (smallR + timePercentage(time) * (R - smallR)) * Math.sin(getRadians(degrees)) * -1 + CY;
return {
x,
y
};
}
useEffect(() => {
const svg = d3.select('#container svg');
// Data mapping ---
let position;
const points = svg.selectAll('.point').data(data);
// Remove old points
points.exit().remove();
// Append and set constants only for new points
points
.enter()
.append('circle')
.attr('class', 'point')
.attr("r", 15)
.attr("opacity", 0.4)
// Then, take both the new and the updated points
.merge(points)
.attr("cx", d => getPosition(d.course, d.time).x)
.attr("cy", d => getPosition(d.course, d.time).y)
.attr("fill", d => d.speed === 0 ? "brown" : "red");
}, [data]);
useEffect(() => {
let i = 0;
setInterval(() => {
setData(i % 2 === 0 ? data1 : data2);
i++;
}, 2000);
}, []);
return ( <
div id = "container"
style = {
{
width: '100%',
height: '100%'
}
} > < /div>
)
}
const data1 = [{
id: 100,
course: 35,
speed: 0,
time: 5
}];
const data2 = [{
id: 101,
course: 80,
speed: 25,
time: 12
}, {
id: 102,
course: 256,
speed: 0,
time: 15
}];
ReactDOM.render( <
FourDirectionsTimeChart / > ,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root" style="width: 500px; height: 500px"></div>

Related

d3-delaunay lines not showing

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>

In D3 - sync'ing up two sets of radio buttons and updating React State

First let me share an example of the code that will very clearly and easily highlight the issue I'm currently having:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
oppDiv: 'none',
oppTeam: 'none'
}
this.chartProps = {
myLightGrey: '#EEE',
myMidGrey: '#999',
myDarkGrey: '#333',
}
}
updateButtonColors(button, parent, self) {
const { myLightGrey, 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)
}
grabTeamInfo() {
var data = [{"teamid":"ARI","division":"NL West"},{"teamid":"ATL","division":"NL East"},{"teamid":"BAL","division":"AL East"},{"teamid":"BOS","division":"AL East"},{"teamid":"CHC","division":"NL Central"},{"teamid":"CWS","division":"AL Central"},{"teamid":"CIN","division":"NL Central"},{"teamid":"CLE","division":"AL Central"},{"teamid":"COL","division":"NL West"},{"teamid":"DET","division":"AL Central"},{"teamid":"HOU","division":"AL West"},{"teamid":"KC","division":"AL Central"},{"teamid":"LAA","division":"AL West"},{"teamid":"LAD","division":"NL West"},{"teamid":"MIA","division":"NL East"},{"teamid":"MIL","division":"NL Central"},{"teamid":"MIN","division":"AL Central"},{"teamid":"NYM","division":"NL East"},{"teamid":"NYY","division":"AL East"},{"teamid":"OAK","division":"AL West"},{"teamid":"PHI","division":"NL East"},{"teamid":"PIT","division":"NL Central"},{"teamid":"STL","division":"NL Central"},{"teamid":"SD","division":"NL West"},{"teamid":"SF","division":"NL West"},{"teamid":"SEA","division":"AL West"},{"teamid":"TB","division":"AL East"},{"teamid":"TEX","division":"AL West"},{"teamid":"TOR","division":"AL East"},{"teamid":"WAS","division":"NL East"}];
return data;
}
drawOppDivision() {
const teamInfo = this.grabTeamInfo();
const { myLightGrey, myMidGrey, myDarkGrey } = this.chartProps;
const { updateButtonColors } = this;
const divs = ["NL East", "NL Central", "NL West", "AL East", "AL Central", "AL West"];
d3.select('g.oppDivision')
.attr("transform", "translate(" + 585 + "," + 135 + ")")
// Draw Button Group For 6 Divisions
// ==================================
const oppDivision = d3.select('g.oppDivision')
.selectAll('.divisions')
.data(divs)
.enter()
.append("g")
.attr("class", "divisions")
.attr("cursor", "pointer")
oppDivision.append("rect")
.attr("x", (d,i) => (i % 3)*67)
.attr("y", (d,i) => i > 2 ? 27 : 0)
.attr("rx", 4).attr("ry", 4)
.attr("width", 65).attr("height", 25)
.attr("stroke", "#BBB")
.attr("fill", "#EEE")
oppDivision.append("text")
.attr("x", (d,i) => 32 + (i % 3)*67)
.attr("y", (d,i) => i > 2 ? 15 + 27 : 15 + 0)
.style("font-size", "0.7em")
.style("text-anchor", "middle")
.style("font-weight", "700")
.text(d => d)
const self = this;
oppDivision
.on("click", function(d,i) {
updateButtonColors(d3.select(this), d3.select(this.parentNode), self)
self.setState({oppDiv: divs[i]})
})
.on("mouseover", function() {
if (d3.select(this).select("rect").attr("fill") != myDarkGrey) {
d3.select(this)
.select("rect")
.attr("fill", myMidGrey); // lol almost here keep trying
}
})
.on("mouseout", function() {
if (d3.select(this).select("rect").attr("fill") != myDarkGrey) {
d3.select(this)
.select("rect")
.attr("fill", myLightGrey);
}
});
// Draw Title
d3.select('g.oppDivision').append("text")
.attr("x", -1).attr("y", -15)
.style("font-size", '1.25em')
.style("font-weight", '700')
.style("fill", myDarkGrey)
.text("Opposing Div / Team")
}
drawOppTeam() {
// Draw Button Group For 5 Teams In Selected Division
// ====================================================
// make an object with (team, division, abbrev) keys?
const teamInfo = this.grabTeamInfo();
const { myLightGrey, myMidGrey, myDarkGrey } = this.chartProps;
const { updateButtonColors } = this;
const { oppDiv } = this.state;
const oppTeamList = teamInfo
.filter(team => team.division == oppDiv)
.map(team => team.teamid)
// d3.select('g.oppTeam').selectAll('*').remove()
d3.select('g.oppTeam')
.attr("transform", "translate(" + 585 + "," + 135 + ")")
const oppTeam = d3.select('g.oppTeam')
.selectAll('.oppteams')
.data(oppTeamList)
oppTeam
.enter()
.append("g")
.attr("class", "oppteams")
.attr("cursor", "pointer")
oppTeam.append("rect")
.attr("x", (d,i) => i * 40)
.attr("y", 65)
.attr("rx", 4).attr("ry", 4)
.attr("width", 38).attr("height", 20)
.attr("stroke", myMidGrey)
.attr("fill", myLightGrey)
oppTeam.append("text")
.attr("x", (d,i) => (i * 40)+20)
.attr("y", 79)
.style("font-size", "0.7em")
.style("font-weight", "700")
.style("text-anchor", "middle")
.text(d => d)
oppTeam // not wanting to work like it should (need D3 GUP)
.exit()
.remove()
const self = this;
oppTeam
.on("click", function(d,i) {
updateButtonColors(d3.select(this), d3.select(this.parentNode), self)
self.setState({oppTeam: oppTeamList[i]})
})
.on("mouseover", function() {
if (d3.select(this).select("rect").attr("fill") != myDarkGrey) {
d3.select(this)
.select("rect")
.attr("fill", myMidGrey); // lol almost here keep trying
}
})
.on("mouseout", function() {
if (d3.select(this).select("rect").attr("fill") != myDarkGrey) {
d3.select(this)
.select("rect")
.attr("fill", myLightGrey);
}
});
// ======
}
componentDidUpdate() {
this.drawOppTeam()
}
componentDidMount() {
d3.select('#my-button-svg')
.attr('width', '100%')
.attr('height', '100%')
.attr('viewBox', "0 0 " + (800) + " " + 600)
.attr('preserveAspectRatio', "xMaxYMax")
this.drawOppDivision();
}
render() {
return(
<div>
<svg id='my-button-svg'>
<g className="oppDivision" />
<g className="oppTeam" />
</svg>
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<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>
<div id='root'>
Come On Work!
</div>
I am struggling with having the 2 D3 radio buttun groups working hand in hand with one another here. What should happen is:
clicking on any of the 6 divisions will display the (correct) list of 5 teams in that baseball division, and will highlight the division.
clicking on a new division will display 5 new teams, and highlight the new division.
clicking on a team will highlight the team.
The current issue I am facing is that:
The list of teams does not appear until the 2nd time I click on a division name,
The team names do not highlight.
I think this is neat, clean radio button code in React, with a simple and useful interactivity between button groups (especially here with teams and divisions, which I need for a baseball app). Fixing why the buttons arent working correctly (and knowing what is wrong with my code), will help me a ton! Thanks in advance for the help.
Quickly fixed it, seems to be working for me now, let me know if it's what you're looking for!
By the way in the long term I would recommend maintaining state either totally in React (where componentDidUpdate() is responsible for making updating the styling) or totally in d3 (meaning you just render the SVG and React doesn't touch it after, the division button click handler calls drawOppTeam() instead of componentDidUpdate()). I think the reason this issue was tricky was that state handling was shared between the two libraries.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
oppDiv: 'none',
oppTeam: 'none'
}
this.chartProps = {
myLightGrey: '#EEE',
myMidGrey: '#999',
myDarkGrey: '#333',
}
}
updateButtonColors(button, parent, self) {
const { myLightGrey, 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)
}
grabTeamInfo() {
var data = [{"teamid":"ARI","division":"NL West"},{"teamid":"ATL","division":"NL East"},{"teamid":"BAL","division":"AL East"},{"teamid":"BOS","division":"AL East"},{"teamid":"CHC","division":"NL Central"},{"teamid":"CWS","division":"AL Central"},{"teamid":"CIN","division":"NL Central"},{"teamid":"CLE","division":"AL Central"},{"teamid":"COL","division":"NL West"},{"teamid":"DET","division":"AL Central"},{"teamid":"HOU","division":"AL West"},{"teamid":"KC","division":"AL Central"},{"teamid":"LAA","division":"AL West"},{"teamid":"LAD","division":"NL West"},{"teamid":"MIA","division":"NL East"},{"teamid":"MIL","division":"NL Central"},{"teamid":"MIN","division":"AL Central"},{"teamid":"NYM","division":"NL East"},{"teamid":"NYY","division":"AL East"},{"teamid":"OAK","division":"AL West"},{"teamid":"PHI","division":"NL East"},{"teamid":"PIT","division":"NL Central"},{"teamid":"STL","division":"NL Central"},{"teamid":"SD","division":"NL West"},{"teamid":"SF","division":"NL West"},{"teamid":"SEA","division":"AL West"},{"teamid":"TB","division":"AL East"},{"teamid":"TEX","division":"AL West"},{"teamid":"TOR","division":"AL East"},{"teamid":"WAS","division":"NL East"}];
return data;
}
drawOppDivision() {
const teamInfo = this.grabTeamInfo();
const { myLightGrey, myMidGrey, myDarkGrey } = this.chartProps;
const { updateButtonColors } = this;
const divs = ["NL East", "NL Central", "NL West", "AL East", "AL Central", "AL West"];
d3.select('g.oppDivision')
.attr("transform", "translate(" + 585 + "," + 135 + ")")
// Draw Button Group For 6 Divisions
// ==================================
const oppDivision = d3.select('g.oppDivision')
.selectAll('.divisions')
.data(divs)
.enter()
.append("g")
.attr("class", "divisions")
.attr("cursor", "pointer")
oppDivision.append("rect")
.attr("x", (d,i) => (i % 3)*67)
.attr("y", (d,i) => i > 2 ? 27 : 0)
.attr("rx", 4).attr("ry", 4)
.attr("width", 65).attr("height", 25)
.attr("stroke", "#BBB")
.attr("fill", "#EEE")
oppDivision.append("text")
.attr("x", (d,i) => 32 + (i % 3)*67)
.attr("y", (d,i) => i > 2 ? 15 + 27 : 15 + 0)
.style("font-size", "0.7em")
.style("text-anchor", "middle")
.style("font-weight", "700")
.text(d => d)
const self = this;
oppDivision
.on("click", function(d,i) {
updateButtonColors(d3.select(this), d3.select(this.parentNode), self)
self.setState({oppDiv: divs[i]})
})
.on("mouseover", function() {
if (d3.select(this).select("rect").attr("fill") != myDarkGrey) {
d3.select(this)
.select("rect")
.attr("fill", myMidGrey); // lol almost here keep trying
}
})
.on("mouseout", function() {
if (d3.select(this).select("rect").attr("fill") != myDarkGrey) {
d3.select(this)
.select("rect")
.attr("fill", myLightGrey);
}
});
// Draw Title
d3.select('g.oppDivision').append("text")
.attr("x", -1).attr("y", -15)
.style("font-size", '1.25em')
.style("font-weight", '700')
.style("fill", myDarkGrey)
.text("Opposing Div / Team")
}
drawOppTeam() {
// Draw Button Group For 5 Teams In Selected Division
// ====================================================
// make an object with (team, division, abbrev) keys?
const teamInfo = this.grabTeamInfo();
const { myLightGrey, myMidGrey, myDarkGrey } = this.chartProps;
const { updateButtonColors } = this;
const { oppDiv } = this.state;
const oppTeamList = teamInfo
.filter(team => team.division == oppDiv)
.map(team => team.teamid)
d3.select('g.oppTeam').selectAll('*').remove()
d3.select('g.oppTeam')
.attr("transform", "translate(" + 585 + "," + 135 + ")")
.attr("cursor", "pointer")
const oppTeam = d3.select('g.oppTeam')
.selectAll('.oppteams')
.data(oppTeamList)
const teams = oppTeam
.enter()
.append("g")
.attr("class", "oppteams")
teams.append("rect")
.attr("x", (d,i) => i * 40)
.attr("y", 65)
.attr("rx", 4).attr("ry", 4)
.attr("width", 38).attr("height", 20)
.attr("stroke", (d) => this.state.oppTeam === d ? myLightGrey : myMidGrey)
.attr("fill", (d) => this.state.oppTeam === d ? myDarkGrey : myLightGrey)
teams.append("text")
.attr("x", (d,i) => (i * 40)+20)
.attr("y", 79)
.attr("fill", (d) => this.state.oppTeam === d ? myLightGrey : myDarkGrey)
.style("font-size", "0.7em")
.style("font-weight", "700")
.style("text-anchor", "middle")
.text(d => d)
//oppTeam // not wanting to work like it should (need D3 GUP)
//.exit()
//.remove()
const self = this;
teams
.on("click", function(d,i) {
updateButtonColors(d3.select(this), d3.select(this.parentNode), self)
self.setState({oppTeam: oppTeamList[i]})
})
.on("mouseover", function() {
if (d3.select(this).select("rect").attr("fill") != myDarkGrey) {
d3.select(this)
.select("rect")
.attr("fill", myMidGrey); // lol almost here keep trying
}
})
.on("mouseout", function() {
if (d3.select(this).select("rect").attr("fill") != myDarkGrey) {
d3.select(this)
.select("rect")
.attr("fill", myLightGrey);
}
});
// ======
}
componentDidUpdate() {
this.drawOppTeam()
}
componentDidMount() {
d3.select('#my-button-svg')
.attr('width', '100%')
.attr('height', '100%')
.attr('viewBox', "0 0 " + (800) + " " + 600)
.attr('preserveAspectRatio', "xMaxYMax")
this.drawOppDivision();
}
render() {
return(
<div>
<svg id='my-button-svg'>
<g className="oppDivision" />
<g className="oppTeam" />
</svg>
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<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>
<div id='root'>
Come On Work!
</div>

Color shading with D3 comparative histogram

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!

Updating a graph on change of data in d3.js

hi I am using the following code to basically create a bar graph by consuming data from a service call. The problem with this is as the data from the service call changes, it is not reflected in the bar graph, it remains in its original state. The Appname and Appcount are two things which i am getting from the service call
jQuery(function($)
{
Appnames = []//array will be populated from service call,
Appcount = []//array will be populated from serv`enter code here`ice call,
chart,
width = 700,
bar_height = 40,
gap = 2,
height = bar_height * Appnames.length;
var margin = {top: 20, right: 120, bottom: 20, left: 120};
/* step 1 - create the background*/
chart = d3.select($("#step-1")[0])
.append('svg')
.attr('class', 'chart')
.attr('width', width)
.attr('height', height);
/* step 2 - create the bars*/
var x, y;
chart = d3.select($("#step-2")[0])
.append('svg')
.attr('class', 'chart')
.attr('width', width + margin.right + margin.left)
.attr('height', height);
x = d3.scale.linear()
.domain([0, d3.max(Appcount)])
.range([0, width]);
y = function(i) { return bar_height * i; }
chart.selectAll("rect")
.data(Appcount)
.enter().append("rect")
.attr("x", 0)
.attr("y", function(d, i) { return y(i);})
.attr("width", x)
.attr("height", bar_height);
/* step 3 - add counts to the bars*/
chart = d3.select($("#step-3")[0])
.append('svg')
.attr('class', 'chart')
.attr('width', width)
.attr('height', height);
chart.selectAll("rect")
.data(Appcount)
.enter().append("rect")
.attr("x", 0)
.attr("y", function(d, i) { return y(i);})
.attr("width", x)
.attr("height", bar_height);
chart.selectAll("text")
.data(Appcount)
.enter().append("text")
.attr("x", x)
.attr("y", function(d, i){ return y(i) + bar_height/2; } )
.attr("dx", -5)
.attr("dy", ".36em")
.attr("text-anchor", "end")
.text(String);
/* step 4 - add Appnames to bars*/
var left_width = 100;
chart = d3.select($("#step-4")[0])
.append('svg')
.attr('class', 'chart')
.attr('width', left_width + width)
.attr('height', height);
chart.selectAll("rect")
.data(Appcount)
.enter().append("rect")
.attr("x", left_width)
.attr("y", function(d, i) { return y(i);})
.attr("width", x)
.attr("height", bar_height);
chart.selectAll("text.score")
.data(Appcount)
.enter().append("text")
.attr("x", function(d) { return x(d) + left_width; })
.attr("y", function(d, i) { return y(i) + bar_height / 2;})
.attr("dx", -5)
.attr("dy", ".36em")
.attr("text-anchor", "end")
.attr('class', 'score')
.text(String);
chart.selectAll("text.name")
.data(Appnames)
.enter().append("text")
.attr("x", left_width / 2)
.attr("y", function(d, i){ return y(i) +bar_height/2; } )
.attr("dy", ".36em")
.attr("text-anchor", "middle")
.attr('class', 'name')
.text(String);
/* step 5 - add a ruler line to the bars*/
var gap = 2, yRangeBand;
// redefine y for adjusting the gap
yRangeBand = bar_height + 2 * gap;
y = function(i) { return yRangeBand * i; };
chart = d3.select($("#step-5")[0])
.append('svg')
.attr('class', 'chart')
.attr('width', left_width + width + 40)
.attr('height', (bar_height + gap * 2) * Appnames.length + 30)
.append("g")
.attr("transform", "translate(10, 20)");
chart.selectAll("line")
.data(x.ticks(d3.max(Appcount)))
.enter().append("line")
.attr("x1", function(d) { return x(d) + left_width; })
.attr("x2", function(d) { return x(d) + left_width; })
.attr("y1", 0)
.attr("y2", (bar_height + gap * 2) * Appnames.length);
chart.selectAll(".rule")
.data(x.ticks(d3.max(Appcount)))
.enter().append("text")
.attr("class", "rule")
.attr("x", function(d) { return x(d) + left_width; })
.attr("y", 0)
.attr("dy", -6)
.attr("text-anchor", "middle")
.attr("font-size", 10)
.text(String);
chart.selectAll("rect")
.data(Appcount)
.enter().append("rect")
.attr("x", left_width)
.attr("y", function(d, i) { return y(i) + gap; })
.attr("width", x)
.attr("height", bar_height);
chart.selectAll("text.score")
.data(Appcount)
.enter().append("text")
.attr("x", function(d) { return x(d) + left_width; })
.attr("y", function(d, i) { return y(i) + yRangeBand/2;})
.attr("dx", -5)
.attr("dy", ".36em")
.attr("text-anchor", "end")
.attr('class', 'score')
.text(String);
chart.selectAll("text.name")
.data(Appnames)
.enter().append("text")
.attr("x", left_width / 2)
.attr("y", function(d, i){ return y(i) + yRangeBand/2; } )
.attr("dy", ".36em")
.attr("text-anchor", "middle")
.attr('class', 'name')
.text(String); }(jQuery));
d3 uses an enter, update and exit pattern. Your code is only handling the enter state. Split it up to handle all 3 states:
var rects = chart.selectAll("rect")
.data(Appcount); //<-- create your data join
rects.enter().append("rect"); //<-- ENTER - when data enters the join append a rect element to dom
rects.exit().remove(); //<-- EXIT - when data leaves the join, remove the rect
rects.attr("x", 0) //<-- UPDATE - update the rects based on data
.attr("y", function(d, i) { return y(i);})
.attr("width", x)
.attr("height", bar_height);

Add text to D3 Bilevel Partition

I'm using mbostock’s Bilevel Partition and I need to include each node's text on each path. I'm trying to use this Coffee Wheel example but I'm having no luck.
This is all being written with Backbone.js which is giving me some trouble with nesting "this", so that may cause a problem.
Here is my render function. I try adding in text at the bottom.
render: function(root, svgVar) {
var that = this;
$('.breadcrumb').append('<li>'+root.name+'</li>');
var svg = d3.select(".associations-container").append("svg")
.attr("width", svgVar.width)
.attr("height", svgVar.height)
.append("g")
.attr("transform", "translate(" + (svgVar.width/2) + "," + ((svgVar.height/2)-svgVar.padding) + ")");
this.partition = d3.layout.partition()
.sort(function(a, b) { return d3.ascending(a.name, b.name); })
.size([2 * Math.PI, svgVar.radius]);
this.arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx - .01 / (d.depth + .5); })
.innerRadius(function(d) { return svgVar.radius / 3 * d.depth; })
.outerRadius(function(d) { return svgVar.radius / 3 * (d.depth + 1) - 1; });
// Compute the initial layout on the entire tree to sum sizes.
// Also compute the full name and fill color for each node,
// and stash the children so they can be restored as we descend.
this.partition
.value(function(d) { return 1; })
.nodes(root)
.forEach(function(d) {
d._children = d.children;
d.sum = d.value;
d.key = that.key(d);
d.fill = that.fill(d);
});
// Now redefine the value function to use the previously-computed sum.
this.partition
.children(function(d, depth) { return depth < 2 ? d._children : null; })
.value(function(d) { return d.sum; });
this.centerCircle = svg.append("circle")
.attr("r", svgVar.radius / 3)
.attr("fill", "grey")
.on("click", this.zoomOut);
var centerControls = svg.append("g");
centerControls.append("svg:foreignObject")
.attr("width", 40)
.attr("height", 40)
.attr("y", "-20px")
.attr("x", "-48px")
.append("xhtml:span")
.attr("class", "control glyphicon glyphicon-filter");
centerControls.append("svg:foreignObject")
.attr("width", 40)
.attr("height", 50)
.attr("y", "-20px")
.attr("x", "8px")
.append("xhtml:span")
.attr("class", "control glyphicon glyphicon-th-list toggle-list");
this.path = svg.selectAll("path")
.data(this.partition.nodes(root).slice(1))
.enter().append("path")
.attr("d", this.arc)
.style("fill", function(d) { return d.fill; })
.each(function(d) { this._current = that.updateArc(d); })
.on("click", this.zoomIn);
this.text = svg.selectAll("text").data(root);
this.textEnter = this.text.enter().append("text")
.style("fill-opacity", 1)
.style("fill", "white")
.attr("text-anchor", function(d) {
return x(d.x + d.dx / 2) >Math.PI ? "end" : "start";
})
.attr("dy", ".2em")
.attr("transform", function(d) {
var multiline = (d.name || "").split(" ").length > 1,
angle = x(d.x + d.dx / 2) * 180 / Math.PI - 90,
rotate = angle + (multiline ? -.5 : 0);
return "rotate(" + rotate + ")translate(" + (y(d.y) + padding) + ")rotate(" + (angle > 90 ? -180 : 0) + ")";
});
this.textEnter.append("tspan")
.attr("x", 0)
.text(function(d) { return d.name; });

Resources