since two days, I'm trying to move a line in simple 10x10 Raster, using the mouse.
I solved this without any issues, with complex svg-"Symbols" but the simplest Elements bring me to my borders…
Ok, simple line with a g-tag:
var svg = d3.select("#drawing");
const graph = svg.append("g")
.attr("id","1")
.attr("ponter-events","fill")
.call(d3.drag()
.on("start", dragGraphicStart)
.on("drag", dragGraphic)
.on("end", dragGraphicStop));
graph.html("<line class='coldbrinewater graphic' x1='10' y1='10' x2='50' y2='50' />");
Now of Course, before the first drag, this line doesn't have a Transformation, so I check this before reading a Position:
var xPos, yPos;
function dragGraphicStart() {
var trans = d3.select(this).attr("transform"); //Prüfen ob bereits ein Transform existiert
if (trans == null || trans == "" || trans == "translate(0)") { //kein Transform vorhanden
xPos = 0;
yPos = 0;
}
else { //Transform gefunden, startwerte ermitteln
trans = trans.replace(",", " "); //I don't know why, sometimes I got a comma between x and y-value
trans = trans.substring(trans.indexOf("(") + 1, trans.indexOf(")")).split(" ");
xPos = parseInt( trans[0]);
yPos = parseInt( trans[1] );
}
}
And then I try to move the line:
function dragGraphic(d, i) {
xPos += Math.round(d3.event.dx / raster) * raster;
//xPos += d3.event.dx;
yPos += Math.round(d3.event.dy / raster) * raster;
//yPos += d3.event.dy;
d3.select(this).attr("transform", "translate(" + xPos + " " + yPos + ")");
}
with the commented lines xPos += d3.event.dx (and y) it works fine but not if I like to calculate the raster before.
I have no idea why, but then I can see in the console, that translate-attribute often only has one Parameter or is empty. like this:
transform"translate(30)" or transform=""
I built a (non) working example here:
https://jsfiddle.net/Telefisch/3oecj6wd/
Thank you
Related
I'm trying to implement a tooltip on mouseover for a multi line chart.
I've followed the code from this example and tried to change it so that I see the X values of the lines for a given hovered Y value, but I'm not able to get it to work.
My attempt can be found below.
In my actual implementation I'm writing in Typescript and the functions 'getTotalLength()' and 'getPointAtLength()' are saying they don't exist on property Element.
Also if you can add a text box at on the line that has the hovered Y value that'd help me a lot!
https://codesandbox.io/s/modest-minsky-hvsms?fontsize=14&hidenavigation=1&theme=dark
Thanks
So after careful review there were several errors which I have corrected.
Your paths for the data lines were not assigned the class so you need to assign the class of dataLine to them when you append them like so:
svg
.selectAll(".dataLine")
.data(nestedData)
.enter()
.append("path")
.attr("fill", "none")
.attr("class", "dataLine")
.attr("stroke", d => itemMap(d.key).color)
.attr("stroke-width", d => itemMap(d.key).lineWeight)
.attr("d", d =>
d3
.line()
.x(d => x(d.xvalue))
.y(d => y(d.yvalue))(d.values)
);
As pointed out in the comment above, stop using arrow functions if you intend to use this. Once you do that, your d3.mouse(this) starts working.
The example you followed had the paths from left to right, while yours is from top to bottom. This required several changes in terms of coordinates to get the alignment of the mouseover line and the circles with the text values near them to align properly. The correct code is as follows:
.on("mousemove", function() {
//#ts-ignore
var mouse = d3.mouse(this);
d3.select(".mouse-line").attr("d", () => {
var d = "M" + plotWidth + "," + mouse[1];
d += " " + 0 + "," + mouse[1];
return d;
});
d3.selectAll(".mouse-per-line").attr("transform", function(d, i) {
var yDepth = y.invert(mouse[1]);
var bisect = d3.bisector(d => d.depth).right;
var idy = bisect(d.values, yDepth);
var beginning = 0;
var end = lines[i].getTotalLength();
var target = null;
while (true) {
target = Math.floor((beginning + end) / 2);
var pos = lines[i].getPointAtLength(target);
if (
(target === end || target === beginning) &&
pos.y !== mouse[1]
) {
break;
}
if (pos.y > mouse[1]) {
end = target;
} else if (pos.y < mouse[1]) {
beginning = target;
} else {
break;
}
}
d3.select(this)
.select("text")
.text(x.invert(pos.x).toFixed(2));
return "translate(" + pos.x + "," + mouse[1] + ")";
});
});
Fully working codesandbox here.
I am working on horizontal segment bar chart. I want to make it so that the bar chart will animate the colour transition between individual segments depending on the value that is generated randomly every few seconds.
I also have a text box that at the moment says "hello" and it is moving simultaneously with the transition.It works only in one direction from left to right.
I cannot make the colour transition between segments and translating the text box from left to right. From left to right I mean that the number generated last time is greater than the currently generated number. The bar chart should turn off the segments from right to left. But it does it from left to right also the text box is acting weirdly when it comes to reverse translation.
I am also getting this error: g attribute transform: Expected transform function, "null". In my code I want to make a transition on my valueLabel and I think the way I am using is not correct. Despite this error the code gets executed.
My fiddle code is here
Many thanks for suggestions
var configObject = {
svgWidth : 1000,
svgHeight : 1000,
minValue : 1,
maxValue : 100,
midRange : 50,
highRange : 75,
numberOfSegments : 50
};
//define variables
var newValue;
var gaugeValue = configObject.minValue - 1;
var mySegmentMappingScale;
var rectArray=[];
//define svg
var svg = d3.select("body").append("svg")
.attr("width", configObject.svgWidth)
.attr("height", configObject.svgHeight)
.append("g")
.attr("transform", 'translate(' + configObject.svgWidth/2 + ',' + configObject.svgHeight/2 + ')');
//var myG=svg.append('g');
var valueLabel= svg.append("text")
.attr('x',0)
.attr('y', (configObject.svgHeight/13)+15)
.text("hello");
var backgroundRect=svg.append("rect")
.attr("fill", "black")
.attr("x",0)
.attr("y", 0)
.attr("width", (configObject.svgWidth/3))
.attr("height", configObject.svgHeight/13);
for(i = 0; i <= configObject.numberOfSegments; i++){
var myRect=svg.append("rect")
.attr("fill", "#2D2D2D")
.attr("x",i * ((configObject.svgWidth/3)/configObject.numberOfSegments))
.attr("y", 0)
.attr("id","rect"+i)
.attr("width", ((configObject.svgWidth/3)/configObject.numberOfSegments)-3)
.attr("height", configObject.svgHeight/13);
rectArray.push(myRect);
}
//define scale
function setmySegmentMappingScale(){
var domainArray = [];
var x=0;
for(i = configObject.minValue; i <= configObject.maxValue+1; i = i + (configObject.maxValue - configObject.minValue)/configObject.numberOfSegments){
if(Math.floor(i) != domainArray[x-1]){
var temp=Math.floor(i);
domainArray.push(Math.floor(i));
x++;
}
}
var rangeArray = [];
for(i = 0; i <= configObject.numberOfSegments+1; i++){// <=
rangeArray.push(i);
}
mySegmentMappingScale = d3.scale.threshold().domain(domainArray).range(rangeArray);
}
//generate random number
function generate(){
var randomNumber = Math.random() * (configObject.maxValue - configObject.minValue) + configObject.minValue;
newValue = Math.floor(randomNumber);
setmySegmentMappingScale();
animateSVG();
}
function animateSVG(){
var previousSegment = mySegmentMappingScale(gaugeValue) -1;
var newSegment = mySegmentMappingScale(newValue) -1;
if(previousSegment <= -1 && newSegment > -1){
for(i = 0; i <= newSegment; i++){
rectArray[i].transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.styleTween("fill", function() { return d3.interpolate( "#2D2D2D","red"); });
valueLabel
.transition().ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.attr("transform","translate(" + (i * ((configObject.svgWidth/3)/configObject.numberOfSegments)+((configObject.svgWidth/3)/configObject.numberOfSegments)) + "," + 0 + ")")
}
}
else if(newSegment > previousSegment){
for(i = previousSegment; i <= newSegment; i++){
rectArray[i].transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.styleTween("fill", function() { return d3.interpolate( "#2D2D2D","red"); });
//console.log(temp);
valueLabel
.transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.attr("transform","translate(" + (i * ((configObject.svgWidth/3)/configObject.numberOfSegments)+((configObject.svgWidth/3)/configObject.numberOfSegments)) + "," + 0 + ")")
}
}
else if(newSegment < previousSegment){
for(i = previousSegment; i > newSegment; i--){
rectArray[i].transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.styleTween("fill", function() { return d3.interpolate( "red","#2D2D2D"); });
valueLabel
.transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.attr("transform","translate(" + (i * ((configObject.svgWidth/3)/configObject.numberOfSegments)-((configObject.svgWidth/3)/configObject.numberOfSegments)) + "," + 0 + ")")
}
}
gaugeValue = newValue;
}
setInterval(function() {
generate()
}, 8000);
The problem is just the delay.
When newSegment > previousSegment, you set the delay like this:
.delay(function(d){return i * 90})
Which makes sense, because i is an increasing variable. But, when newSegment < previousSegment the same math doesn't work anymore: i is a decreasing variable, and the delay has to increase as i decreases, not the other way around.
This is what you need:
.delay(function(d){return Math.abs(i -previousSegment)*90})
Here is your updated fiddle: https://jsfiddle.net/b7usw2nr/
I'm quite new to D3 and been working through trying to figure everything out. I'm trying to configure this example here to update with new data and transition appropriately.
Here is the code pen I have configured (click the submit to update)
http://codepen.io/anon/pen/pbjLRW?editors=1010
From what I can gather, using some variation of .exit() is required for a clean data transition, but after reading some tutorials I'm still finding it difficult to know how it works. I have seen examples where simply removing the containers before calling the draw function works, but in my limited experience it can cause a flicker when changing data so I'm not sure if it's best practice?
Now, I'm not sure why the data is not updating correctly in my codepen, but my main concern is trying to get the transition right. Ideally I would like to know how I could just move the needle when changing data, so It would go from 90 > 40 for example, instead of 90 > 0 > 40.
However, I will definitely settle for figuring out why it doesn't redraw itself in the same location once clicking submit in the linked codepen.
Here is my update function;
function updateGuage() {
d3.selectAll("text").remove()
d3.selectAll('.needle').remove()
chart.remove()
name = "qwerty";
value = "25";
drawGuage();
}
initial draw;
function drawGuage() {
percToDeg = function(perc) {
return perc * 360;
};
percToRad = function(perc) {
return degToRad(percToDeg(perc));
};
degToRad = function(deg) {
return deg * Math.PI / 180;
};
// Create SVG element
svg = el.append('svg').attr('width', width + margin.left + margin.right).attr('height', height + margin.top + margin.bottom);
// Add layer for the panel
chart = svg.append('g').attr('transform', "translate(" + ((width + margin.left) / 2) + ", " + ((height + margin.top) / 2) + ")");
chart.append('path').attr('class', "arc chart-first");
chart.append('path').attr('class', "arc chart-second");
chart.append('path').attr('class', "arc chart-third");
arc3 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
arc2 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
arc1 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
repaintGauge = function() {
perc = 0.5;
var next_start = totalPercent;
arcStartRad = percToRad(next_start);
arcEndRad = arcStartRad + percToRad(perc / 3);
next_start += perc / 3;
arc1.startAngle(arcStartRad).endAngle(arcEndRad);
arcStartRad = percToRad(next_start);
arcEndRad = arcStartRad + percToRad(perc / 3);
next_start += perc / 3;
arc2.startAngle(arcStartRad + padRad).endAngle(arcEndRad);
arcStartRad = percToRad(next_start);
arcEndRad = arcStartRad + percToRad(perc / 3);
arc3.startAngle(arcStartRad + padRad).endAngle(arcEndRad);
chart.select(".chart-first").attr('d', arc1);
chart.select(".chart-second").attr('d', arc2);
chart.select(".chart-third").attr('d', arc3);
}
/////////
var texts = svg.selectAll("text")
.data(dataset)
.enter();
texts.append("text")
.text(function() {
return dataset[0].metric;
})
.attr('id', "Name")
.attr('transform', "translate(" + ((width + margin.left) / 6) + ", " + ((height + margin.top) / 1.5) + ")")
.attr("font-size", 25)
.style("fill", "#000000");
var trX = 180 - 210 * Math.cos(percToRad(percent / 2));
var trY = 195 - 210 * Math.sin(percToRad(percent / 2));
// (180, 195) are the coordinates of the center of the gauge.
displayValue = function() {
texts.append("text")
.text(function() {
return dataset[0].value;
})
.attr('id', "Value")
.attr('transform', "translate(" + trX + ", " + trY + ")")
.attr("font-size", 18)
.style("fill", '#000000');
}
texts.append("text")
.text(function() {
return 0;
})
.attr('id', 'scale0')
.attr('transform', "translate(" + ((width + margin.left) / 100) + ", " + ((height + margin.top) / 2) + ")")
.attr("font-size", 15)
.style("fill", "#000000");
texts.append("text")
.text(function() {
return gaugeMaxValue / 2;
})
.attr('id', 'scale10')
.attr('transform', "translate(" + ((width + margin.left) / 2.15) + ", " + ((height + margin.top) / 30) + ")")
.attr("font-size", 15)
.style("fill", "#000000");
texts.append("text")
.text(function() {
return gaugeMaxValue;
})
.attr('id', 'scale20')
.attr('transform', "translate(" + ((width + margin.left) / 1.03) + ", " + ((height + margin.top) / 2) + ")")
.attr("font-size", 15)
.style("fill", "#000000");
var Needle = (function() {
//Helper function that returns the `d` value for moving the needle
var recalcPointerPos = function(perc) {
var centerX, centerY, leftX, leftY, rightX, rightY, thetaRad, topX, topY;
thetaRad = percToRad(perc / 2);
centerX = 0;
centerY = 0;
topX = centerX - this.len * Math.cos(thetaRad);
topY = centerY - this.len * Math.sin(thetaRad);
leftX = centerX - this.radius * Math.cos(thetaRad - Math.PI / 2);
leftY = centerY - this.radius * Math.sin(thetaRad - Math.PI / 2);
rightX = centerX - this.radius * Math.cos(thetaRad + Math.PI / 2);
rightY = centerY - this.radius * Math.sin(thetaRad + Math.PI / 2);
return "M " + leftX + " " + leftY + " L " + topX + " " + topY + " L " + rightX + " " + rightY;
};
function Needle(el) {
this.el = el;
this.len = width / 2.5;
this.radius = this.len / 8;
}
Needle.prototype.render = function() {
this.el.append('circle').attr('class', 'needle-center').attr('cx', 0).attr('cy', 0).attr('r', this.radius);
return this.el.append('path').attr('class', 'needle').attr('id', 'client-needle').attr('d', recalcPointerPos.call(this, 0));
};
Needle.prototype.moveTo = function(perc) {
var self,
oldValue = this.perc || 0;
this.perc = perc;
self = this;
// Reset pointer position
this.el.transition().delay(100).ease('quad').duration(200).select('.needle').tween('reset-progress', function() {
return function(percentOfPercent) {
var progress = (1 - percentOfPercent) * oldValue;
repaintGauge(progress);
return d3.select(this).attr('d', recalcPointerPos.call(self, progress));
};
});
this.el.transition().delay(300).ease('bounce').duration(1500).select('.needle').tween('progress', function() {
return function(percentOfPercent) {
var progress = percentOfPercent * perc;
repaintGauge(progress);
return d3.select(this).attr('d', recalcPointerPos.call(self, progress));
};
});
};
return Needle;
})();
needle = new Needle(chart);
needle.render();
needle.moveTo(percent);
setTimeout(displayValue, 1350);
}
Any help/advice is much appreciated,
Thanks
What you wanna check out is How selections work written by Mike Bostock. After reading this article, everything around enter, update and exit selections will become clearer.
In a nutshell:
You create a selection of elements with selectAll('li')
You join the selection with a data array through calling data([...])
Now D3 compares what's already in the DOM with the joined data. Each DOM element processed this way has a __data__ property that allows D3 to bind a data item to an element.
After you've joined data, you receive the enter selection by calling enter(). This is every data element that has not yet been bound to the selected DOM elements. Typically you use the enter selection to create new elements, e.g. through append()
By calling exit() you receive the exit selection. These are all already existing DOM elements which no longer have an associated data item after the join. Typically you use the exit selection to remove DOM elements with remove()
The so called update selection is the one thing that's been returned after joining the selection with data(). You will want to store the update selection in a variable, so you have access to it even after calling enter() or exit().
Note the difference between d3v3 and d3v4:
In d3v3, when you've already added elements via the enter selection, the update selection includes those newly created DOM elements as well. It's crucial to know that the update selection changes after you've created new elements.
However, this is no longer true when using d3v4. The change log says
"In addition, selection.append no longer merges entering nodes into the update selection; use selection.merge to combine enter and update after a data join."
It is important to know that there are three different operations you can perform after binding data. Handle additions, deletions and modify things that did not change (or have been added just before).
Here is an example creating and manipulating a simple list: http://jsbin.com/sekuhamico/edit?html,css,js,output
var update = () => {
// bind data to list elements
// think of listWithData as a virtual representation of
// the array of list items you will later see in the
// DOM. d3.js does not handle the mapping from this
// virtual structure to the DOM for you. It is your task
// to define what is to happen with elements that are
// added, removed or updated.
var listWithData = ul.selectAll('li').data(listItems);
// handle additions
// by calling enter() on our virtual list, you get the
// subset of entries which need to be added to the DOM
// as their are not yet present there.
listWithData.enter().append('li').text(i => i.text).on('click', i => toggle(i));
// handle removal
// by calling exit() on our virtual list, you get the
// subset of entries which need to be removed from the
// DOM as they are not longer present in the virtual list.
listWithData.exit().remove();
// update existing
// acting directly on the virtual list will update any
// elements currently present in the DOM. If you would
// execute this line before calling exit(), you would
// also manipulate those items to be removed. If you
// would even call it before calling enter() you would
// miss on updating the newly added element.
listWithData.attr('class', i => i.active ? 'active' : '');
};
Be aware that in reality you probably need to add some sort of id to your items. To ensure the right items are removed and you do not get ordering issues.
Explanation
The update function knows nothing about what, or even if anything has changed. It does not know, nor care, if there are new data elements or if old ones have been removed. But both things could happen. Therefore we handle both cases by calling enter() and exit() respectively. The d3 functions enter() and exit() provides us with the subsets of list elements that should be added or removed. Finally we need to take care of changes in the existing data.
var listItems = [{ text: 1, active: false}, { text: 2, active: true}];
var ul = d3.select('#id').append('ul');
var update = () => {
var listWithData = ul.selectAll('li').data(listItems);
// add new
listWithData.enter().append('li').text(i => i.text).on('click', i => toggle(i));
// remove old
listWithData.exit().remove();
// update existing
listWithData.attr('class', i => i.active ? 'active' : '');
};
update();
$('#add').click(() => {
listItems.push({
text: listItems.length+1,
active: false
});
update();
});
var toggle = (i) => {
i.active = !i.active;
update();
};
li.active {
background-color:lightblue;
}
li {
padding: 5px;
}
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<div id="id"></div>
<button id="add">Add</button>
</body>
</html>
I have a radar that I can zoom into, this all works if I am scrolling in with the mouse or pinch zooming. However, when i double click on an arc within the radar it needs to zoom in to it.
i have removed the default double click with
.on('dblclick.zoom',null);
and I have a double click event on each arc that can be double clicked (so it is not going through the zoom listener). This double click handler calls the zoom listeners scale and translate functions and then the event. I have tried calling the scale then event and then the translate and then the event but I get the same result, nothing zooms
RadarDraw.ZoomListener.scale(6);
RadarDraw.ZoomListener.translate([0, 0]);
RadarDraw.ZoomListener.event(svg.transition().duration(500));
from what I have read this should work. Is there something I am missing here?
Thanks
Mark
my zoom handler function looks like this
function zoom(){
if (d3.event.scale <= 1 || d3.event.scale > 5) {
//on scale 1 (zoomed out) center the radar to its origianl position
//if (RadarDraw.ZoomListener.scale() <= 1 && previousZoom)
//{
// resetZoomOut();
// previousZoom = false;
//}
var scalesize;
if (d3.event.scale <= 1)
{
scalesize = 1;
}
if (d3.event.scale > 5)
{
scalesize = 0.2;
}
$(".isadot").each(function (i, v) {
var tmp = v;
var PrevTransform = tmp.attributes["transform"].nodeValue;
//copy translate only remove other scale
var currentTrans = PrevTransform.split(" ");
tmp.attributes["transform"].nodeValue = currentTrans[0] + " scale(" + scalesize + ")";
});
previousZoomLevel = d3.event.scale;
return;
} else {
// RadarDraw.ZoomListener.translate(d3.event.translate);
//allow panning not zoom between min and max
svg.attr("transform", "translate("
+ (d3.event.translate[0] + (_config.Width / 2)) + "," + (d3.event.translate[1] + (_config.Height / 2))
+ ")scale(" + d3.event.scale + ")");
if (d3.event.scale > 1)
previousZoom = true;
//if we are zoomed in and out previous zoom level is 5 or above then exit zoom (we are either scrolling out or pinching out of zoom)
if (tools.IsArcFocused && previousZoomLevel >= 5) {
tools.ExitZoom(false);
}
//scale the dots by the zom level
var scalesize = 0.6;
//switch (RadarDraw.ZoomListener.scale())
//{
// case ():
// break;
// default:
// scalesize = 0.6;
// break;
//}
if (d3.event.scale <= 1) {
scalesize = 1;
}
if (d3.event.scale >= 5) {
scalesize = 0.4;
}
$(".isadot").each(function (i, v) {
var tmp = v;
var PrevTransform = tmp.attributes["transform"].nodeValue;
//copy translate only remove other scale
var currentTrans = PrevTransform.split(" ");
tmp.attributes["transform"].nodeValue = currentTrans[0] + " scale(" + scalesize + ")";
});
}
}
EDIT
Rob was correct it ended up being .on('dblclick.zoom',null); that was causing the issue in ie 9, after commenting it out it acted as expected. Thanks for the back and forth Rob, got there in the end haha.
I am trying to modify this Google Maps + D3 example to include lines between points. The approach I took was to add new svg elements for each line.
I've managed to create a new set of svg elements:
var markerLink = layer.selectAll(".links")
.data(links)
.each(pathTransform) // update existing markers
.enter().append("line")
.each(pathTransform)
.attr("class", "links");
Where links is an array of arrays. Each inner array has 4 values corresponding to latitude and longitude of the beginning and end of each line: x1, y1, x2 and y2 of the d3 line.
The pathTransform function:
function pathTransform(d) {
var t, b, l, r, w, h;
var d1 = new google.maps.LatLng(d[1], d[0]);
var d2 = new google.maps.LatLng(d[3], d[2]);
d1 = projection.fromLatLngToDivPixel(d1);
d2 = projection.fromLatLngToDivPixel(d2);
if ( d1.y < d2.y ) {
t = d1.y - 25;
b = d2.y + 25;
} else {
t = d2.y - 25;
b = d1.y + 25;
}
if ( d1.x < d2.x ) {
l = d1.x - 25;
r = d2.x + 25;
} else {
l = d2.x - 25;
r = d1.x + 25;
}
return d3.select(this)
.style("left", l + "px")
.style("top", t + "px")
.style("width", (r - l) + "px")
.style("height", (b - t) + "px");
}
takes the latitude and longitude values and returns a properly sized and positioned svg element.
Following the example with the marker svg elements, I tried to append a line to the markerLink elements. This is where I run into trouble. I can't figure out how to position the lines within each svg.
Is this a good approach or is there another way to add circles, paths, lines and labels to google map overlays?