Trying to place two d3 line graphs inside one Ionic2 page - angularjs

I am trying to have two (or more) similiar graph on one page inside an Ionic2 app. I use d3-ng2-service for wrapping the d3 types for Angular2. My problem is the following: When I try to place the two graphs in two different div elements each inside their respective custom element the drawing fails for both. When I did select the first div in the page the second graph overrides the first one, but it does get drawn.
Is there a clever way to place graphs more the the one graph? The examples always give the outer container a unique id, which is, what I try to do too:
import { Component, Input, OnInit, ElementRef } from '#angular/core';
import { D3Service, D3, Selection, ScaleLinear, ScaleTime, Axis, Line } from 'd3-ng2-service'; // <-- import the D3 Service, the type alias for the d3 variable and the Selection interface
#Component({
selector: 'd3-test-app',
templateUrl: 'd3-test-app.html',
providers: [D3Service],
})
export class D3TestAppComponent {
//Time is on x-axis, value is on y-axis
#Input('timeSeries') timeSeries: Array<{isoDate: string | Date | number | {valueOf(): number}, value: number}>;
#Input('ref') ref: string;
/* the size input defines, how the component is drawn */
#Input('size') size: string;
private d3: D3;
private margin: {top: number, right: number, bottom: number, left: number};
private width: number;
private height: number;
private d3ParentElement: Selection<any, any, any, any>; // <-- Use the Selection interface (very basic here for illustration only)
constructor(element: ElementRef,
d3Service: D3Service) { // <-- pass the D3 Service into the constructor
this.d3 = d3Service.getD3(); // <-- obtain the d3 object from the D3 Service
this.d3ParentElement = element.nativeElement;
}
ngOnInit() {
let x: ScaleTime<number, number>;
let y: ScaleLinear<number, number>;
let minDate: number;
let maxDate: number;
let minValue: number = 0;
let maxValue: number;
// set the dimensions and margins of the graph
switch (this.size) {
case "large":
this.margin = {top: 20, right: 20, bottom: 30, left: 50};
this.width = 640 - this.margin.left - this.margin.right;
this.height = 480 - this.margin.top - this.margin.bottom;
break;
case "medium":
this.margin = {top: 20, right: 0, bottom: 20, left: 20};
//golden ratio
this.width = 420 - this.margin.left - this.margin.right;
this.height = 260 - this.margin.top - this.margin.bottom;
break;
case "small":
this.margin = {top: 2, right: 2, bottom: 3, left: 5};
this.width = 120 - this.margin.left - this.margin.right;
this.height = 80 - this.margin.top - this.margin.bottom;
break;
default:
this.margin = {top: 20, right: 20, bottom: 30, left: 50};
this.width = 640 - this.margin.left - this.margin.right;
this.height = 480 - this.margin.top - this.margin.bottom;
}
// ...
if (this.d3ParentElement !== null) {
let d3 = this.d3; // <-- for convenience use a block scope variable
//THIS FAILS...
let selector: string = '#' + this.ref + ' .graphContainer';
console.log(selector);
let svg = d3.select( selector).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 + ")");
this.timeSeries.forEach((d) => {
d.isoDate = +d3.isoParse(d.isoDate as string);
d.value = +d.value;
if (minDate == null || minDate >= d.isoDate) {
minDate = d.isoDate as number;
}
if (maxDate == null || maxDate <= d.isoDate) {
maxDate = d.isoDate as number;
}
// if (minValue == null || minValue >= d.value) {
// minValue = d.value as number;
// }
if (maxValue == null || maxValue <= d.value) {
maxValue = d.value as number;
}
});
// TODO magic numbers to real min max
x = d3.scaleTime().domain([minDate, maxDate]).range([0,this.width]);
y = d3.scaleLinear().domain([0, maxValue]).range([this.height, 0]);
let xAxis: Axis<number | Date | {valueOf() : number;}> = d3.axisBottom(x);
let yAxis: Axis<number | {valueOf(): number;}> = d3.axisLeft(y);
let valueLine: Line<{isoDate: number; value: number}> = d3.line<{ isoDate: number; value: number }>()
.x(function (d) { return x(d.isoDate)})
.y(function (d) { return y(d.value)});
// Add the valueline path.
svg.append("path")
.data([this.timeSeries as {isoDate: number, value: number}[]])
.attr("class", "line")
.attr("d", valueLine);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + this.height + ")")
.call(xAxis)
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
}
}
myParser() : (string) => Date {
return this.d3.utcParse("%Y-%m-%dT%H:%M:%S.%LZ");
}
}
The HTML:
<div class='graphContainer'>
</div>
The HTML file where the custom component is used:
<ion-header>
<ion-navbar #dashboardNav>
<ion-title>Dashboard</ion-title>
<button ion-button menuToggle="favMenu" right>
<ion-icon name="menu"></ion-icon>
</button>
</ion-navbar>
</ion-header>
<ion-content>
<ion-item *ngFor="let entry of dashboard">
{{ entry.name }}
<d3-test-app [id]='entry.name' [timeSeries]='entry.timeSeries' [ref]='entry.name' size='medium'></d3-test-app>
</ion-item>
</ion-content>

