Troubles layering map tiles on top of each other with React, D3v5, openstreetmaps? - reactjs

I'm trying to have multiple map layers of maps on top of each other using react, d3, and openstreetmaps. I originally got started with React and the react-simple-maps library a few months ago, and so far I've been able to replicate this setup of getting a topojson file into d3 to generate the map, which is much more customize-able. The shapefiles you can download give you lines around the countries, or the states or counties or even US zip codes...pretty cool!
An open source (hint: free!) way to get a more granular, street-level view, along with other features like rivers, lakes etc would be a nice add-on to the maps I am building for my data visualizations. So far, I have the d3 map in place, but really confused on the openstreetmap, which for some reason insists on being positioned at the bottom right corner of the page, and not direclty overtop of the d3 map? I know there are lot of other libraries, including the not-so-free resources offered by google and mapbox and the other big players in the map game, but I'm close to getting this working...any ideas where I'm going wrong? I think it has something to do with grouping the svg maps properly but I'm not sure? Here's some code...any ideas are appreciated! A quick note is that these topojson files are large and can be slow to load, so I use axios to fetch them from a container component higher up in the tree to pass them down as props. Here's an example of that, where you have a nicely positioned map and a stubborn openstreetmap that lives below it. Some of it is removed for brevity, my apologies if it doesn't run?
EDIT: sorry for my WALL of code, after two weeks of no replies, here's a codepen that shows just the openstreetmap as it's own component:
https://stackblitz.com/edit/react-ktjlxk?embed=1&file=index.js
import React, { Component } from "react";
import "./worldMapOSM.css";
import * as d3 from "d3";
import * as d3Tile from "d3-tile";
import { geoMercator, geoPath } from "d3-geo";
//styles for map projection
const width = 960;
const height = 500;
class WorldMapOSM extends Component {
projection() {
return geoMercator() //geoConicConformal
.scale(120)
.center([6.8682168, 52.3401469])
.translate([width / 2, height / 2])
.clipExtent([[0, 0], [width, height]]) //????
.precision(0.5);
}
componentDidMount() {
const width = Math.max(960, window.innerWidth),
height = Math.max(500, window.innerHeight),
prefix = prefixMatch(["webkit", "ms", "Moz", "O"]);
const svg = d3.select(this.refs.anchor);
const map = d3
.select("body")
.append("div")
.attr("class", "map")
.style("width", width + "px")
.style("height", height + "px")
const layer = map.append("div").attr("class", "layer");
const tile = d3Tile
.tile()
.size([width, height])
.scale(this.projection().scale() * 2 * Math.PI)
.translate(this.projection([0, 0]))
.zoomDelta((window.devicePixelRatio || 1) - 0.5);
const tiles = d3Tile.tile();
const image = layer
.style(prefix + "transform", matrix3d(tiles.scale, tiles.translate))
.selectAll(".tile")
.data(tiles, function(d) {
return d;
})
image.exit().remove();
image
.enter()
.append("img")
.attr("class", "tile")
.attr("src", function(d) {
return (
"http://" +
"abc"[d.y % 3] +
".tile.openstreetmap.org/" +
d.z +
"/" +
d.x +
"/" +
d.y +
".png"
);
})
.style("left", function(d) {
return (d[0] << 8) + "px";
})
.style("top", function(d) {
return (d[1] << 8) + "px";
});
svg
.append("use")
.attr("xlink:href", "#land")
.attr("class", "stroke");
formatLocation(this.projection.invert(d3.mouse(this)), zoom.scale())
function matrix3d(scale, translate) {
var k = scale / 256,
r = scale % 1 ? Number : Math.round;
return (
"matrix3d(" +
[
k,0,0,0,0,k,0,0,0,0,k,0,
r(translate[0] * scale),
r(translate[1] * scale),
0,
1
] +
")"
);
}
function prefixMatch(p) {
var i = -1,
n = p.length,
s = document.body.style;
while (++i < n)
if (p[i] + "Transform" in s) return "-" + p[i].toLowerCase() + "-";
return "";
}
function formatLocation(p, k) {
var format = d3.format("." + Math.floor(Math.log(k) / 2 - 2) + "f");
return (
(p[1] < 0 ? format(-p[1]) + "°S" : format(p[1]) + "°N") +
" " +
(p[0] < 0 ? format(-p[0]) + "°W" : format(p[0]) + "°E")
);
}
}
}
render() {
return (
<div className="worldMap dropShadow">
<svg width={960} height={600} viewBox="0 0 960 600">
<g id="map">
{/* D3 map */}
<g className="eurCountries">
{this.props.Countries.map((d, i) => (
<path
key={`path-${i}`}
d={geoPath().projection(this.projection())(d)}
className="state"
fill={`rgba(187,218,247,${(1 / 1000) * i})`}
stroke="#afafaf"
strokeWidth={0.5}
/>
))}
</g>
<g ref="anchor" />{/* openstreetmap */}
</g>
</svg>
</div>
);
}
}
export default WorldMapOSM;

