Multi Line graph tooltip in d3 giving error - javascript

I have a multi line graph and i want to have a tool tip on it so that as i hover my mouse over the line it shows the point on x and y axis.
I have my data somewhere in the below form and i'm refrencing the mouse event part from somewhere here
I have my data in the below format and i'm passing it as object in angular between component
{storeid: "", peoplesum: 137.14285714285714, date: "2018-06-02"}
{storeid: "", peoplesum: 139.28571428571428, date: "2018-06-03"}
{storeid: "", peoplesum: 123, date: "2018-06-04"}
{storeid: "", peoplesum: 144, date: "2018-06-05"}
{storeid: "", peoplesum: 150, date: "2018-06-06"}
{storeid: "", peoplesum: 154.28571428571428, date: "2018-06-07"}
{storeid: "", peoplesum: 159.85714285714286, date: "2018-06-08"}
{storeid: "", peoplesum: 145.71428571428572, date: "2018-06-09"}
{storeid: "", peoplesum: 129.42857142857142, date: "2018-06-10"}
{storeid: "", peoplesum: 147, date: "2018-06-11"}
{storeid: "", peoplesum: 123, date: "2018-06-12"}
Code
import { Component, OnInit, Input } from '#angular/core';
import * as d3 from "d3"
import * as d3Scale from 'd3-scale';
import * as d3Shape from 'd3-shape';
import * as d3Array from 'd3-array';
import * as d3Axis from 'd3-axis';
import {timeParse} from "d3-time-format";
#Component({
selector: 'app-d3graph',
template: `
<h2>{{subtitle}}</h2>
`
})
export class D3graphComponent implements OnInit {
#Input() storeIntraffic: string;
#Input() dateForD3: string;
#Input() peopleInSumStr: string;
title: string = 'D3.js with Angular 2!';
subtitle: string = 'Line Chart';
peopleInSumArr: any[];
private margin = {top: 20, right: 20, bottom: 30, left: 50};
private width: number;
private height: number;
private x: any;
private y: any;
private svg: any;
private priceline: any;
private line: d3Shape.Line<[number, number]>;
d3Data: any;
dashboard_date: any;
myArray: any[];
groupName: string;
data: any;
constructor() {
this.margin = { top: 30, right: 20, bottom: 70, left: 50 },
this.width = 1300 - this.margin.left - this.margin.right,
this.height = 800 - this.margin.top - this.margin.bottom;
}
ngOnInit() { }
ngAfterViewChecked() {
if (this.storeIntraffic !== undefined && typeof this.storeIntraffic === 'string') {
this.data = JSON.parse(this.peopleInSumStr);
this.peopleInSumArr = JSON.parse(this.peopleInSumStr);
this.dashboard_date = this.dateForD3;
// this.dashboard_date = this.dateForD3;
// console.log('d3 this.peopleInSumArr', this.peopleInSumStr);
// this.peopleInSumArr = JSON.parse(this.peopleInSumStr);
// console.log('d3 this.peopleInSumArr jajdjhdhjd', this.peopleInSumArr);
// console.log('this.dateForD3', this.dateForD3);
// this.storeIntraffic_plot();
this.initSvg();
this.initAxis();
this.drawAxis();
this.drawLine();
}
}
private initSvg() {
d3.select("svg").remove();
this.svg = d3.select("#svgcontainer")
.append("svg")
.attr("width", this.width + this.margin.left + this.margin.right)
.attr("height", this.height + this.margin.top + this.margin.bottom)
.append("g")
.attr("transform",
"translate(" + this.margin.left + "," + this.margin.top + ")")
.attr("stroke-width", 2);
}
private initAxis() {
// Parse the date / time
var parseDate = timeParse("%b %Y");
// Set the ranges
this.x = d3Scale.scaleTime().range([0, this.width]);
this.y = d3Scale.scaleLinear().range([this.height, 0]);
}
private drawAxis() {
var X = this.x;
var Y = this.y;
// Define the line
this.priceline = d3Shape.line()
.x(function (d) { return X(new Date(d.date)); })
.y(function (d) { return Y(d.peoplesum); });
}
private drawLine() {
var mindate = new Date(this.dashboard_date['startTime']),
maxdate = new Date(this.dashboard_date['endTime']);
this.x.domain(d3.extent([mindate, maxdate]));
// Scale the range of the data
var svgVar = this.svg;
var pricelineVar = this.priceline;
var margin = this.margin;
var height = this.height;
this.y.domain([0, d3.max(this.data, function (d) { return d.peoplesum; })]);
console.log("this.data", this.data);
// Nest the entries by symbol
var dataNest = d3.nest()
.key(function (d) { return d.storeid;})
.entries(this.data);
console.log("asdasd", dataNest);
// set the colour scale
var color = d3.scaleOrdinal(d3.schemeCategory10);
console.log("width", this.width);
console.log("width", dataNest.length);
var legendSpace = this.width / dataNest.length; // spacing for the legend
console.log("this.legendSpace", legendSpace);
// Loop through each symbol / key
dataNest.forEach(function (d, i) {
svgVar.append("path")
.attr("class", "line")
.style("stroke", function () { // Add the colours dynamically
return d.color = color(d.key);
})
.on("mouseover", onMouseOver)
.attr("d",d.peoplesum)
.attr("d", pricelineVar(d.values))
.attr("stroke-width", 3)
.style("fill", "none")
function onMouseOver(d, i) {
d3.select(this)
.attr('class', 'highlight');
d3.select(this)
.transition()
.duration(200)
//.attr('width', this.x.bandwidth() + 5)
.attr("y", function(d) { return this.y(d.peoplesum) - 10; })
.attr("height", function(d) { return height - this.y(d.peoplesum) + 10; })
.append("text")
.attr('class', 'val')
.attr('x', function() {
return this.X(d.date);
})
.attr('y', function() {
return this.Y(d.peoplesum) ;
})
}
// Add the Legend
svgVar.append("text")
.attr("x", (legendSpace / 2) + i * legendSpace) // space legend
.attr("y", height + (margin.bottom / 2) + 5)
.attr("class", "legend") // style the legend
.style("fill", function () { // Add the colours dynamically
return d.color = color(d.key);
})
.text(d.key)
.attr("stroke-width", 3)
});
console.log("after", dataNest);
// Add the X Axis
this.svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + this.height + ")")
.call(d3.axisBottom(this.x));
// Add the Y Axis
this.svg.append("g")
.attr("class", "axis")
.call(d3.axisLeft(this.y));
}
}
I'm not sure what pricelineVar(d.values) is doing here and The error i'm getting is :
**core.js:1598 ERROR TypeError: Cannot read property 'peoplesum' of undefined
at SVGPathElement.<anonymous> (d3graph.component.ts:140)**

