I would like to use the "zoomToMapObject" method based on a selection on a dropdown menu.
For some reason the start zoom location is the middle of the map and not the set the geoPoint.
(The zooming works but the start location make it look a bit weird.)
My current approach looks like this:
const duration = this.chart.zoomToMapObject(selectedPoloygon, this.countryZoom, true).duration;
setTimeout(() => {
this.chart.homeGeoPoint = geoPoint;
this.chart.homeZoomLevel = this.countryZoom;
}, duration);
this.handleCountrySelection(selectedPoloygon);
Somehow even setting the homeGeoPoint / homeZoomLevel doesn't affect next zoom actions.
**UPDATE: Workaround heavy cost (from 1300 nodes to over 9000) **
I examined the problem a step further. It seems the middle point gets set when I push a new mapImageSeries into the map.
My workarround currently is to draw all points on the map and hide them.
Then after I select a country I change the state to visible.
However this approach is very costly. The DOM-Nodes rises from 1300 to ~ 9100.
My other approach with creating them after a country has been selected AND the zoom animation finished was much more
effective. But due to the map starting every time for a center location it is not viable? Or did I do s.th. wrong?
Here is my current code which is not performant:
// map.ts
export class MapComponent implements AfterViewInit, OnDestroy {
imageSeriesMap = {};
// ... standard map initialization ( not in zone of course )
// creating the "MapImages" which is very costly
this.dataService.getCountries().forEach(country => {
const imageSeriesKey = country.id;
const imageSeriesVal = chart.series.push(new am4maps.MapImageSeries()); // takes arround 1-2 ms -> 300 x 2 ~ 500 ms.
const addressForCountry = this.dataService.filterAddressToCountry(country.id); // returns "DE" or "FR" for example.
const imageSeriesTemplate = imageSeriesVal.mapImages.template;
const circle = imageSeriesTemplate.createChild(am4core.Circle);
circle.radius = 4;
circle.fill = am4core.color(this.colorRed);
circle.stroke = am4core.color('#FFFFFF');
circle.strokeWidth = 2;
circle.nonScaling = true;
circle.tooltipText = '{title}';
imageSeriesTemplate.propertyFields.latitude = 'latitude';
imageSeriesTemplate.propertyFields.longitude = 'longitude';
imageSeriesVal.data = addressForCountry.map(address => {
return {
latitude: Number.parseFloat(address.lat),
longitude: Number.parseFloat(address.long),
title: address.company
};
});
imageSeriesVal.visible = false;
this.imageSeriesMap[imageSeriesKey] = imageSeriesVal;
});
// clicking on the map
onSelect(country) {
this.imageSeriesMap[country].visible = true;
setTimeout( () => {
const chartPolygons = <any>this.chart.series.values[0];
const polygon = chartPolygons.getPolygonById(country);
const anim = this.chart.zoomToMapObject(polygon, 1, true, 1000);
anim.events.on('animationended', () => {});
this.handleCountrySelection(polygon);
}, 100);
});
}
handleCountrySelection(polygon: am4maps.MapPolygon) {
if (this.selectedPolygon && this.selectedPolygon !== polygon) {
this.selectedPolygon.isActive = false;
}
polygon.isActive = true;
const geoPoint: IGeoPoint = {
latitude: polygon.latitude,
longitude: polygon.longitude
};
this.chart.homeGeoPoint = geoPoint;
this.chart.homeZoomLevel = this.countryZoom;
this.selectedPolygon = polygon;
}
}
Thanks to your thorough followup I was able to replicate the issue. The problem you were having is triggered by any one of these steps:
dynamically pushing a MapImageSeries to the chart
dynamically creating a MapImage via data (also please note in the pastebind you provided, data expects an array, I had to change that while testing)
In either step, the chart will fully zoom out as if resetting itself. I'm going to look into why this is happening and if it can be changed, so in the meantime let's see if the workaround below will work for you.
If we only use a single MapImageSeries set in advance (I don't particularly see a reason to have multiple MapImageSeries, would one not do?), that eliminates problem 1 from occurring. Asides from data, we can create() MapImages manually via mapImageSeries.mapImages.create(); then assign their latitude and longitude properties manually, too. With that, problem 2 does not occur either, and we seem to be good.
Here's a demo with a modified version of the pastebin:
https://codepen.io/team/amcharts/pen/c460241b0efe9c8f6ab1746f44d666af
The changes are that the MapImageSeries code is taken out of the createMarkers function so it only happens once:
const mapImageSeries = chart.series.push(new am4maps.MapImageSeries());
const imageSeriesTemplate = mapImageSeries.mapImages.template;
const circle = imageSeriesTemplate.createChild(am4core.Circle);
circle.radius = 10;
circle.fill = am4core.color('#ff0000');
circle.stroke = am4core.color('#FFFFFF');
circle.strokeWidth = 2;
circle.nonScaling = true;
circle.tooltipText = 'hi';
In this case, there's no need to pass chart to createMarkers and return it, so I've passed polygon instead just to demo dynamic latitude/longitudes, I also assign our new MapImage to the polygon's data (dataItem.dataContext) so we can refer to it later. Here's the new body of createMarkers:
function createMarkers(polygon) {
console.log('calling createMarkers');
if ( !polygon.dataItem.dataContext.redDot) {
const dataItem = polygon.dataItem;
// Object notation for making a MapImage
const redDot = mapImageSeries.mapImages.create();
// Note the lat/long are direct properties
redDot.id = `reddot-${dataItem.dataContext.id}`;
// attempt to make a marker in the middle of the country (note how this is inaccurate for US since we're getting the center for a rectangle, but it's not a rectangle)
redDot.latitude = dataItem.north - (dataItem.north - dataItem.south)/2;
redDot.longitude = dataItem.west - (dataItem.west - dataItem.east)/2;;
dataItem.dataContext.redDot = redDot;
}
}
There's no need for the animationended event or anything, it just works since there is no longer anything interfering with your code. You should also have your performance back.
Will this work for you?
Original answer prior to question's edits below:
I am unable to replicate the behavior you mentioned. Also, I don't know what this.countryZoom is.
Just using the following in a button handler...
chart.zoomToMapObject(polygon);
...seems to zoom just fine to the country, regardless of the current map position/zoomLevel.
If you need to time something after the zoom animation has ended, the zoomToMapObject returns an Animation, you can use its 'animationended' event, e.g.
const animation = this.chart.zoomToMapObject(selectedPoloygon, this.countryZoom, true);
animation.events.on("animationended", () => {
// ...
});
Here's an example with all that with 2 external <button>s, one for zooming to USA and the other Brazil:
https://codepen.io/team/amcharts/pen/c1d1151803799c3d8f51afed0c6eb61d
Does this help? If not, could you possibly provide a minimal example so we can replicate the issue you're having?
Related
I am trying to get the chart scrolled horizontally from externally triggered event calls like when a button is clicked elsewhere on the page. I am equally able to get the start and indexes of where i want the chart horizontally scrolled to. The chart's DateAxis is defined as below:
this.categoryAxis = chart.xAxes.push(new am4charts.DateAxis());
this.categoryAxis.dataFields.date = "value_date";
this.categoryAxis.cursorTooltipEnabled = false;
this.categoryAxis.start = 0.7;
this.categoryAxis.keepSelection = true;
And the function that is called by an external event is defined as below:
function(res){
let cpre = res.tree.split(',');
cpre.forEach((el, i) => {
cpre[i] = Number(el);
});
let visibleEl = this.data.findIndex((el, i) => {
return isEqual(cpre, el.graphIndex);
});
console.log(visibleEl);
//Trying to display 6 date points at any given time, not working.
let startIndex = visibleEl - 6 < 0 ? 0 : visibleEl - 6;
this.categoryAxis.zoomToIndexes(visibleEl, startIndex);
}
This line this.categoryAxis.zoomToIndexes(visibleEl, startIndex); currently zooms to the target indexes, but also zooms in the map which is not the desired effect. I only want to have it scroll to the desired position without zooming, thanks.
You can try to use valueAxis.strictMinMax (docs).
valueAxis.strictMinMax = true;
If that doesn't work try to use dateAxis.zoomToDates() instead of zoomToIndexes (docs).
Is it possible to indicate events along a series in amCharts v4 similar to the Stock Event in the v3 stock chart?
While I was brought on board specifically for v4 and am not familiar with v3, I'm confident you can simulate some of these features using Bullets.
A bullet is a Container (basically a placeholder parent for whatever visual object or additional Containers that you want), that will appear at every point of data. You can put a label there as well as a line and any other shape, e.g.:
var stockBullet = series.bullets.push(new am4charts.Bullet());
stockBullet.dy = -20;
var circle = stockBullet.createChild(am4core.Circle);
circle.stroke = "#000";
circle.strokeWidth = 1;
circle.radius = 10;
circle.fill = series.fill.brighten(-0.3);
circle.dy = -10;
var line = stockBullet.createChild(am4core.Line);
line.stroke = "#000";
line.strokeWidth = 1;
line.height = 20;
var label = stockBullet.createChild(am4core.Label);
label.fill = am4core.color("#000");
label.strokeWidth = 0;
label.dy = -20;
label.textAlign = "middle";
label.horizontalCenter = "middle"
Since we don't want a bullet to appear at every point of data, only at Stock Events, we can handle that once the bullets are ready on the chart by going through their data, disabling them if need be, otherwise providing text for our label (and maybe tooltipText if need be) (presume there is a property stockEvent in the data):
stockBullet.events.on("inited", function(event) {
if (event.target.dataItem && event.target.dataItem.dataContext && event.target.dataItem.dataContext.stockEvent) {
event.target.children.getIndex(2).text = event.target.dataItem.dataContext.stockEvent.text;
} else {
event.target.disabled = true;
}
});
Getting tooltips of different objects to play well with each other can be tricky depending on your chart, e.g. if it has Chart Cursor enabled there's a cursorTooltipEnabled property to prevent triggering a tooltip over bullets. To simplify things in this case what I did is make an invisible series per unique stock event bullet. For each stock event, use adapters to set its paired series' tooltipText to what's desired, and the base, visible series' tooltipText to "":
series.adapter.add("tooltipText", function(text, target) {
if (target.tooltipDataItem.dataContext.stockEvent) {
return "";
}
return text;
});
// ...
hiddenSeries.adapter.add("tooltipText", function(text, target) {
if (target.tooltipDataItem.dataContext.stockEvent) {
return target.tooltipDataItem.dataContext.stockEvent.description;
}
return "";
});
Here's a demo:
https://codepen.io/team/amcharts/pen/337984f18c6329ce904ef52a0c3eeaaa
Screenshot:
I'm working on some screen space shaders using THREE.JS and GLSL. I'd like to both render a "diffuse" buffer with per object materials/textures AND render an "ID" buffer where each object has a unique color for object edge detection. This would also be really useful for object picking as some of the THREE.JS examples show.
I am already using Scene.overrideMaterial to render normal buffers (and some other cool stuff) but haven't figured out a way to do something similar for an ID buffer. Is there something like this that exists? Seems useful enough that it would be likely. Have other people developed solutions to this problem? Maybe by mapping an objects UUID to a color value?
-----> EDIT
Here is my attempt using this example:
https://threejs.org/examples/webgl_interactive_instances_gpu.html
I'm not concerned with selection at this point, regardless this isn't working. I think I may be setting up onBeforeRender() incorrectly. Any help on this?
Thanks!
initPieces = function(geometry){
...
//set id Color to unique color
//for picking will use setHex(i) method
m.userData.idColor = material.color;
...
m.onBeforeRender = function (){
if(Viewer._scene.overrideMaterial){
if(Viewer._scene.overrideMaterial._name == "id"){
var updateList = [];
var u = scene.overrideMaterial.uniforms;
//picking color in user data
var d = this.userData;
//Is this just equivalent to checking whether this is the picking material?
if(u.idColor){
u.idColor.value = (d.idColor);
// u.needsUpdate = true;
updateList.push("idColor");
}
//Don't understand what this is doing
//Throws "WebGL INVALID_OPERATION: uniformMatrix4fv: location is not from current program"
if (updateList.length){
var materialProperties = renderer.properties.get(material);
if( materialProperties.program){
var gl = renderer.getContext();
var p = materialProperties.program;
gl.useProgram( p.program );
var pu = p.getUniforms();
updateList.forEach(function(name){
pu.setValue( gl, name, u[name].value);
});
}
}
}
}
}
// Doesn't show onBeforeRender set as I expected?
console.log(m);
...
}
...
}
...
function render(scene, camera){
...
scene.overrideMaterial = getMaterial("id");
renderer.render(scene, camera, getTarget("id"));
scene.overrideMaterial = null;
...
}
NEW:
So here is the code at codepen:
http://codepen.io/cmer41k/pen/pRJNww/
Currently function UpdateCoords(draggable) - is commented out in the code.
What I wanted is to update on mouseup event the coordinates of the path (circle as path here) to the absolute ones and remove transform attribute.
But I am failing to do that;(( sorry only learning
OLD:
In my code I have an svg element (path) that gets dragged around the root svg obj (svg) via transform="translate(x,y)" property.
I wanted to update such path element's attribute "d" (the string that describes all coords) to use absolute coordinates and get rid of transformed\translate thing.
Basically:
was: d="M10,10 30,10 20,30" + transform="translate(20,0);
to be: d="M30,10 50,10 40,30" + transform="translate(0,0)" (or if we can delete the transform - even better)
So I did the code that does the thing for me, but there is a bug that prevents proper result.
I am sure I am doing something wrong in here:
var v = Object.keys(path.controlPoints).length
// controlPoints here is just a place in path object where I store the coords for the path.
var matrix = path.transform.baseVal.consolidate();
//I validated that the above line does give me proper transform matrix with proper x,y translated values. Now below I am trying to loop through and update all control points (coordinates) of the path
for (i=0; i<v; i++) {
var position = svg.createSVGPoint();
position.x = path.controlPoints["p"+i].x;
position.y = path.controlPoints["p"+i].y;
// so for each of path's control points I create intermediate svgpoint that can leverage matrix data (or so I think) to "convert" old coords into the new ones.
position = position.matrixTransform(matrix);
path.controlPoints["p"+i].x = position.x;
path.controlPoints["p"+i].y = position.y;
}
// I am sure I am doing something wrong here, maybe its because I am not "cleaning"/resetting this position thing in this loop or smth?
Sorry I am not a programmer, just learning stuff and the question is - in this code snipped provided the goal that I described - is something wrong with how I handle "position"?
Alright, the code snipped is now functioning properly!
So after I figured how to obtain properly the matrix I still had a weird displacement for any subsequent draggables.
I became clear that those displacements happen even before my function.
I debugged it a bit and realized that I was not clearing the ._x and ._y params that I use for dragging.
Now code works!
http://codepen.io/cmer41k/pen/XpbpQJ
var svgNS = "http://www.w3.org/2000/svg";
var draggable = null;
var canvas = {};
var inventory = {};
var elementToUpdate = {};
//debug
var focusedObj = {};
var focusedObj2 = {};
// to be deleted
window.onload = function() {
canvas = document.getElementById("canvas");
inventory = document.getElementById("inventory");
AddListeners();
}
function AddListeners() {
document.getElementById("svg").addEventListener("mousedown", Drag);
document.getElementById("svg").addEventListener("mousemove", Drag);
document.getElementById("svg").addEventListener("mouseup", Drag);
}
// Drag function //
function Drag(e) {
var t = e.target, id = t.id, et = e.type; m = MousePos(e); //MousePos to ensure we obtain proper mouse coordinates
if (!draggable && (et == "mousedown")) {
if (t.className.baseVal=="inventory") { //if its inventory class item, this should get cloned into draggable
copy = t.cloneNode(true);
copy.onmousedown = copy.onmouseup = copy.onmousemove = Drag;
copy.removeAttribute("id");
copy._x = 0;
copy._y = 0;
canvas.appendChild(copy);
draggable = copy;
dPoint = m;
}
else if (t.className.baseVal=="draggable") { //if its just draggable class - it can be dragged around
draggable = t;
dPoint = m;
}
}
// drag the spawned/copied draggable element now
if (draggable && (et == "mousemove")) {
draggable._x += m.x - dPoint.x;
draggable._y += m.y - dPoint.y;
dPoint = m;
draggable.setAttribute("transform", "translate(" +draggable._x+","+draggable._y+")");
}
// stop drag
if (draggable && (et == "mouseup")) {
draggable.className.baseVal="draggable";
UpdateCoords(draggable);
console.log(draggable);
draggable._x = 0;
draggable._y = 0;
draggable = null;
}
}
edit See here for the non-working example of what I'm trying to do: http://bl.ocks.org/elsherbini/5814788
I am using dc.js to plot data collected from bee hives at my university. I am pushing new data to the graphs on every database change (using the magic of Meteor). When the database is over 5000 records or so, rerendering the lines gets really slow. So I want to use simplify.js to preprocess the lines before rendering. To see what I'm talking about, go to http://datacomb.meteor.com/. The page freezes after a couple of seconds, so be warned.
I have started to extend dc.js with a simpleLineChart, which would inherit from the existing dc.lineChart object/function. Here is what I have so far:
dc.simpleLineChart = function(parent, chartGroup) {
var _chart = dc.lineChart(),
_tolerance = 1,
_highQuality = false,
_helperDataArray;
_chart.tolerance = function (_) {
if (!arguments.length) return _tolerance;
_tolerance = _;
return _chart;
};
_chart.highQuality = function (_) {
if (!arguments.length) return _highQuality;
_highQuality = _;
return _chart;
};
return _chart.anchor(parent, chartGroup);
}
simplify.js takes in an array of data, a tolerance, and a boolean highQuality, and returns a new array with fewer elements based on it's simplification algorithm.
dc.js uses crossfilter.js. dc.js charts are associated with a particular crossfilter dimension and group. Eventually, it uses the data from someGroup().all() as the data to pass to a d3.svg.line(). I can't find where this is happening in the dc.js source, but this is where I need to intervene. I want to find this method, and override it in the dc.simpleLineChart object that I am making.
I was thinking something like
_chart.theMethodINeedToOverride = function(){
var helperDataArray = theChartGroup().all().map(function(d) { return {
x: _chart.keyAccessor()(d),
y: _chart.valueAccessor()(d)};})
var simplifiedData = simplify(helperDataArray, _tolerance, _highQuality)
g.datum(simplifiedData); // I know I'm binding some data at some point
// I'm just not sure to what or when
}
Can anyone help me either identify which method I need to override, or even better, show me how to do so?
dc.js source: https://github.com/NickQiZhu/dc.js/blob/master/dc.js
edit:
I think I may have found the function I need to override. The original function is
function createGrouping(stackedCssClass, group) {
var g = _chart.chartBodyG().select("g." + stackedCssClass);
if (g.empty())
g = _chart.chartBodyG().append("g").attr("class", stackedCssClass);
g.datum(group.all());
return g;
}
And I have tried to override it like so
function createGrouping(stackedCssClass, group) {
var g = _chart.chartBodyG().select("g." + stackedCssClass);
if (g.empty())
g = _chart.chartBodyG().append("g").attr("class", stackedCssClass);
var helperDataArray = group().all().map(function(d) { return {
x: _chart.keyAccessor()(d),
y: _chart.valueAccessor()(d)};})
var simplifiedData = simplify(helperDataArray, _tolerance, _highQuality)
g.datum(simplifiedData);
return g;
}
However, when I make a simpleLineChart, it is just a linechart with a tolerance() and highQuality() method. See here: http://bl.ocks.org/elsherbini/5814788
Well, I pretty much did what I set out to do.
http://bl.ocks.org/elsherbini/5814788
The key was to not only modify the createGrouping function, but also the lineY function in the code. (lineY gets set to tell the d3.svg.line() instance how to set the y value of a given point d)
I changed it to
var lineY = function(d, dataIndex, groupIndex) {
return _chart.y()(_chart.valueAccessor()(d));
};
The way lineY was written before, it was looking up the y value in an array, rather than using the data bound to the group element. This array had it's data set before i made my changes, so it was still using the old, pre-simplification data.