Hard to debug without seeing a stack trace but it looks like this is failing because of how you select the element. I will base my answer on that assumption.
Querying by ID is handy when you have to look inside the DOM for a specific element and when you are sure there is only one element with that ID. Since you are inside an Angular element you already have the reference you need, it's the element itself, no need to dynamically create ID references.
I am not an expert in ng2 at all, but take a look a at how to select the raw element in Angular and choose the best approach for the framework. Say you go for something like the example shown on this answer:
constructor(public element: ElementRef) {
this.element.nativeElement // <- your direct element reference
}
NB - looks like there are various way of achieving this... not sure this is the best/correct one, but the goal is to get the ref anyway
Then simply select it via the D3 dsl like you are already doing by passing that raw reference
// At this point this.element.nativeElement
// should contain the raw element reference
if (this.d3ParentElement !== null) {
const { d3, element } = this; // <-- for convenience use block scope variables
const svg = d3.select(element.nativeElement).append("svg")...

Related

how to align d3 graph x axis tick label with the lower end of y axis?

Could some one please help with d3 ticks alignment?
I want the x axis ticks label in the screenshot below to be aligned to bottom location (-2.00) in this case. I have tried quite a few options, but could not figure out.
Any help is appreciated! Thanks
My code is given at the bottom.
const xScale = d3
.scaleLinear()
.domain([
data && data[0] ? Math.min(...data[0].items.map(d => d.hour)) : 0,
data && data[0] ? Math.max(...data[0].items.map(d => d.hour)) : 0
])
.range([0, width]);
const yScale = d3
.scaleLinear()
.domain([
data && data[0] ? getDataYMin() - 2 * (Math.ceil(Math.abs(getDataYMin())/10)) : 0,
data && data[0] ? getDataYMax() + 4 * (Math.ceil(getDataYMax()/10)): 0
]).range([height, 0]);
const svgEl = d3.select(svgRef.current);
svgEl.selectAll("*").remove(); // Clear svg content before adding new elements
const svg = svgEl
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Add X grid lines with labels
const xAxis = d3.axisBottom(xScale).ticks(24).tickSize(-height + margin.bottom).tickPadding(10);
const xAxisGroup = svg.append("g").attr("transform", `translate(0, ${height - margin.bottom})`).call(xAxis);
xAxisGroup.select(".domain").remove();
xAxisGroup.selectAll(".tick:first-of-type line").attr("class", "axis_bar").attr("stroke", "black");
xAxisGroup.selectAll(".tick:not(:first-of-type) line").attr("class", "axis_y_tick").attr("stroke", "rgba(155, 155, 155, 0.5)").style("stroke-dasharray", "5 5");
xAxisGroup.selectAll("text").attr("opacity", 0.5).attr("color", "black").attr("font-size", "0.75rem");
// Add Y grid lines with labels
const yAxis = d3.axisLeft(yScale).ticks(10).tickSize(-width).tickFormat(d3.format(".2f"));
const yAxisGroup = svg.append("g").call(yAxis);
yAxisGroup.select(".domain").remove();
yAxisGroup.selectAll(".tick:first-of-type line").attr("class", "axis_bar").attr("stroke", "black");
yAxisGroup.selectAll(".tick:not(:first-of-type) line").attr("class", "axis_y_tick").attr("stroke", "rgba(155, 155, 155, 0.5)").style("stroke-dasharray", "5 5");
yAxisGroup.selectAll("text").attr("opacity", 0.5).attr("color", "black").attr("font-size", "0.75rem");
The dimensions are defined constants and are passed through property to the chart component (this is a React application)
const marginValue: Margin = {
top: 30,
right: 30,
bottom: 30,
left: 60
};
const dimensionsInput : Dimensions = {
width: 1040,
height: 400,
margin: marginValue
};

Chartjs doughnut chart rounded corners for a cicle with a 3*Pi/2 circumference

I'm working with React and ChartJS to draw a doughnut chart with a 3*Pi/2 circumference
and rounded corner.
I saw these two posts where they explain how to round corners for data sets and it is working as expected with a complete circle and with half a circle:
ChartJs - Round borders on a doughnut chart with multiple datasets
Chartjs doughnut chart rounded corners for half doghnut
One answer on this post is to change "y" or "x" translation by factor of n, for example 2 in the following case: ctx.translate(arc.round.x, arc.round.y*2);
With this in mind I started to change values for x and y but have not yet reach the correct set of values that will make it work.
For example I tried to use a factor of 3/2 on the translation of y and this is what I get.
ctx.translate(arc.round.x, (arc.round.y * 3) / 2);
with no factor I get the following:
ctx.translate(arc.round.x, arc.round.y);
The code to round the end corner is exactly the same as in the posts I refer. But here it is just in case:
let roundedEnd = {
// #ts-ignore
afterUpdate: function (chart) {
var a = chart.config.data.datasets.length - 1;
for (let i in chart.config.data.datasets) {
for (
var j = chart.config.data.datasets[i].data.length - 1;
j >= 0;
--j
) {
if (Number(j) == chart.config.data.datasets[i].data.length - 1)
continue;
var arc = chart.getDatasetMeta(i).data[j];
arc.round = {
x: (chart.chartArea.left + chart.chartArea.right) / 2,
y: (chart.chartArea.top + chart.chartArea.bottom) / 2,
radius:
chart.innerRadius +
chart.radiusLength / 2 +
a * chart.radiusLength,
thickness: (chart.radiusLength / 2 - 1) * 2.5,
backgroundColor: arc._model.backgroundColor,
};
}
a--;
}
},
// #ts-ignore
afterDraw: function (chart) {
var ctx = chart.chart.ctx;
for (let i in chart.config.data.datasets) {
for (
var j = chart.config.data.datasets[i].data.length - 1;
j >= 0;
--j
) {
if (Number(j) == chart.config.data.datasets[i].data.length - 1)
continue;
var arc = chart.getDatasetMeta(i).data[j];
var startAngle = Math.PI / 2 - arc._view.startAngle;
var endAngle = Math.PI / 2 - arc._view.endAngle;
ctx.save();
ctx.translate(arc.round.x, arc.round.y);
console.log(arc.round.startAngle);
ctx.fillStyle = arc.round.backgroundColor;
ctx.beginPath();
//ctx.arc(arc.round.radius * Math.sin(startAngle), arc.round.radius * Math.cos(startAngle), arc.round.thickness, 0, 2 * Math.PI);
ctx.arc(
arc.round.radius * Math.sin(endAngle),
arc.round.radius * Math.cos(endAngle),
arc.round.thickness,
0,
2 * Math.PI
);
ctx.closePath();
ctx.fill();
ctx.restore();
}
}
}, };
These are the options to configure the chart:
const chartJsOptions = useMemo<chartjs.ChartOptions>(() => {
if (data) {
return {
elements: {
center: {
text: `${data.impact > 0 ? "%"}`,
color: isDarkTheme ? darkText : greyAxis, // Default is #000000
fontStyle: "Open Sans Hebrew, sans-serif",
sidePadding: 20, // Default is 20 (as a percentage)
minFontSize: 15, // Default is 20 (in px), set to false and text will not wrap.
lineHeight: 20, // Default is 25 (in px), used for when text wraps
},
},
legend: {
display: false,
},
// rotation: Math.PI / 2,
rotation: (3 * Math.PI) / 4,
circumference: (3 * Math.PI) / 2,
maintainAspectRatio: false,
animation: {
duration: ANIMATION_DURATION,
},
plugins: {
datalabels: false,
labels: false,
},
cutoutPercentage: 90,
tooltips: {
enabled: false,
rtl: true,
},
};
} else {
return {};
} }, [data, isDarkTheme]);
Here is where I call the react component for the chart:
<Doughnut
data={chartJsData}
options={chartJsOptions}
plugins={[roundedEnd]} />
How can I correctly calculate the rounded edges on a 3*Pi/2 circumference or any other circumference between complete and half?
This issue may be more of a math than programing and my geometrical math is also a bit rusty.

How to create and set a setMapTypeId using react-google-maps

I was looking for a way to create my own mars map in a website, using google maps.
I found this example in google map api
function initMap() {
var map = new google.maps.Map(document.getElementById('map'), {
center: {lat: 0, lng: 0},
zoom: 1,
streetViewControl: false,
mapTypeControlOptions: {
mapTypeIds: ['moon']
}
});
var moonMapType = new google.maps.ImageMapType({
getTileUrl: function(coord, zoom) {
var normalizedCoord = getNormalizedCoord(coord, zoom);
if (!normalizedCoord) {
return null;
}
var bound = Math.pow(2, zoom);
return '//mw1.google.com/mw-planetary/lunar/lunarmaps_v1/clem_bw' +
'/' + zoom + '/' + normalizedCoord.x + '/' +
(bound - normalizedCoord.y - 1) + '.jpg';
},
tileSize: new google.maps.Size(256, 256),
maxZoom: 9,
minZoom: 0,
radius: 1738000,
name: 'Moon'
});
map.mapTypes.set('moon', moonMapType);
map.setMapTypeId('moon');
}
// Normalizes the coords that tiles repeat across the x axis (horizontally)
// like the standard Google map tiles.
function getNormalizedCoord(coord, zoom) {
var y = coord.y;
var x = coord.x;
// tile range in one direction range is dependent on zoom level
// 0 = 1 tile, 1 = 2 tiles, 2 = 4 tiles, 3 = 8 tiles, etc
var tileRange = 1 << zoom;
// don't repeat across y-axis (vertically)
if (y < 0 || y >= tileRange) {
return null;
}
// repeat across x-axis
if (x < 0 || x >= tileRange) {
x = (x % tileRange + tileRange) % tileRange;
}
return {x: x, y: y};
}
/* Always set the map height explicitly to define the size of the div
* element that contains the map. */
#map {
height: 100%;
}
/* Optional: Makes the sample page fill the window. */
html, body {
height: 100%;
margin: 0;
padding: 0;
}
<div id="map"></div>
<!-- Replace the value of the key parameter with your own API key. -->
<script
async
defer
src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCkUOdZ5y7hMm0yrcCQoCvLwzdM6M8s5qk&callback=initMap">
</script>
https://jsfiddle.net/dobleuber/319kgLh4/
It works perfect, but I would like to create the same thing with react using react-google-maps.
I looked out in the react-google-maps code but I only see getters no setters for the map props:
getMapTypeId, getStreetView, ect.
Is there any way to achieve this without modify the react-google-maps code?
Thanks in advance
use props mapTypeId="moon" in react-google-maps
I've found a better way to solve this that preserve the changes on re-render, leaving it here to anyone who comes here.
there is an onLoad function that exposes a map instance, we can use this to set mapTypeId instead of passing it as an option. In this way, if the user changes the map type later, it will preserve the changes on re-render.
<GoogleMap
onLoad={(map) => {
map.setMapTypeId('moon');
}}
/>