By changing the code to the below snippet i was able to resolve the error
svgVar.selectAll("path")
.data(dataNest)
.enter()
.append("path")
.attr("class", "line")
.style("stroke", function () { // Add the colours dynamically
return d.color = color(d.key);
})
.on("mouseover", onMouseOver)
.attr("d", function(d) { return pricelineVar(d.values); })
.attr("fill","none");

Related

TypeError: Cannot read property 'timeFormat' of undefined

I am creating a chart using d3 js and I was trying to convert a DateTime string to time. but it is returning TypeError: Cannot read property 'timeFormat' of undefined
The bellow is the full file that Created.
import {BaseElement} from '../../core/base-element';
import {utilsMixin} from '../../core/mixins/utils-mixin';
// import {LitElement} from 'lit-element';
import * as d3 from 'd3';
export const AreaChartBase = class extends utilsMixin(dataSourceMixin(BaseElement)) {
constructor() {
super();
this.title = '';
this.chartTheme = {
margin : {
top: 35,
right: 145,
bottom: 35,
left: 45
},
barsColors : '#23d160',
tickColor : '#aeaeae',
pathColor : '#aeaeae',
gridColor : '#aeaeae',
barTitleColor : "#23d160",
}
}
static get is() {
return 'area-chart';
}
static get properties() {
return {
title: String,
chartTheme: Object,
}
}
timeFormat(str){
const dateTimeString = new Date(str);
const time = dateTimeString.getTime();
const normalTime = new Date(time).toLocaleDateString("en-US");
return normalTime;
}
areaChart(selector, props, data){
// initialize the variables
const {
margin,
width,
height,
barsColors,
tickColor,
pathColor,
gridColor,
barTitleColor
} = props;
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const svg = d3.select(selector);
let chart = svg.selectAll('svg')
.data([null]);
chart = chart.enter()
.append('svg')
.merge(svg)
// .attr('width', innerWidth)
// .attr('height', innerHeight);
.attr('viewBox', `0,0, ${innerWidth}, ${innerHeight}`)
chart.append('text')
.attr('x', 0)
.attr('y', margin.top / 2)
.attr('text-anchor', 'start')
.style('font-size', '18px')
.attr('fill', '#4c6072')
.text('Area chart');
let g = chart.selectAll('g').data([null]);
g = g.enter()
.append('g')
.merge(g)
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// Add X axis --> it is a date format
const x = d3.scaleBand()
.rangeRound([0, innerWidth])
.padding(0.1);
const y = d3.scaleLinear()
.range([innerHeight - margin.bottom - 17, 0]);
x.domain(data.map(function (d) {
console.log(this.timeFormat(d[0]));
return this.timeFormat(d[0]);
}));
y.domain( d3.extent(data, function(d) { return +d[4]; }) );
const xAxis = d3.axisBottom(x);
const yAxis = d3.axisLeft(y)
.ticks(5)
.tickSizeInner(-innerWidth)
.tickSizeOuter(0)
.tickPadding(10);
let xAxisG = g.selectAll('.x axis').data([null]);
xAxisG = xAxisG.enter()
.append('g')
.attr('class', 'x axis')
.merge(xAxisG)
.attr('transform', `translate(0, ${innerHeight - margin.bottom - 17})`)
let yAxisG = g.selectAll('y axis').data([null]);
yAxisG = yAxisG.enter()
.append('g')
.attr('class', 'y axis')
.merge(yAxisG)
xAxisG.call(xAxis);
xAxisG.selectAll('.tick text')
.attr('fill', tickColor)
xAxisG.selectAll('.tick line')
.attr('stroke', pathColor);
xAxisG.select('.domain')
.attr('stroke', 'transparent');
yAxisG.call(yAxis);
yAxisG.selectAll('.tick text')
.attr('fill', tickColor)
yAxisG.selectAll('.tick line')
.attr('stroke', pathColor);
yAxisG.select('.domain')
.attr('stroke', 'transparent');
// append the svg object to the body of the page
// let svg = d3.select(selector)
// .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 + ")");
// const x = d3.scaleBand()
// .domain(data.map(function (d) {
// return d[0];
// }))
// .range([ 0, width ]);
// svg.append("g")
// .attr("transform", "translate(0," + (height+5) + ")")
// .call(d3.axisBottom(x).ticks(5).tickSizeOuter(0));
// // Add Y axis
// var y = d3.scaleLinear()
// .domain( d3.extent(data, function(d) { return +d[4]; }) )
// .range([ height, 0 ]);
// svg.append("g")
// .attr("transform", "translate(-5,0)")
// .call(d3.axisLeft(y).tickSizeOuter(0));
// Add the area
svg.append("path")
.datum(data)
.attr("fill", "#69b3a2")
.attr("fill-opacity", .3)
.attr("stroke", "none")
.attr("d", d3.area()
.x(function(d) { return x(d[0]) })
.y0( height )
.y1(function(d) { return y(+d[4]) })
)
// Add the line
svg.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "#69b3a2")
.attr("stroke-width", 2)
.attr("d", d3.line()
.x(function(d) { return x(d[0]) })
.y(function(d) { return y(+d[4]) })
)
// Add the line
svg.selectAll("myCircles")
.data(data)
.enter()
.append("circle")
.attr("fill", "red")
.attr("stroke", "none")
.attr("cx", function(d) { return x(d[0]) })
.attr("cy", function(d) { return y(parseInt(d[4])) })
.attr("r", 2)
}
initAreaChart(dsc){
const rows = dsc.rows;
const data = dsc.data;
const cont = this.qs('#chart');
this.areaChart(cont, Object.assign({}, this.chartTheme, {
width: this.qs('#container').clientWidth,
height: 400,
}), rows);
}
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
let self = this;
this.loader.then(dsc => {
self.initAreaChart(dsc);
});
}
dscDataName() {
return this.e.defaultValue;
}
init(pElement, loader) {
super.init(pElement, loader);
var self = this;
self.loader = this.loadData();
}
}```
if you guys can help me find a fix for the above error I will appreciate it. Thanks in advance
Try to use arrow function. Arrow functions keep context.
x.domain(data.map( (d) => {
console.log(this.timeFormat(d[0]));
return this.timeFormat(d[0]);
}));
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this

How can I display two trees with a central root?

I am trying to create two D3 trees with what appears to be a share central root. The trees should expand to the right and left as the user clicks on my nodes.
I came across an example here on stackoverflow.
Here is the link to that SO: Tree with children towards multiple side in d3.js (similar to family tree)
I even put together a bl.ocks.org version of this code here: https://bl.ocks.org/redcricket/de324f83aa6c84db2588c1a1f53cc5e3
The above examples are D3 v3. I am adapting the above example to D3 v4 and integrating it into an angular component,
but I have run into a problem in that I can only display only one tree at a time.
The entry point into my code is this angular component and its service component:
The component:
import { Component, OnInit, OnChanges, ViewChild, ElementRef, Input, Output, EventEmitter} from '#angular/core';
import { AngularD3TreeLibService } from './custom-d3-tree.service';
#Component({
selector: 'custom-angular-d3-tree-lib',
template: `<div class="d3-chart" #chart></div> `,
styleUrls: ['./custom-d3-tree.component.css']
})
export class AngularD3TreeLibComponent implements OnInit, OnChanges {
#ViewChild('chart') private chartContainer: ElementRef;
#Input() treeData: any = [];
#Output() onNodeChanged: EventEmitter<any>= new EventEmitter();
#Output() onNodeSelected: EventEmitter<any>= new EventEmitter();
constructor( private treeService: AngularD3TreeLibService ) {
treeService.setNodeChangedListener((node)=>{ this.onNodeChanged.emit(node); })
treeService.setNodeSelectedListener((node)=>{ this.onNodeSelected.emit(node); })
}
ngOnInit() {}
ngOnChanges(changes: any) { this.seedTree(); }
seedTree(){
if(!!this.treeData){
this.treeService.createChart(this.chartContainer, this.treeData);
this.treeService.update();
}
}
}
The service:
import { Injectable } from '#angular/core';
import { TreeModel } from './tree.dendo.model';
#Injectable({
providedIn: 'root'
})
export class AngularD3TreeLibService {
treeModel: TreeModel= new TreeModel();
constructor() { }
createChart(chartContainer: any, treeData: any): void {
let element = chartContainer.nativeElement;
element.innerHTML= "";
this.treeModel.addSvgToContainer(chartContainer);
this.treeModel.createLayout();
this.treeModel.createTreeData(treeData);
}
update(){
this.treeModel.rightTreeUpdate(this.treeModel.rroot);
this.treeModel.leftTreeUpdate(this.treeModel.lroot);
}
}
Note in the AngularD3TreeLibService.update() method I call rightTreeUpdate before I call leftTreeUpdate.
This results in only my left tree being visible.
In my TreeModel code I am able to display the right tree but not the left by calling leftTreeUpdate before rightTreeUpdate
in my click() function.
I suspect I am doing something wrong in my setNodes() and setLinks() methods as I really do not understand the purpose of things like nodeEnter, nodeUpdate and nodeExit.
Here is an edited (for brevity) version of my TreeModel.
import * as d3 from 'd3';
export class TreeModel {
rroot: any; // right root
lroot: any; // left root
treeLayout: any;
svg: any;
N: number = 10;
treeData: any;
rect_width: number = 125;
rect_height: number = 42;
height: number;
width: number;
margin: any = { top: 200, bottom: 90, left: 100, right: 90};
duration: number= 750;
nodeWidth: number = 1;
nodeHeight: number = 1;
nodeRadius: number = 5;
horizontalSeparationBetweenNodes: number = 1;
verticalSeparationBetweenNodes: number = 10;
selectedNodeByDrag: any;
selectedNodeByClick: any;
previousClickedDomNode: any;
... omitted for brevity ...
constructor(){}
addSvgToContainer(chartContainer: any){
let element = chartContainer.nativeElement;
this.width = element.offsetWidth - this.margin.left - this.margin.right;
this.height = element.offsetHeight - this.margin.top - this.margin.bottom;
this.svg = d3.select(element).append('svg')
.attr('width', element.offsetWidth)
.attr('height', element.offsetHeight)
.append("g")
.attr("transform", "translate("
+ this.margin.left + "," + this.margin.top + ")");
this.svg = this.svg.append("g");
... omitted for brevity ...
}
// zoom stuff
... omitted for brevity ...
// end zoom stuff
createLayout(){
this.treeLayout = d3.tree()
.size([this.height, this.width])
.nodeSize([this.nodeWidth + this.horizontalSeparationBetweenNodes, this.nodeHeight + this.verticalSeparationBetweenNodes])
.separation((a,b)=>{return a.parent == b.parent ? 50 : 200});
}
getRandomColor() {
... omitted for brevity ...
}
chunkify(a, n, balanced) {
... omitted for brevity ...
}
twoTreeBuildCenterNodesChildren(children:any) {
// this routine is suppose to build a json/tree object that represent the children of the center node.
// if there are more than N number of nodes on any level we need to create an additional level to
// accommodate these nodes.
... omitted for brevity ...
}
compare(a,b) {
... omitted for brevity ...
}
buildTwoTreeData(apiJson:any) {
var componentType = Object.keys(apiJson)[0];
var centerNodeLeft = {'component_type': componentType, "name": apiJson[componentType].name, "color": "#fff", "children": []};
var centerNodeRight = {'component_type': componentType, "name": apiJson[componentType].name, "color": "#fff", "children": []};
var tmp_leftNodes = [];
for ( var i=0; i < apiJson[componentType].multiparent.length; i++ ) {
var c = apiJson[componentType].multiparent[i];
c['color'] = this.getRandomColor();
c['name'] = c.parent.name;
tmp_leftNodes.push(c);
}
var leftNodes = tmp_leftNodes.sort(this.compare);
var rightNodes = apiJson[componentType].children.sort(this.compare);
var right_center_node_children = this.twoTreeBuildCenterNodesChildren(rightNodes.sort(this.compare));
var left_center_node_children = this.twoTreeBuildCenterNodesChildren(leftNodes.sort(this.compare));
centerNodeLeft.children = left_center_node_children;
centerNodeRight.children = right_center_node_children;
return[centerNodeLeft, centerNodeRight];
}
translateJson(apiJson:any){ return this.buildTwoTreeData(apiJson); }
createTreeData(rawData: any){
var parsedData = this.translateJson(rawData);
this.lroot = d3.hierarchy(parsedData[0]);
this.lroot.x0 = this.height / 2;
this.lroot.y0 = 0;
this.lroot.children.map((d)=>this.collapse(d));
this.rroot = d3.hierarchy(parsedData[1]);
this.rroot.x0 = this.height / 2;
this.rroot.y0 = 0;
this.rroot.children.map((d)=>this.collapse(d));
}
collapse(d) {
if(d.children) {
d._children = d.children
d._children.map((d)=>this.collapse(d));
d.children = null
}
}
expand_node(d) {
if (d.children) { d._children = d.children; d.children = null; } else { d.children = d._children; d._children = null; }
}
expand(d) {
if(d._children) {
d.children = d._children
d.children.map((d)=>this.expand(d));
d.children = null
}
}
rightTreeUpdate(source) {
const treeData = this.treeLayout(this.rroot);
this.setNodes(source, treeData, 'right');
this.setLinks(source, treeData, 'right');
}
leftTreeUpdate(source) {
const treeData = this.treeLayout(this.lroot);
this.setNodes(source, treeData, 'left');
this.setLinks(source, treeData, 'left');
}
setNodes(source:any, treeData: any, side: string){
let nodes = treeData.descendants();
let treeModel= this;
if ( side === 'left') {
let width = this.width;
nodes.forEach(function (d) { d.y = (d.depth * -180) });
} else {
// this draws everything to the right.
nodes.forEach(function(d){ d.y = d.depth * 180});
}
var node = this.svg.selectAll('g.node')
.data(nodes, function(d) { return d.id || (d.id = ++this.i); });
var nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr("transform", function(d) {
return " translate(" + source.y0 + "," + source.x0 + ")";
});
nodeEnter.append('rect')
.attr('class', 'node-rect')
.attr('x', 0)
.attr('y', 0)
.attr('rx', 6)
.attr('ry', 6)
.attr('width', this.rect_width)
.attr('height', this.rect_height)
.attr('stroke', 'black')
.style("fill", function(d) {
return d.data.color;
});
nodeEnter.append('text')
.attr('y', 20)
.attr('x', 40)
.attr("text-anchor", "middle")
.text(function(d){
return (d.data.name || d.data.description || d.id);
});
var nodeUpdate = nodeEnter.merge(node);
nodeUpdate.transition()
.duration(this.duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
var nodeExit = node.exit().transition()
.duration(this.duration)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
// On exit reduce the node circles size to 0
nodeExit.select('circle')
.attr('r', 1e-6);
// Store the old positions for transition.
nodes.forEach(function(d){
d.x0 = d.x;
d.y0 = d.y;
});
// On exit reduce the opacity of text labels
nodeExit.select('text')
.style('fill-opacity', 1e-6);
nodeEnter
.on('click', function(d){
treeModel.click(d, this);
//treeModel.update(d);
// treeModel.rightTreeUpdate(d);
});
}
... omitted for brevity ...
setLinks( source: any, treeData: any, side: string){
let links = treeData.descendants().slice(1);
var link = this.svg.selectAll('path.link')
.data(links, function(d) { return d.id; });
// Enter any new links at the parent's previous position.
var linkEnter = link.enter().insert('path', "g")
.attr("class", "link")
.attr('fill', 'none')
.attr('stroke', 'black')
.attr('d', (d)=>{
var o = {x: source.x0, y: source.y0}
return this.rdiagonalCurvedPath(o, o)
});
var linkUpdate = linkEnter.merge(link);
linkUpdate.transition()
.duration(this.duration)
.attr('d', (d)=>{return this.rdiagonalCurvedPath(d, d.parent)});
var linkExit = link.exit().transition()
.duration(this.duration)
.attr('d', (d) => {
var o = {x: source.x, y: source.y}
return this.rdiagonalCurvedPath(o, o)
})
.remove();
}
click(d, domNode) {
if( d._children ) {
this.expand_node(d);
} else if ( d.children) {
this.collapse(d);
} else {
console.log('click() skipping load of new data for now ');
}
// HERE IS WHERE I CALL
// rightTreeUpdate() after leftTreeUpdate() which displays the right tree, but not the left tree.
this.leftTreeUpdate(this.lroot);
this.rightTreeUpdate(this.rroot);
}
... omitted for brevity ...
}
After following Andrew Ried's suggestion I was able to draw both trees by changing just four lines of code.
In my setNodes() method I now have this:
// var node = this.svg.selectAll('g.node')
var node = this.svg.selectAll('g.node'+side)
.data(nodes, function(d) { return d.id || (d.id = ++this.i); });
var nodeEnter = node.enter().append('g')
// .attr('class', 'node')
.attr('class', 'node'+side)
.attr("transform", function(d) {
return " translate(" + source.y0 + "," + source.x0 + ")";
});
and in my setLinks method a similar change:
var link = this.svg.selectAll('path.link'+side)
.data(links, function(d) { return d.id; });
// Enter any new links at the parent's previous position.
var linkEnter = link.enter().insert('path', "g")
.attr("class", "link"+side)
.attr('fill', 'none')
.attr('stroke', 'black')
.attr('d', (d)=>{
var o = {x: source.x0, y: source.y0}
return this.rdiagonalCurvedPath(o, o)
});
Here's what my two trees look like now.
I still need to work on correctly drawing the links on the left, but that's another issue. Thanks Andrew!

how to perform d3 drag and drop

Having Issue with d3.drag for angular 4. whenever I drag the rectangle object , it is moving fine for first time. After release of mousepress and again trying to drag the rectangle, it is going back to previous event and not able to make mouse control on draggable object. Please give solution to my problem.
import { Component,Input, ElementRef, OnInit } from '#angular/core';
import * as d3 from 'd3';
interface LineData{
xVal: number,
yVal:number
}
#Component({
selector: 'app-line-chart',
template:'<svg height="500" width="500" ></svg>',
styleUrl: []
})
export class LineChartComponent implements OnInit {
#Input() data : LineData[];
private parentNativeElement : any;
constructor(private element:ElementRef) {
this.parentNativeElement = element.nativeElement;
}
ngOnInit() {
var width = 300;
var height = 300;
var margin = {top: 10, right: 10, bottom: 30, left: 10}
var x = d3.scaleLinear().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
var xAxis = d3.axisBottom(x)
.scale(x)
.ticks(5);
var yAxis = d3.axisLeft(y)
.scale(y)
.ticks(5);
var valueline:any = d3.line()
.x(function (d) {
return x(d['xVal']);
})
.y(function (d) {
return y(d['yVal']);
});
console.log(valueline);
var svg = d3.select("svg");
d3.select(this.parentNativeElement)
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
// Get the data
var data = [
{
"xVal": 1,
"yVal": 2
},
{
"xVal": 2,
"yVal": 4
},
{
"xVal": 3,
"yVal": 1
},
{
"xVal": 4,
"yVal": 5
},
{
"xVal": 5,
"yVal": 3
}
];
// Scale the range of the data
x.domain(d3.extent(data,
function (d) {
return d.xVal;
}));
y.domain([
0, d3.max(data,
function (d) {
return d.yVal;
})
]);
let color = d3.scaleOrdinal(d3.schemeCategory10);
svg.append("path").datum(data).attr("class","path")// Add the valueline path.
.attr("fill", "none")
.attr("stroke", "red")
.attr("stroke-width", 1.5)
.attr("d", valueline(data)).attr("class", "line");
let rectangle:any = d3.range(1).map(function(){
return{
x: Math.floor(Math.random()*width),
y: Math.floor(Math.random()*height)
};
});
console.log(rectangle);
let dragRect = svg.selectAll('g').data(rectangle).enter().append("g")
dragRect.append("rect")
.attr("x",function(d){return d['x'];})
.attr("y",function(d){return d['y'];})
.attr("height",50)
.attr("width",50).style("fill", "steelblue");
svg.selectAll('g').attr("transform",
"translate(" + margin.left + "," + margin.top + ")").data(rectangle)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
function dragstarted(d){
d3.select(this).raise().classed("active",true);
}
function dragged(d){
d.xVal = x.invert(d3.event.x);
d.yVal = y.invert(d3.event.y);
d3.select(this).select("rect")
.attr("x", x(d.xVal))
.attr("y", y(d.yVal))
.attr("transform","translate("+d.xVal+","+d.yVal+")")
console.log(d);
}
function dragended(d){
d3.select(this).raise().classed("active",false);
//d3.select('rect#no-drag').on('mousedown.drag',null);
}
svg.append("g") // Add the X Axis
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g") // Add the Y Axis
.attr("class", "y axis"`enter code here`)
.call(yAxis);
}
}
The basic problem seems to be that dragged function is not remembering the x & y between successive drag events.
To do this, you need
d3.select(this)
.attr("x", d.x = x(d.xVal))
.attr("y", d.y = y(d.yVal))
instead of
d3.select(this)
.attr("x", x(d.xVal))
.attr("y", y(d.yVal))
Run this code snippet to check it out
console.clear()
var width = 300;
var height = 300;
var margin = {top: 10, right: 10, bottom: 30, left: 10}
var x = d3.scaleLinear().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
var xAxis = d3.axisBottom(x).scale(x).ticks(5);
var yAxis = d3.axisLeft(y).scale(y).ticks(5);
var valueline = d3.line()
.x(function (d) { return x(d['xVal']); })
.y(function (d) { return y(d['yVal']); });
var svg = d3.select("svg");
d3.select(this.parentNativeElement)
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
// Get the data
var data = [
{
"xVal": 1,
"yVal": 2
},
{
"xVal": 2,
"yVal": 4
},
{
"xVal": 3,
"yVal": 1
},
{
"xVal": 4,
"yVal": 5
},
{
"xVal": 5,
"yVal": 3
}
];
// Scale the range of the data
x.domain(d3.extent(data,
function (d) {
return d.xVal;
}));
y.domain([
0, d3.max(data,
function (d) {
return d.yVal;
})
]);
let color = d3.scaleOrdinal(d3.schemeCategory10);
svg.append("path").datum(data).attr("class","path")
.attr("fill", "none")
.attr("stroke", "red")
.attr("stroke-width", 1.5)
.attr("d", valueline(data)).attr("class", "line");
let rectangle = d3.range(3).map(function() {
return {
x: Math.floor(Math.random()*width),
y: Math.floor(Math.random()*height)
};
});
let dragRect = svg.selectAll('g').data(rectangle).enter()
.append("g")
dragRect.append("rect")
.attr("x",function(d){return d['x'];})
.attr("y",function(d){return d['y'];})
.attr("height", 50)
.attr("width", 50)
.style("fill", "steelblue")
svg.selectAll('rect')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.data(rectangle)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
);
const dragBounds = {}
const tickHeight = 10;
function setDragBounds(subject) {
dragBounds.top = 0 - margin.top;
dragBounds.left = 0 - margin.left;
dragBounds.bottom = height - tickHeight - subject.attr('height');
dragBounds.right = width - margin.right - subject.attr('width');
}
function dragstarted(d){
/*
Calculate drag bounds at dragStart because it's one event vs many
events if done in 'dragged()'
*/
setDragBounds(d3.select(this))
d3.select(this).raise().classed("active", true);
}
function dragged(d){
d3.select(this)
.attr("x", getX(d.x = d3.event.x) )
.attr("y", getY(d.y = d3.event.y) );
}
function getX(x) {
return x < dragBounds.left ? dragBounds.left
: x > dragBounds.right ? dragBounds.right
: x
}
function getY(y) {
return y < dragBounds.top ? dragBounds.top
: y > dragBounds.bottom ? dragBounds.bottom
: y
}
function dragended(d){
d3.select(this).classed("active", false);
}
svg.append("g") // Add the X Axis
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
svg.append("g") // Add the Y Axis
.attr("class", "y axis")
.call(yAxis);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg height="500" width="500" ></svg>
Note I attached the drag events to rect instead of g, which I assume was a typo.
Keeping within bounds
To constrain the drag to bounds the way I found works best is a max/min function on the x & y values.
function dragged(d){
d3.select(this)
.attr("x", getX(d.x = d3.event.x) )
.attr("y", getY(d.y = d3.event.y) );
}
function getX(x) {
return x < dragBounds.left ? dragBounds.left
: x > dragBounds.right ? dragBounds.right
: x
}
function getY(y) {
return y < dragBounds.top ? dragBounds.top
: y > dragBounds.bottom ? dragBounds.bottom
: y
}
Bounds are set at drag start to avoid repetition of any calculation.
const dragBounds = {}
const tickHeight = 10;
function setDragBounds(subject) {
dragBounds.top = 0 - margin.top;
dragBounds.left = 0 - margin.left;
dragBounds.bottom = height - tickHeight - subject.attr('height');
dragBounds.right = width - margin.right - subject.attr('width');
}
function dragstarted(d){
/*
Calculate drag bounds at dragStart because it's one event vs many
events if done in 'dragged()'
*/
setDragBounds(d3.select(this))
d3.select(this).raise().classed("active", true);
}