Related

D3.csv not loading data from local csv file

I created a copy of the csv file in my local folder because i wanted to mess around with the data a little bit. When i get rid of the link and replace it with the name of my local csv file, the graph doesnt render for some reason. I have added a picture that shows my local file structure. it is in the same folder. What am i doing wrong?
import React, {Component, useRef, useEffect} from 'react';
import * as d3 from "d3";
import { select } from 'd3-selection'
import { extent, max, min } from "d3-array";
class Linechart extends Component {
constructor(props){
super(props)
this.createBarChart = this.createBarChart.bind(this)
}
componentDidMount() {
this.createBarChart()
}
componentDidUpdate() {
this.createBarChart()
}
createBarChart() {
var margin = {top: 30, right: 30, bottom: 30, left: 60},
width = 960 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var node = this.node
var divObj = 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
// when i replace the line below wit this line of code, it doesnt read it d3.csv("5_OneCatSevNumOrdered.csv""), function(data) {
d3.csv("https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/5_OneCatSevNumOrdered.csv", function(data) {
// group the data: I want to draw one line per group
var sumstat = d3.nest() // nest function allows to group the calculation per level of a factor
.key(function(d) { return d.name;})
.entries(data);
// Define the div for the tooltip
var tooltip = divObj
.append("div")
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden")
.text("I AM A TOOLTIP BOOM SHAKALAKA!! BOOM SHAKALAKA!!");
// Add X axis --> it is a date format
var x = d3.scaleLinear()
.domain(d3.extent(data, function(d) { return d.year; }))
.range([ 0, width ]);
svgObj.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("stroke-width","0.3")
.call(d3.axisBottom(x).tickSize(-height).tickFormat('').ticks(5));
//ticks
svgObj.append("g")
.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.n; })])
.range([ height, 0 ]);
svgObj.append("g")
.attr("stroke-width","0.3")
.call(d3.axisLeft(y).tickSize(-width).tickFormat('').ticks(5));
//ticks
svgObj.append("g")
.call(d3.axisLeft(y).ticks(5));
// color palette
var res = sumstat.map(function(d){ return d.key }) // list of group names
console.log(res)
var color = d3.scaleOrdinal()
.domain(res)
.range(['#e41a1c','#377eb8','#4daf4a','#984ea3','#ff7f00','#ffff33','#a65628','#f781bf','#999999'])
// Draw the line
svgObj.selectAll(".line")
.data(sumstat)
.enter()
.append("path")
.attr("fill", "none")
.attr("stroke", function(d){ return color(d.key) })
.attr("stroke-width", 2.5)
.attr("d", function(d){
return d3.line()
.x(function(d) { return x(d.year); })
.y(function(d) { return y(+d.n); })
(d.values)
})
.on("mouseover", function(){return tooltip.style("visibility", "visible");})
.on("mousemove", function(){return tooltip.style("top", (d3.event.pageY-10)+"px").style("left",(d3.event.pageX+10)+"px");})
.on("mouseout", function(){return tooltip.style("visibility", "hidden");})
})
}
render() {
return <div ref={node => this.node = node} className="example_div"> </div>
}
}
export default Linechart;
d3.csv is part of the d3-fetch library. That library uses the native fetch method to obtain a file from somewhere on the internet. It's unfortunately not able to deal with files on your hard drive.
You'll need to make the file available on localhost:<some port>, just like you're probably doing with your react code. Depending on what you use, you might need to change your webpack/gulp/rollup configuration. Otherwise, if you have a server-side API running with Python/C#/Ruby just serve it from there as a static file.
In any case, the file name/directory will never work, try serving it through localhost.
Edit: serving the file you put on Github
d3.csv("https://raw.githubusercontent.com/QamarFarooq/data-for-testing/main/5_OneCatSevNumOrdered.csv").then((data) => {
console.log(data);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

D3 Bar Chart + React Hooks - exit().remove() not working

I'm using react and d3, trying to create a simple bar chart that updates the chart when data is refreshed. The chart is updating when the data changes, but it seems to be layering on top of the old chart. I think the issue is with the d3 exit().remove() function.
As I understand it d3's exit method should return an array of items to be removed, however when I console log it I see an array of "undefined"s. I'm super grateful for any help!
Here is the codesandbox:
https://codesandbox.io/s/gifted-field-n66hw?file=/src/Barchart.js
Here is the code snippet:
import React, { useEffect, useRef } from "react";
import * as d3 from "d3";
const BarChart = props => {
const { randomData, width, height, padding } = props;
const ref = useRef(null);
function colorGradient(v) {
return "rgb(0, " + v * 5 + ", 0";
}
//insert & remove elements using D3
useEffect(() => {
if (randomData.length > 0 && ref.current) {
const group = d3.select(ref.current);
// the data operator binds data items with DOM elements
// the resulting selection contains the enter and exit subselections
const update = group
.append("g")
.selectAll("rect")
.data(randomData);
let bars = update
.enter() // create new dom elements for added data items
.append("rect")
.merge(update)
.attr("x", (d, i) => i * (width / randomData.length))
.attr("y", d => height - d * 5)
.attr("width", width / randomData.length - padding)
.attr("height", d => d * 5)
.attr("fill", d => colorGradient(d));
let labels = update
.enter()
.append("text")
.text(d => d)
.attr("text-anchor", "middle")
.attr(
"x",
(d, i) =>
i * (width / randomData.length) +
(width / randomData.length - padding) / 2
)
.attr("y", d => height - d * 5 + 12)
.style("font-family", "sans-serif")
.style("font-size", 12)
.style("fill", "#ffffff");
update.exit().remove();
}
}, [randomData, height, padding, width]);
return (
<svg width={width} height={height}>
<g ref={ref} />
</svg>
);
};
export default BarChart;
Everytime you update the chart you run this:
const update = group
.append("g") // create a new g
.selectAll("rect") // select all the rectangles in that g (which are none)
.data(randomData);
update is now an empty selection, there are no rects to select in the newly created g. So when we use update.enter(), a DOM element is created for every item in the data array. Using enter will create an element for every item in the data array that doesn't have a corresponding element already.
update.exit() will be empty, because there are no elements selected in update, so nothing will be removed. Previously created bars are not touched, you aren't selecting them.
If we change your code just to remove the .append("g"), it gets us closer to working (eg). The bars were colored white, so they were not visible, I've changed the fill color so the update selection is visible
If we remove .append("g") we have some other problems on update now:
you are not exiting text (as you are not selecting text with .selectAll(), only rect elements), and
you're merging text and rectangles into one selection, which is a bit problematic if you want to position and color them differently on update.
The second problem could be explained a bit more:
update.enter().append("text") // returns a selection of newly created text elements
.merge(update) // merges the selection of newly created text with existing rectangles
.attr("fill", .... // affects both text and rects.
These two issues can be resolved by using the enter/update/exit cycle correctly.
One thing to note is that D3's enter update exit pattern isn't designed to enter elements more than once with the same statement, you're entering text and rects with the same enter statement, see here.
Therefore, one option is to use two selections, one for text and one for rects:
const updateRect = group
.selectAll("rect")
.data(randomData);
let bars = updateRect
.enter() // create new dom elements for added data items
.append("rect")
.merge(updateRect)
.attr("x", (d, i) => i * (width / randomData.length))
.attr("y", d => height - d * 5)
.attr("width", width / randomData.length - padding)
.attr("height", d => d * 5)
.attr("fill", d => colorGradient(d));
const updateText = group
.selectAll("text")
.data(randomData);
let labels = updateText
.enter()
.append("text")
.merge(updateText)
.text(d => d)
.attr("text-anchor", "middle")
.attr(
"x",
(d, i) =>
i * (width / randomData.length) +
(width / randomData.length - padding) / 2
)
.attr("y", d => height - d * 5 + 12)
.style("font-family", "sans-serif")
.style("font-size", 12)
.style("fill", "#fff");
updateRect.exit().remove();
updateText.exit().remove();
Here in sandbox form.
The other option is to use a parent g to hold both rect and text, this could be done many ways, but if you don't need a transition between values or the number of bars, would probably be most simple like:
const update = group
.selectAll("g")
.data(randomData);
// add a g for every extra datum
const enter = update.enter().append("g")
// give them a rect and text element:
enter.append("rect");
enter.append("text");
// merge update and enter:
const bars = update.merge(enter);
// modify the rects
bars.select("rect")
.attr("x", (d, i) => i * (width / randomData.length))
.attr("y", d => height - d * 5)
.attr("width", width / randomData.length - padding)
.attr("height", d => d * 5)
.attr("fill", d => { return colorGradient(d)});
// modify the texts:
bars.select("text")
.text(d => d)
.attr("text-anchor", "middle")
.attr(
"x",
(d, i) =>
i * (width / randomData.length) +
(width / randomData.length - padding) / 2
)
.attr("y", d => height - d * 5 + 12)
.style("font-family", "sans-serif")
.style("font-size", 12)
.style("fill", "#ffffff");
Here's that in sandox form.
A bit further explanation: selection.select() selects the first matching element for each element in the selection - so we can select the sole rectangle in each parent g (that we add when entering the parent) with bars.select("rect") above. D3 passes the parent datum to the child when appending in the above. Note: If we had nested data (multiple bars or texts per data array item) we'd need to have nested enter/exit/update cycles.

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');
}}
/>

Combining react-leaflet and leaflet-pixi-overlay

I am trying to display a complex map with many moving markers, with different icons etc... I have a pure react / react-leaflet solution, but it start to struggle with ~1k markers moving every second.
It looks like leaflet-pixi-overlay could be a way of really speeding things up. But I am just starting with the whole chain (react/javascript/maps/leaflet/etc..) and have problems when connecting all this.
Right now I am just trying to display a polygon in my overlay, and nothing is rendered. It turns out that the translation of lat/long to x/y is failing for that polygon's points. Pixi's latLngToLayerPoint function returns 'infinity' for x and y.
This seems to come from the fact that the zoom is not defined for the layer in question (it is 'infinity' also).
Even if I call latLngToLayerPoint with the zoom setting from the map, things fail too (x/y values are not infinite any more, but they are way out there).
This is my code:
import React, { useEffect, useRef } from 'react';
import { Map, TileLayer } from 'react-leaflet'
import "leaflet/dist/leaflet.css"
import * as PIXI from 'pixi.js'
import 'leaflet-pixi-overlay' // Must be called before the 'leaflet' import
import L from 'leaflet';
let polyLatLngs = [[50.630, 13.047], [50.645, 13.070], [50.625, 13.090], [50.608, 13.070], [50.630, 13.047]]
let pixiContainer = new PIXI.Container()
let prevZoom
let firstDraw = true;
let projectedPolygon;
var shape = new PIXI.Graphics();
pixiContainer.addChild(shape);
let myOverlay = L.pixiOverlay(function (utils) {
let map = utils.getMap()
let zoom = map.getZoom()
console.log('map zoom=' + zoom + ', center=' + map.getCenter())
console.log(' bounds=' + JSON.stringify(map.getBounds()))
console.log(' size=' + map.getSize() + ', panes=' + JSON.stringify(map.getPanes()))
if (map) {
var container = utils.getContainer();
var renderer = utils.getRenderer();
var project = utils.latLngToLayerPoint;
var scale = utils.getScale();
if (firstDraw) {
projectedPolygon = polyLatLngs.map((coords, index) => {
console.log('i=' + index + ', coords=' + coords + ', proj=' + project(coords))
return project(coords)
// return project(coords, zoom) // : this fails too
})
}
if (firstDraw || prevZoom !== zoom) {
shape.clear();
shape.lineStyle(3 / scale, 0x3388ff, 1);
shape.beginFill(0x3388ff, 0.2);
projectedPolygon.forEach(function (coords, index) {
if (index === 0) shape.moveTo(coords.x, coords.y);
else shape.lineTo(coords.x, coords.y);
});
shape.endFill();
}
firstDraw = false;
prevZoom = zoom;
renderer.render(container);
}
}, pixiContainer)
function PxMap(props) {
const mapRef = useRef(null);
useEffect(() => {
if (mapRef.current !== null) {
let map = mapRef.current.leafletElement;
console.log('useEffect: add overlay ')
console.log(JSON.stringify(map.getPane('overlayPane').childElementCount))
myOverlay.addTo(map);
console.log(JSON.stringify(map.getPane('overlayPane').childElementCount))
}
}, [mapRef]);
return (
<div style={{ flexgrow: 1, height: '100%' }}>
<Map
preferCanvas={true}
ref={mapRef}
style={{ height: '100%' }}
center={[50.63, 13.047]}
zoom={12}
>
<TileLayer
attribution='&copy OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png?"
/>
</Map>
</div>
)
}
export default PxMap
I think that things are connected correctly between React and leaflet, the map displays ok, I can see the overlay being added etc...
BUT there is a connection missing somewhere, to give more context / information to PIXI.
Any ideas? Thanks!
Finally found the problem, drilling down into the leaflet-pixi-overlay lib.
The solution is to define the minZoom and maxZoom in the Map element:
<Map
preferCanvas={true}
ref={mapRef}
style={{ height: '100%' }}
center={[50.63, 13.047]}
zoom={12}
minZoom={ 9} // Add these options...
maxZoom={ 16} //
Internally, L.PixiOverlay.js relies on these two values to define:
// return the layer projection zoom level
projectionZoom: function (map) {return (map.getMaxZoom() + map.getMinZoom()) / 2;},
Which in turn is used to define the default zoom setting?
this._initialZoom = this.options.projectionZoom(map);
....
zoom = (zoom === undefined) ? _layer._initialZoom : zoom;
var projectedPoint = map.project(L.latLng(latLng), zoom);
Not sure why this is done this way, but setting these options solves the problem.

Trying to place two d3 line graphs inside one Ionic2 page

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")...

Resources