Add horizontal lines on y-axis below labels

I have a scatter plot graph in my reactJS app using d3 and svg. I need to display horizontal lines on y-axis below the labels. Currently, the horizontal lines are appearing beside the labels.
here's my code:
const yAxis = d3.axisLeft(yScale).tickFormat(function(d) {
if (d != minYFloor)
return d + " yds";
else return "";
})
.tickSize(-width - 20, 0, 0);
tickSize function in above code renders horizontal lines in the graph like this:
This is my css:
.axisY line {
fill: none;
stroke: #fff;
opacity: 0.3;
shape-rendering: crispEdges;
}
While I need something like this:
How do I go about achieving that?
Here is a basic example of how you can translate the ticks in the axis.
Let's suppose this running snippet:
var svg = d3.select("svg");
var scale = d3.scalePoint()
.domain([0, 50, 100, 150, 200])
.range([140, 10]);
var axis = d3.axisLeft(scale)
.tickFormat(function(d) {
return d + " yds"
})
.tickSize(-250);
var g = svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(50,0)")
.call(axis);
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
As you can see, it's a common y axis, just like yours.
To translate the ticks we first assign that axis'group a specific class (or ID):
.attr("class", "axis")
Then, we select the texts with the ticks class, and move them:
svg.selectAll(".axis .tick text")
.style("text-anchor", "start")
.attr("transform", "translate(4,-6)")
Here is the demo:
var svg = d3.select("svg");
var scale = d3.scalePoint()
.domain([0, 50, 100, 150, 200])
.range([140, 10]);
var axis = d3.axisLeft(scale)
.tickFormat(function(d) {
return d + " yds"
})
.tickSize(-250);
var g = svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(50,0)")
.call(axis);
svg.selectAll(".axis .tick text")
.style("text-anchor", "start")
.attr("transform", "translate(4,-6)")
.axis path {
stroke: none;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
Here I'm using magic numbers, change them accordingly.
Finally, there is an important advice: unlike what your question's title suggest, move the labels, not the lines. The lines are the actual indicator of the position to the users seeing the datavis.

D3 area graph animation

I have an area graph ( see js fiddle https://jsfiddle.net/o7df3tyn/ ) I want to animate this area graph. I tried the approach in this
question , but this doesnt seem to help because I have more line graphs in the the same svg element
var numberOfDays = 30;
var vis = d3.select('#visualisation'),
WIDTH = 1000,
HEIGHT = 400,
MARGINS = {
top: 20,
right: 20,
bottom: 20,
left: 50
};
var drawArea = function (data) {
var areaData = data;
// var areaData = data.data;
var xRange = d3.scale.linear().range([MARGINS.left, WIDTH - MARGINS.right]).domain([0, numberOfDays + 1]),
yRange = d3.scale.linear().range([HEIGHT - MARGINS.top, MARGINS.bottom]).domain([_.min(areaData), _.max(areaData)]);
var area = d3.svg.area()
.interpolate("monotone")
.x(function(d) {
return xRange(areaData.indexOf(d));
})
.y0(HEIGHT)
.y1(function(d) {
return yRange(d);
});
var path = vis.append("path")
.datum(areaData)
.attr("fill", 'lightgrey')
.attr("d", area);
};
var data = [1088,978,1282,755,908,1341,616,727,1281,247,1188,11204,556,15967,623,681,605,7267,4719,9665,5719,5907,3520,1286,1368,3243,2451,1674,1357,7414,2726]
drawArea(data);
So I cant use the curtain approach.
I want to animate the area from bottom.
Any ideas / explanations ?
Just in case anyone else stuck in the same problem, #thatOneGuy nailed the exact problem. My updated fiddle is here https://jsfiddle.net/sahils/o7df3tyn/14/
https://jsfiddle.net/DavidGuan/o7df3tyn/2/
vis.append("clipPath")
.attr("id", "rectClip")
.append("rect")
.attr("width", 0)
.attr("height", HEIGHT);
You can have a try now.
Remember add clip-path attr to the svg elements you want to hide
In this case
var path = vis.append("path")
.datum(areaData)
.attr("fill", 'lightgrey')
.attr("d", area)
.attr("clip-path", "url(#rectClip)")
Update:
If we want to grow the area from bottom:
vis.append("clipPath")
.attr("id", "rectClip")
.append("rect")
.attr("width", WIDTH)
.attr("height", HEIGHT)
.attr("transform", "translate(0," + HEIGHT + ")")
d3.select("#rectClip rect")
.transition().duration(6000)
.attr("transform", "translate(0," + 0 + ")")
The other answer is okay but this doesn't animate the graph.
Here is how I would do it.
I would add an animation tween to the path so it tweens from 0 to the point on the path.
Something like so :
//create an array of 0's the same size as your current array :
var startData = areaData.map(function(datum) {
return 0;
});
//use this and tween between startData and data
var path = vis.append("path")
.datum(startdata1)
.attr("fill", 'lightgrey')
.attr("d", area)
.transition()
.duration(1500)
.attrTween('d', function() {
var interpolator = d3.interpolateArray(startData, areaData );
return function(t) {
return area(interpolator(t));
}
});
The reason why yours wasn't working was because of this line :
.x(function(d) {
return xRange(areaData.indexOf(d));
})
d at this point is a value between 0 and the current piece of data, so areaData.indexOf(d) will not work.
Just change this :
.x(function(d,i) {
return xRange(i);
})
This will increment along the x axis :)
Updated fiddle : https://jsfiddle.net/thatOneGuy/o7df3tyn/17/

Resources