Cannot read property 'getAttribute' of null : Angular and d3.select

I'm building a series of d3 graphs within angular 5. I'm graphing the same d3 multiple times with different data each time, so I'm assigning a variable to the id. The problem is, the id is assigned at the same time that 'd3.select()' is looking for it, therefore it hasn't been assigned yet and I get error Cannot read property 'getAttribute' of null
How can I assign the 'id' of my intended 'svg' a variable from the same data that I use to graph it? Right now it's all in an 'onInit' function in my component.
In my precipitation.component.ts:
this.input_id += this.data.id
var svg = d3.select("#"+this.input_id)
My html looks like:
<svg width="180" height="100"[attr.id]="input_id" > </svg>
The whole precipitation.component.ts for reference:
import { Component, OnInit, Input } from '#angular/core';
import { DataService } from '../data.service';
import { Http } from '#angular/http';
import * as d3 from 'd3';
#Component({
selector: 'app-precipitation',
templateUrl: './precipitation.component.html',
styleUrls: ['./precipitation.component.css']
})
export class PrecipitationComponent implements OnInit {
#Input() site1;
constructor(private _dataService: DataService, private http: Http) { }
date: Date;
dates: any;
value: Number;
eachobj: any;
data: any;
values: any;
average: any;
myClasses: any;
name: any;
input_id;
ngOnInit() {
this.name = this.site1.name;
var parse = d3.timeParse("%Y-%m-%d")
this.data = this.site1
this.input_id = ""
this.input_id += "precipitation"
this.input_id += this.data.id
this.dates = Object.keys(this.data.historical_data)
this.values = Object.values(this.data.historical_data)
this.values.forEach((obj, i) => obj.date = this.dates[i]);
this.data = this.values
var svg = d3.select("#"+this.input_id)
// svg = svg.select("." + this.name)
var margin = { top: 20, right: 20, bottom: 30, left: 0 },
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
barPadding = 5,
barWidth = (width / this.data.length);
var last = this.data[0].date
var first = this.data[(this.data.length) - 1].date
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scaleBand().rangeRound([0, width]).padding(0.1)
.domain(this.data.map(function (d) { return d['date']; }));
var y = d3.scaleLinear().rangeRound([height, 0])
.domain(<[Date, Date]>d3.extent(this.data, function (d) { return d['precipitation']; }));
g.selectAll(".bar")
.data(this.data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function (d) { return x(d['date']); })
.attr("y", function (d) { return y(d['precipitation']); })
.attr("width", barWidth - barPadding)
.attr("height", function (d) { return height - y(d['precipitation']); });
var line = d3.line()
.x(function (d) { return x(d['date']); })
.y(function (d) { return y(d['precipitation']); });
svg.append('g')
.attr("class", "axis--x")
.append("text")
.attr("fill", "#000")
.attr("x", (width / 2) - 80)
.attr("y", height + 40)
.text(first)
.style("font-size", "09")
.style("font-family", "Roboto")
.style('fill', '#5a5a5a');
svg.append('g')
.attr("class", "axis--x")
.append("text")
.attr("fill", "#000")
.attr("x", (width / 2) + 45)
.attr("y", height + 40)
.text(last)
.style("font-size", "09")
.style("font-family", "Roboto")
.style('fill', '#5a5a5a');
svg.append("path")
.data(this.data)
.attr("class", "line")
.attr("d", line);
}
}
By the way I receive the data through the parent component:
<app-precipitation [site1]="site"></app-precipitation>

