How to create a stock event in amCharts v4? - amcharts

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:

Related

How can AMCharts 4 be scrolled horizontally from javascript code?

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

Trying to set custom colours for a chart

I'm trying to add custom colours to a column chart, so each column has a different colour. I have the following code:
__this._chartColours = ['#2776BD', '#00A1D0','#00C195','#7ED321','#A8C600','#C9B600','#E3A600', '#F7941E', '#FC7149'];
__this._chart = am4core.create(__this._tileChartDiv[0], am4charts.XYChart);
if(result.chartDataMap != null)
{
var colorSet = new am4core.ColorSet();
var counter = 0;
$.each(result.chartDataMap, function(xAxis, yAxis)
{
__this._dataProvider.push({"category": xAxis, "column-1": yAxis});
__this._chart.colors.list.push(am4core.color(__this._chartColours[counter]));
});
__this._chart.data = __this._dataProvider;
__this._chart.padding(40, 40, 40, 40);
var categoryAxis = __this._chart.xAxes.push(new am4charts.CategoryAxis());
categoryAxis.renderer.grid.template.location = 0;
categoryAxis.dataFields.category = "category";
categoryAxis.renderer.minGridDistance = 60;
categoryAxis.title.text = result.xAxisTitle;
var label = categoryAxis.renderer.labels.template;
label.wrap = true;
label.maxWidth = 120;
var valueAxis = __this._chart.yAxes.push(new am4charts.ValueAxis());
valueAxis.title.text = result.yAxisTitle;
var series = __this._chart.series.push(new am4charts.ColumnSeries());
series.dataFields.categoryX = "category";
series.dataFields.valueY = "column-1";
series.tooltipText = "{valueY.value}"
series.columns.template.strokeOpacity = 0;
__this._chart.cursor = new am4charts.XYCursor();
}
It renders the chart fine based on the dataprovider I create but it is not setting the colors. I got the colours code from here: https://www.amcharts.com/docs/v4/concepts/colors/. The XY Chart and derivative charts section
I tried to use a theme, but that didn't work either:
function am4themes_myTheme(target)
{
if (target instanceof am4core.ColorSet)
{
$.each(__this._chartColours, function(index, item)
{
target.list.push(am4core.color(item));
});
}
}
am4core.useTheme(am4themes_myTheme);
It sets all the columns to the first colour. Then I tried adding a color property to the dataProvider for each column but again it sets them all to have the first colour.
I'm pretty much out of ideas.
There are a few issues here.
First, if you want the chart to use only your colors, instead of appending to the default chart's ColorSet, you have to manually override it by assigning an array of Colors to chart.colors.list (instead of pushing values to it).
Next, the column's color (fill) by default is based on its series. So even if you populate the chart's ColorSet, it's only each new series that will get a different color, not each column.
To set an individual column's color it would be something like:
column.fill = am4core.color("#2776BD");
To get each column to have its own color, we can set that upon the column's first instantiation, i.e. on its template's inited event. Further, a column's dataItem will have a property/reference to its index, so we can use that with ColorSet's getIndex method to assign colors in sequence.
So your final code might look something like this:
__this._chart.colors.list = [
am4core.color("#2776BD"),
am4core.color("#00A1D0"),
am4core.color("#00C195"),
am4core.color("#7ED321"),
am4core.color("#A8C600"),
am4core.color("#C9B600"),
am4core.color("#E3A600"),
am4core.color("#F7941E"),
am4core.color("#FC7149")
];
series.columns.template.events.once("inited", function(event){
event.target.fill = chart.colors.getIndex(event.target.dataItem.index);
});
Here's a fork of our Simple Column Chart demo with the above code and your custom colors:
https://codepen.io/team/amcharts/pen/2ef06f392b347412c61bcdcd3439a5c6

AmCharts AmMap - Set starting location for zoom actions

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?

UPDATED: Javascript logic to fix in a small function (SVG, obtaining absolute coords)

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;
}
}

Reloading labels when scrolling esri map

What I'm trying to do is reload my labels when the user moves or scrolls the map to a different position. Currently when the user zooms in past a certain level the labels load and every thing works correct. When the user starts to move the map to a different state the labels disappear and you have to zoom out and zoom back in to regenerate the labels.
I've changed onZoomEnd to update / update-end / load / onLoad
Here is the code:
function initUI(graphics) {
dojo.connect(globals.map, 'onZoomEnd', function () {
console.log("Initial zoom level is :" + globals.map.getZoom());
var font = new esri.symbol.Font(14, esri.symbol.Font.STYLE_NORMAL, esri.symbol.Font.VARIANT_NORMAL, esri.symbol.Font.WEIGHT_BOLDER, "Arial");
var gl = globals.featureLayers[1].graphics;
globals.map.graphics.clear();
if (globals.map.getZoom() >= 9) {
console.log(codeID);
for (var i = 0; i < gl.length ; i++) {
var g = globals.featureLayers[1].graphics[i];
if (codeID == 1 || codeID == 32 || codeID == 28 || codeID == 33 || codeID == 10) {
var strLabel = g.attributes.NAME + ":" + $.formatNumber(findFips(g), { format: '#,###', locale: "us" });//creates string label formatted
var textSymbol = new esri.symbol.TextSymbol(strLabel, font);//create symbol with attribute name
textSymbol.setColor(new dojo.Color([0, 0, 0]));//set the color
var pt = g.geometry.getExtent().getCenter(); //get center of county
var labelPointGraphic = new esri.Graphic(pt, textSymbol); //create label graphic
//add label to the intended graphic
globals.map.graphics.add(labelPointGraphic);
}
else {
var strLabelPct = g.attributes.NAME + " : " + $.formatNumber(findFips(g), {format: '#,###.0', locale: "us"}) + "%";
var textSymbol = new esri.symbol.TextSymbol(strLabelPct);//create symbol with attribute name
textSymbol.setColor(new dojo.Color([0, 0, 0]));//set the color
var pt = g.geometry.getExtent().getCenter(); //get center of county
var labelPointGraphic = new esri.Graphic(pt, textSymbol); //create label graphic
//add label to the intended graphic
globals.map.graphics.add(labelPointGraphic);
}
}//end for
}//end if
});//end on zoom end
If possible, the new 3.7 Esri ArcGIS JavaScript API has a new LabelLayer that may help deal with your issue. It doesn't have as many features but is a great start for a beta feature.
Label Layer
Here is a block of code that I've used (written in 3.7). It uses the new AMD style require and "dojo/on" to attach the updated event triggers to the map.
map.on('zoom-end', function() {
handleMapPanZoom(); // Turns some complex layers on and off.
maxOffset = calcOffset(map); // Updates the max offset at each zoom level.
for (var i = 0; i < lyrs.length; i++) {
lyrs[i].setMaxAllowableOffset(maxOffset);
}
});
map.on('extent-change', function() {
handleMapPanZoom();
});

Resources