I’m implementing some D3 code in class format so that I have a reusable chart.
The errors are as follows:
GetElementsByClassName
returns an HTMLCollection of length 0, but elements to be selected are classed correctly.
Circles
appear at x0 and yMax, rather than at data positions (Issue is probably connected to the first).
Text
is not appended to circle and not visible (This might work when the circles work).
I’m implementing this pretty much exactly as it looks, with the exception that I’m putting tooltips on 2/4 of the lines, and I’m using a class.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <script src="https://d3js.org/d3.v7.js"></script> </head> <body> <div id="graph"></div> <script> class Chart { constructor(opts) { this.data = opts.data; this.element = opts.element; } draw() { this.width = this.element.offsetWidth; this.height = this.width / 2; this.padding = 50; this.margin = { top: 20, bottom: 20, left: 30, right: 50 }; this.element.innerHTML = ''; const svg = d3.select(this.element).append('svg'); svg.attr('width', this.width); svg.attr('height', this.height); this.plot = svg.append('g') .attr('transform', `translate(${this.margin.left},${this.margin.top})`); this.createScales(); this.addAxes(); this.addLine(); this.tTip(); } createScales() { this.keynames = d3.scaleOrdinal(); this.keynames.domain(Object.keys(this.data[0]).filter(key => key !== 'date')); this.keymap = this.keynames.domain().map( keyname => ({ name: keyname, values: this.data.map( d => ({ date: d.date, key: +d[keyname] }) ) }) ); const m = this.margin; const xExtent = d3.extent(this.data, d => d.date); const yExtent = [0, d3.max(this.keymap, d => d3.max(d.values, function (v) { return v.key }))]; this.xScale = d3.scaleTime() .range([0, this.width - m.right]) .domain(xExtent).nice(); this.yScale = d3.scaleLinear() .range([this.height - (m.top + m.bottom), 0]) .domain(yExtent).nice(); } addAxes() { const m = this.margin; const xAxis = d3.axisBottom() .scale(this.xScale); const yAxis = d3.axisLeft() .scale(this.yScale); this.plot.append("g") .attr("class", "x axis") .attr("transform", `translate(0, ${this.height - (m.top + m.bottom)})`) .call(xAxis.ticks(8)); this.plot.append("g") .attr("class", "y axis") .call(yAxis.ticks(4)) .append("text") .attr("transform", "rotate(-90)") .attr("y", 6) .attr("dy", ".71em") .attr("fill", "black") .style("text-anchor", "end") .text("$USD"); } addLine() { const line = d3.line() .x(d => this.xScale(d.date)) .y(d => this.yScale(d.key)); this.plot.append('g') .selectAll('path') .data(this.keymap) .join('path') .classed('line', true) .attr('d', function (d) { return line(d.values) }) .style('stroke', this.lineColor || 'red') .style('fill', 'none'); } tTip(){ let mouseG = this.plot.append("g") .attr("class", "mouse-over-effects"); mouseG.append("path") .attr("class", "mouse-line") .style("stroke", "rgba(50,50,50,1)") .style("stroke-width", "0.5px") .style("opacity", "0"); var lines = document.getElementsByClassName('.standard'); //issue here let mousePerLine = mouseG.selectAll('.mouse-per-line') .data(this.keymap) .enter() .append("g") //join instead of append? .attr("class", "mouse-per-line"); mousePerLine.append("circle") //join instead of append? .attr("r", 4) .style("stroke", "black" ) .style("fill", "blue" ) .style("fill-opacity", "0.3") .style("stroke-width", "1px") .style("opacity", "0"); mousePerLine.append("text") //join instead of append? .attr("transform", function(d){ if (d.name == 'aapl') { return "translate(-50,30)" } else { return "translate(-50, -30)" } }).style("text-shadow", " -2px -2px 0 #FFF, 0 -2px 0 #FFF, 2px -2px 0 #FFF, 2px 0 0 #FFF, 2px 2px 0 #FFF, 0 2px 0 #FFF,-2px 2px 0 #FFF,-2px 0 0 #FFF"); mouseG.append('svg:rect') .attr('width', this.width) .attr('height', this.height) .attr('x', '0') .attr('fill', 'none') .attr('pointer-events', 'all') .on('mouseout', function() { d3.select(".mouse-line") .style("opacity", "0"); d3.selectAll(".mouse-per-line circle") .style("opacity", "0"); d3.selectAll(".mouse-per-line text") .style("opacity", "0"); }) .on('mouseover', function() { d3.select(".mouse-line") .style("opacity", "1"); d3.selectAll(".mouse-per-line circle") .style("opacity", "1"); d3.selectAll(".mouse-per-line text") .style("opacity", "1"); }) .on('mousemove', () => { let mouse = d3.pointer(event); d3.select(".mouse-line") .attr("d", () => { let d = "M" + mouse[0] + "," + this.height; d += " " + mouse[0] + "," + 0; return d; }); d3.selectAll(".mouse-per-line") .attr("transform", (d, i) => { let xDate = this.xScale.invert(mouse[0]), bisect = d3.bisector(d => d.date).right, idx = bisect(d.values, xDate); let beginning = 0, end = lines[i].getTotalLength(), target = null; while (true){ let target = Math.floor((beginning + end) / 2), pos = lines[i].getPointAtLength(target); //issue here if ((target === end || target === beginning) && pos.x !== mouse[0]) { break; } if (pos.x > mouse[0]){ end = target; } else if (pos.x < mouse[0]){ beginning = target; } else break; //position found } d3.select(this).select('text') .text( () => { "$" + this.yScale.invert(pos.y).toFixed(2)}) return "translate(" + mouse[0] + "," + pos.y +")"; }) .style('font-family', 'Helvetica') .style('font-size', '11px') .style('letter-spacing', '1px') .style('text-transform', 'uppercase'); }); } setColor(newColor) { this.plot.select('.line') .style('stroke', newColor); this.lineColor = newColor; } setData(data) { this.data = data; this.draw(); } } const chart = new Chart({ element: document.querySelector('#graph') }); const data = d3.csvParse(`Date,AAPL,SMA_AAPL,TSLA,SMA_TSLA 2018-12-31,38.33848571777344,,66.55999755859375, 2019-01-02,38.382225036621094,,62.02399826049805, 2019-01-03,34.55907440185547,,60.071998596191406, 2019-01-04,36.03437805175781,,63.53799819946289, 2019-01-07,35.95417022705078,,66.99199676513672, 2019-01-08,36.63956832885742,,67.06999969482422, 2019-01-09,37.26177215576172,,67.70600128173828, 2019-01-10,37.380863189697266,,68.99400329589844, 2019-01-11,37.013858795166016,,69.4520034790039, 2019-01-14,36.4572868347168,,66.87999725341797, 2019-01-15,37.20343780517578,,68.88600158691406, 2019-01-16,37.657936096191406,,69.20999908447266, 2019-01-17,37.88154602050781,,69.46199798583984, 2019-01-18,38.11487579345703,,60.45199966430664, 2019-01-22,37.259342193603516,,59.784000396728516, 2019-01-23,37.410030364990234,,57.518001556396484, 2019-01-24,37.113521575927734,,58.301998138427734, 2019-01-25,38.34333801269531,,59.40800094604492, 2019-01-28,37.988487243652344,,59.2760009765625, 2019-01-29,37.59474182128906,,59.492000579833984, 2019-01-30,40.16377258300781,,61.75400161743164, 2019-01-31,40.453006744384766,,61.40399932861328, 2019-02-01,40.472450256347656,,62.44200134277344, 2019-02-04,41.622066497802734,,62.577999114990234, 2019-02-05,42.33420181274414,,64.2699966430664, 2019-02-06,42.34878158569336,,63.444000244140625, 2019-02-07,41.546722412109375,,61.50199890136719, 2019-02-08,41.59553909301758,,61.15999984741211, 2019-02-11,41.35633087158203,,62.56800079345703, 2019-02-12,41.71269989013672,38.606483713785806,62.36199951171875,63.48539975484212 2019-02-13,41.539398193359375,38.71318079630534,61.63399887084961,63.32119979858398 2019-02-14,41.69073486328125,38.823464457194014,60.75400161743164,63.278866577148435 2019-02-15,41.59797286987305,39.05809440612793,61.57600021362305,63.32899996439616 2019-02-19,41.72246551513672,39.247697321573895,61.12799835205078,63.24866663614909 2019-02-20,41.990962982177734,39.44892374674479,60.512001037597656,63.032666778564455 2019-02-21,41.75419616699219,39.619411341349284,58.24599838256836,62.738533401489256 2019-02-22,42.22041702270508,39.78469950358073,58.94200134277344,62.44640007019043 2019-02-25,42.5279655456543,39.95626958211263,59.75400161743164,62.13840001424153`, function (d) { function removeNaN(e, c) { if (e > 0) { return e; } else { return c; } } return { date: d3.timeParse("%Y-%m-%d")(d.Date), aapl: +d.AAPL, tsla: +d.TSLA, aapl_sma: removeNaN(+d.SMA_AAPL, +d.AAPL), tsla_sma: removeNaN(+d.SMA_TSLA, +d.TSLA) }; }); chart.setData(data); </script> </body> </html>
As you can see the mouse interactions are pretty janky so I hope someone can help.
Advertisement
Answer
There are some differences between arrow function and regular function.
Fixed some more errors:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <script src="https://d3js.org/d3.v7.js"></script> </head> <body> <div id="graph"></div> <script> let pos = null; class Chart { constructor(opts) { this.data = opts.data; this.element = opts.element; } draw() { this.width = this.element.offsetWidth; this.height = this.width / 2; this.padding = 50; this.margin = { top: 20, bottom: 20, left: 30, right: 50 }; this.element.innerHTML = ''; const svg = d3.select(this.element).append('svg'); svg.attr('width', this.width); svg.attr('height', this.height); this.plot = svg.append('g') .attr('transform', `translate(${this.margin.left},${this.margin.top})`); this.createScales(); this.addAxes(); this.addLine(); this.tTip(); } createScales() { this.keynames = d3.scaleOrdinal(); this.keynames.domain(Object.keys(this.data[0]).filter(key => key !== 'date')); this.keymap = this.keynames.domain().map( keyname => ({ name: keyname, values: this.data.map( d => ({ date: d.date, key: +d[keyname] }) ) }) ); const m = this.margin; const xExtent = d3.extent(this.data, d => d.date); const yExtent = [0, d3.max(this.keymap, d => d3.max(d.values, function (v) { return v.key }))]; this.xScale = d3.scaleTime() .range([0, this.width - m.right]) .domain(xExtent).nice(); this.yScale = d3.scaleLinear() .range([this.height - (m.top + m.bottom), 0]) .domain(yExtent).nice(); } addAxes() { const m = this.margin; const xAxis = d3.axisBottom() .scale(this.xScale); const yAxis = d3.axisLeft() .scale(this.yScale); this.plot.append("g") .attr("class", "x axis") .attr("transform", `translate(0, ${this.height - (m.top + m.bottom)})`) .call(xAxis.ticks(8)); this.plot.append("g") .attr("class", "y axis") .call(yAxis.ticks(4)) .append("text") .attr("transform", "rotate(-90)") .attr("y", 6) .attr("dy", ".71em") .attr("fill", "black") .style("text-anchor", "end") .text("$USD"); } addLine() { const line = d3.line() .x(d => this.xScale(d.date)) .y(d => this.yScale(d.key)); this.plot.append('g') .selectAll('path') .data(this.keymap) .join('path') .classed('line', true) .attr('d', function (d) { return line(d.values) }) .style('stroke', this.lineColor || 'red') .style('fill', 'none'); } tTip() { let mouseG = this.plot.append("g") .attr("class", "mouse-over-effects"); mouseG.append("path") .attr("class", "mouse-line") .style("stroke", "rgba(50,50,50,1)") .style("stroke-width", "0.5px") .style("opacity", "0"); var lines = document.getElementsByClassName('line'); //issue here let mousePerLine = mouseG.selectAll('.mouse-per-line') .data(this.keymap) .enter() .append("g") //join instead of append? .attr("class", "mouse-per-line"); mousePerLine.append("circle") //join instead of append? .attr("r", 4) .style("stroke", "black" ) .style("fill", "blue" ) .style("fill-opacity", "0.3") .style("stroke-width", "1px") .style("opacity", "0"); mousePerLine.append("text") //join instead of append? .attr("transform", function (d) { if (d.name == 'aapl') { return "translate(-50,30)" } else { return "translate(-50, -30)" } }).style("text-shadow", " -2px -2px 0 #FFF, 0 -2px 0 #FFF, 2px -2px 0 #FFF, 2px 0 0 #FFF, 2px 2px 0 #FFF, 0 2px 0 #FFF,-2px 2px 0 #FFF,-2px 0 0 #FFF"); mouseG.append('svg:rect') .attr('width', this.width) .attr('height', this.height) .attr('x', '0') .attr('fill', 'none') .attr('pointer-events', 'all') .on('mouseout', function () { d3.select(".mouse-line") .style("opacity", "0"); d3.selectAll(".mouse-per-line circle") .style("opacity", "0"); d3.selectAll(".mouse-per-line text") .style("opacity", "0"); }) .on('mouseover', function () { d3.select(".mouse-line") .style("opacity", "1"); d3.selectAll(".mouse-per-line circle") .style("opacity", "1"); d3.selectAll(".mouse-per-line text") .style("opacity", "1"); }) .on('mousemove', () => { let mouse = d3.pointer(event); d3.select(".mouse-line") .attr("d", () => { let d = "M" + mouse[0] + "," + this.height; d += " " + mouse[0] + "," + 0; return d; }); d3.selectAll(".mouse-per-line") .attr("transform", function (d, i) { let xDate = chart.xScale.invert(mouse[0]), bisect = d3.bisector(d => d.date).right, idx = bisect(d.values, xDate); let beginning = 0, end = lines[i].getTotalLength(), target = null; while (true) { let target = Math.floor((beginning + end) / 2); pos = lines[i].getPointAtLength(target); //issue here if ((target === end || target === beginning) && pos.x !== mouse[0]) { break; } if (pos.x > mouse[0]) { end = target; } else if (pos.x < mouse[0]) { beginning = target; } else break; //position found } d3.select(this).select('text') .text("$" + chart.yScale.invert(pos.y).toFixed(2)) return "translate(" + mouse[0] + "," + pos.y + ")"; }) .style('font-family', 'Helvetica') .style('font-size', '11px') .style('letter-spacing', '1px') .style('text-transform', 'uppercase'); }); } setColor(newColor) { this.plot.select('.line') .style('stroke', newColor); this.lineColor = newColor; } setData(data) { this.data = data; this.draw(); } } const chart = new Chart({ element: document.querySelector('#graph') }); const data = d3.csvParse(`Date,AAPL,SMA_AAPL,TSLA,SMA_TSLA 2018-12-31,38.33848571777344,,66.55999755859375, 2019-01-02,38.382225036621094,,62.02399826049805, 2019-01-03,34.55907440185547,,60.071998596191406, 2019-01-04,36.03437805175781,,63.53799819946289, 2019-01-07,35.95417022705078,,66.99199676513672, 2019-01-08,36.63956832885742,,67.06999969482422, 2019-01-09,37.26177215576172,,67.70600128173828, 2019-01-10,37.380863189697266,,68.99400329589844, 2019-01-11,37.013858795166016,,69.4520034790039, 2019-01-14,36.4572868347168,,66.87999725341797, 2019-01-15,37.20343780517578,,68.88600158691406, 2019-01-16,37.657936096191406,,69.20999908447266, 2019-01-17,37.88154602050781,,69.46199798583984, 2019-01-18,38.11487579345703,,60.45199966430664, 2019-01-22,37.259342193603516,,59.784000396728516, 2019-01-23,37.410030364990234,,57.518001556396484, 2019-01-24,37.113521575927734,,58.301998138427734, 2019-01-25,38.34333801269531,,59.40800094604492, 2019-01-28,37.988487243652344,,59.2760009765625, 2019-01-29,37.59474182128906,,59.492000579833984, 2019-01-30,40.16377258300781,,61.75400161743164, 2019-01-31,40.453006744384766,,61.40399932861328, 2019-02-01,40.472450256347656,,62.44200134277344, 2019-02-04,41.622066497802734,,62.577999114990234, 2019-02-05,42.33420181274414,,64.2699966430664, 2019-02-06,42.34878158569336,,63.444000244140625, 2019-02-07,41.546722412109375,,61.50199890136719, 2019-02-08,41.59553909301758,,61.15999984741211, 2019-02-11,41.35633087158203,,62.56800079345703, 2019-02-12,41.71269989013672,38.606483713785806,62.36199951171875,63.48539975484212 2019-02-13,41.539398193359375,38.71318079630534,61.63399887084961,63.32119979858398 2019-02-14,41.69073486328125,38.823464457194014,60.75400161743164,63.278866577148435 2019-02-15,41.59797286987305,39.05809440612793,61.57600021362305,63.32899996439616 2019-02-19,41.72246551513672,39.247697321573895,61.12799835205078,63.24866663614909 2019-02-20,41.990962982177734,39.44892374674479,60.512001037597656,63.032666778564455 2019-02-21,41.75419616699219,39.619411341349284,58.24599838256836,62.738533401489256 2019-02-22,42.22041702270508,39.78469950358073,58.94200134277344,62.44640007019043 2019-02-25,42.5279655456543,39.95626958211263,59.75400161743164,62.13840001424153`, function (d) { function removeNaN(e, c) { if (e > 0) { return e; } else { return c; } } return { date: d3.timeParse("%Y-%m-%d")(d.Date), aapl: +d.AAPL, tsla: +d.TSLA, aapl_sma: removeNaN(+d.SMA_AAPL, +d.AAPL), tsla_sma: removeNaN(+d.SMA_TSLA, +d.TSLA) }; }); chart.setData(data); </script> </body> </html>
Instead of listing all of them, I’ve created a diff file. Download and check. Left side is the original code.