No tooltip when hover the mouse over multiline chart

I'm using d3 as my charting library and after the graph plot as i hover my mouse i can't see any values at the datapoint but in the HTML console i can see event is getting triggered.
I want to see date and value when i hover but i'm not getting any idea how to proced with it as i'm very new to d3.
below is the small snippet i coded :
dataNest.forEach(function (d, i) {
svgVar.selectAll("path")
.data(dataNest)
.enter()
.append("path")
.attr("class", "line")
.style("stroke", function () { // Add the colours dynamically
return d.color = color(d.key);})
.on("mouseover", tooltip)
.attr("d", function(d) { return pricelineVar(d.values); })
.attr("fill","none");
function tooltip(d,i)
{
svgVar.style("left", d3.event.pageX + "px")
.style("top", d3.event.pageY + "px")
.style("display", "inline-block")
.html(" x:"+ d.date );
}
If anyone wants to give any suggestion i can paste the full code as well:
Full code
import { Component, OnInit, Input } from '#angular/core';
import * as d3 from "d3"
import * as d3Scale from 'd3-scale';
import * as d3Shape from 'd3-shape';
import * as d3Array from 'd3-array';
import * as d3Axis from 'd3-axis';
import {timeParse} from "d3-time-format";
#Component({
selector: 'app-d3graph',
template: `
<h2>{{subtitle}}</h2>
`
})
export class D3graphComponent implements OnInit {
#Input() storeIntraffic: string;
#Input() dateForD3: string;
#Input() peopleInSumStr: string;
title: string = 'D3.js with Angular 2!';
subtitle: string = 'Line Chart';
peopleInSumArr: any[];
private margin = {top: 20, right: 20, bottom: 30, left: 50};
private width: number;
private height: number;
private x: any;
private y: any;
private svg: any;
private priceline: any;
private line: d3Shape.Line<[number, number]>;
d3Data: any;
dashboard_date: any;
myArray: any[];
groupName: string;
data: any;
constructor() {
this.margin = { top: 30, right: 20, bottom: 70, left: 50 },
this.width = 1300 - this.margin.left - this.margin.right,
this.height = 800 - this.margin.top - this.margin.bottom;
}
ngOnInit() { }
ngAfterViewChecked() {
if (this.storeIntraffic !== undefined && typeof this.storeIntraffic === 'string') {
this.data = JSON.parse(this.peopleInSumStr);
this.peopleInSumArr = JSON.parse(this.peopleInSumStr);
this.dashboard_date = this.dateForD3;
// this.dashboard_date = this.dateForD3;
// console.log('d3 this.peopleInSumArr', this.peopleInSumStr);
// this.peopleInSumArr = JSON.parse(this.peopleInSumStr);
// console.log('d3 this.peopleInSumArr jajdjhdhjd', this.peopleInSumArr);
// console.log('this.dateForD3', this.dateForD3);
// this.storeIntraffic_plot();
this.initSvg();
this.initAxis();
this.drawAxis();
this.drawLine();
}
}
private initSvg() {
d3.select("svg").remove();
this.svg = d3.select("#svgcontainer")
.append("svg")
.attr("width", this.width + this.margin.left + this.margin.right)
.attr("height", this.height + this.margin.top + this.margin.bottom)
.append("g")
.attr("transform",
"translate(" + this.margin.left + "," + this.margin.top + ")")
.attr("stroke-width", 2);
}
private initAxis() {
// Parse the date / time
var parseDate = timeParse("%b %Y");
// Set the ranges
this.x = d3Scale.scaleTime().range([0, this.width]);
this.y = d3Scale.scaleLinear().range([this.height, 0]);
}
private drawAxis() {
var X = this.x;
var Y = this.y;
// Define the line
this.priceline = d3Shape.line()
.x(function (d) { return X(new Date(d.date)); })
.y(function (d) { return Y(d.peoplesum); });
}
private drawLine() {
var mindate = new Date(this.dashboard_date['startTime']),
maxdate = new Date(this.dashboard_date['endTime']);
this.x.domain(d3.extent([mindate, maxdate]));
// Scale the range of the data
var svgVar = this.svg;
var pricelineVar = this.priceline;
var margin = this.margin;
var height = this.height;
this.y.domain([0, d3.max(this.data, function (d) { return d.peoplesum; })]);
console.log("this.data", this.data);
// Nest the entries by symbol
var dataNest = d3.nest()
.key(function (d) { return d.storeid;})
.entries(this.data);
// set the colour scale
var color = d3.scaleOrdinal(d3.schemeCategory10);
var legendSpace = this.width / dataNest.length; // spacing for the legend
// Loop through each symbol / key
var tooltip = d3.select("body").append("div").style("display", "none");
function enableTooltip(d) {
tooltip.html("x:" + d.date)
.style("left", d3.event.pageX + "px")
.style("top", d3.event.pageY + "px")
.style("display", null);
}
dataNest.forEach(function (d, i) {
svgVar.selectAll("path")
.data(dataNest)
.enter()
.append("path")
.on("mouseover", enableTooltip)
.attr("d", function(d) { return pricelineVar(d.values); })
.attr("fill", "none");
// Add the Legend
svgVar.append("text")
.attr("x", (legendSpace / 2) + i * legendSpace) // space legend
.attr("y", height + (margin.bottom / 2) + 5)
.attr("class", "legend") // style the legend
.style("fill", function () { // Add the colours dynamically
return d.color = color(d.key);
})
.text(d.key)
.attr("stroke-width", 3)
});
console.log("after", dataNest);
// Add the X Axis
this.svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + this.height + ")")
.call(d3.axisBottom(this.x));
// Add the Y Axis
this.svg.append("g")
.attr("class", "axis")
.call(d3.axisLeft(this.y));
}
}
To get a tootltip during mouseover, first you have to define a tooltip with display as none.
var tooltip = d3.select("body").append("div").style("display", "none");
Then during mouseover event enable the tooltip and bind data to show in the tooltip.
function enableTooltip(d) {
tooltip.html("x:" + d.date)
.style("left", d3.event.pageX + "px")
.style("top", d3.event.pageY + "px")
.style("display", null);
}
You dont need forEach to iterate array. d3 data() takes care of that.
svgVar.selectAll("path")
.data(this.data)
.enter()
.append("path")
.on("mouseover", enableTooltip)
.attr("d", function(d) { return pricelineVar(d.values); })
.attr("fill", "none");
svgVar.selectAll("text")
.data(this.data)
.enter()
.append("text")
.attr("x", (legendSpace / 2) + i * legendSpace)
.attr("y", height + (margin.bottom / 2) + 5)
.attr("class", "legend")
.style("fill", function () {
return d.color = color(d.key);
})
.text(d.key)
.attr("stroke-width", 3);

Categories

Resources