amcharts4 proper way to handle zero values on logarithmic value axis - javascript

I want to display some data with high variety in amcharts4, so I like to use the logarithmic scale on the value axis. Unfortunately the data contain some zero values, which of course can't be displayed with logarithmic scale.
I tried to change the zero values to 1 before rendering the chart, which would work, but now the values are not correct any more.
data.forEach(item => {
for (const key in item) {
if (item[key] === 0) {
item[key] = 1;
}
}
});
Is there any better way to handle zero values with logarithmic value axis, that I can display the correct data?
Here is a code pen, which shows my current solution.

Edit
As of version 4.9.34, treatZeroAs is officially supported. Just set it on the value axis to the desired value to remap your zero values to:
valueAxis.treatZeroAs = 0.1;
Updated codepen.
The below workaround isn't needed anymore, but you may find the value axis adapter snippet helpful in changing the first label when using treatZeroAs.
Old method - pre 4.9.34
There doesn't appear to be a direct equivalent to v3's treatZeroAs property, which automatically handled this sort of thing. Pre-processing the data is one step, but you can also copy the original value into a separate object property and use a series tooltip adapter to dynamically show your actual value:
data.forEach(item => {
for (const key in item) {
if (item[key] <= 0) {
item[key+"_actual"] = item[key]; //copy the original value into a different property
item[key] = 1;
}
}
});
// ...
//display actual data that was re-mapped if it exists
chart.series.each((series) => {
series.adapter.add("tooltipText", (text, target) => {
if (target.dataFields) {
let valueField = target.dataFields.valueY;
let tooltipData = target.tooltipDataItem;
if (tooltipData.dataContext[valueField + "_actual"] !== undefined) {
return '{' + valueField + '_actual}';
}
else {
return text;
}
}
else {
return text;
}
})
});
If you want to fake a zero label, you can use an adapter for that as well since your smallest value in this case will be 1:
//fake the zero axis label
valueAxis.renderer.labels.template.adapter.add("text", (text) => {
if (text === "1") {
return "0"
}
else {
return text;
}
})
Codepen

Related

Why does this code work for counting one item in a list but not the others?

I am using trying to count the number of "Manager"'s "MIT"'s and "Instore"'s in a .csv file. The code I have works for finding all the "Manager"'s but wont work for the other two.
I've tried splitting the function up into 3 separate functions as I thought the problem may have been using more than one dc.numberDisplay in a function but that didn't work. I've tried the function so there's only one, if it's looking for Managers it works, MIT's it does not and Instores it does not. I've tried changing the order of the code and still nothing. I've put console.log(p.mit_count) within each line of add_item, remove_item and initialise. I've put console.log(d.value.mit_count) within the valueAccessor.
Scott,Instore,3,BMC,96
Mark,Instore,4,Intro,94
Wendy,Instore,3,Intro,76
Lucas,Instore,2,Intro,96
.defer(d3.csv, "data/Results.csv")
.await(makeGraphs)
//function for making and rendering graphs
function makeGraphs(error, staffData) {
var ndx = crossfilter(staffData);
show_number_of_staff(ndx);
dc.renderAll();
}
function show_number_of_staff(ndx) {
var dim = ndx.dimension(dc.pluck('Rank'));
function add_item(p, v) {
if (v.Rank == "Manager") {
p.manager_count++;
}
else if (v.Rank == "MIT") {
p.mit_count++;
}
else if (v.Rank == "Instore") {
p.instore_count++;
}
return p;
}
function remove_item(p, v) {
if (v.Rank == "Manager") {
p.manager_count--;
}
else if (v.Rank == "MIT") {
p.mit_count--;
}
else if (v.Rank == "Instore") {
p.instore_count--;
}
return p;
}
function initialise(p, v) {
return { manager_count: 0, mit_count: 0, instore_count: 0 };
}
var staffCounter = dim.group().reduce(add_item, remove_item, initialise);;
dc.numberDisplay("#managerCount")
.formatNumber(d3.format(".0"))
.valueAccessor(function(d) {
return d.value.manager_count;
})
.group(staffCounter);
dc.numberDisplay("#mitCount")
.formatNumber(d3.format(".0"))
.valueAccessor(function(d) {
return d.value.mit_count;
})
.group(staffCounter);
dc.numberDisplay("#instoreCount")
.formatNumber(d3.format(".0"))
.valueAccessor(function(d) {
return d.value.instore_count;
})
.group(staffCounter);
}
console.log(p.mit_count) shows that it counts to 13 (as I am expecting it to), but then in the valueAccessor console.log(d.value.mit_count) shows 0. I cannot get why this works for "Manager" but nothing else. I'm almost embarrassed that this has taken me over a week. It just seems so simple!
I think if you log staffCounter.all() you'll find that it is counting managers, MITs, and Instores in 3 separate bins. This is because your dimension is sorting by Rank, then your group is binning again by Rank, so you end up with one bin per rank.
Normally you will want to pass a groupAll object, with one bin, to the numberDisplay. However, it is permissive and will also accept ordinary groups with multiple bins (source). It will sort the bins and take the last.
Why? I'm not sure. I think in almost every case you will want to reduce to one bin, but someone clearly had a use case where they wanted to display the largest value out of many bins.
I was surprised to find that the behavior is not documented, so I've updated the numberDisplay documentation.
With a groupAll your code will look something like:
var staffCounter = ndx.groupAll().reduce(add_item, remove_item, initialise);
dc.numberDisplay("#managerCount")
.formatNumber(d3.format(".0"))
.valueAccessor(function(d) {
return d.manager_count; // no .value here
})
.group(staffCounter);
Note that no crossfilter dimension is used here.

How to use forEach to update all values matching this criteria

I am creating a function for selecting/deselecting checkboxes representing objects in my array.
Currently I have:
selectAll(allType: string, state) {
this.modalData.columnPermissions.forEach(a =>
a["can" + allType] = state
);
}
(allType allows me to target "canRead" or "canWrite" keys depending on which SELECT ALL the user chooses from the top of 2 columns.)
This is working fine - however a scenario has now been introduced where if an object contains the property IDM=TRUE then "canWrite" should always be FALSE
I'm struggling on how to now adapt my selectAll function to exclude any object with a property of IDM=TRUE on the KEY canWrite
Any help is appreciated
I have resolved this with info from depperm
this.modalData.columnPermissions.forEach(a => {
if (allType === 'Write' && !a.IDM) {
a["can" + allType] = state
} else if (allType === 'Read') {
a["can" + allType] = state
}
})

if statement in a mapping

Currently I'm getting a
continue must be inside a loop
which I recognize as a syntax error on my part because it should be fixed.
Will fixing this to retain this logic in an if statement work with the mapping?
sales = data.map(function(d) {
if (isNaN(+d.BookingID) == false && isNaN(+d["Total Paid"]) == false) {
return [+d.BookingID, +d["Total Paid"]];
} else {
continue;
}
});
map is meant to be 1:1.
If you also want filtering, you should filter and then map
sales = (
data
.filter(d => (!isNaN(+d.BookingID)&& !isNaN(+d["Total Paid"]))
.map(d => [+d.BookingID, +d["Total Paid"]];
});
As others have mentioned, you cannot "continue" from within a map callback to skip elements. You need to use filter. To avoid referencing the fields twice, once in the filter, and once in the map, I'd filter afterwards:
sales = data
.map(d => [+d["bookingId"], +d["Total Paid"]])
.filter(([id, total]) => !isNaN(id) && !isNaN(total));
or, to make it easier in case you later want to include additional values in the array:
sales = data
.map(d => [+d["bookingId"], +d["Total Paid"]])
.filter(results => results.every(not(isNaN)));
where
function not(fn) { return x => !fn(x); }
or
function allNotNaN(a) { return a.every(not(isNaN)); }
and the, using parameter destructuring:
sales = data
.map(({bookingId, "Total Paid": total)) => [bookingId, total])
.filter(allNotNaN);

JQGrid custom summary match

Have successfully used JQGrid for a few projects, but struggling to get it to do what I want in this example.
I think I need to create a custom summaryType that checks whether records match, rather than sum, avg, min, max etc.
I need to check whether record 'Us', matches 'Them' and display the text 'Match' where the red X's are, could anyone give me some pointers on how to do this.
Surprisingly simple when you understand how it works, thanks to the comments on this answer
the jqGrid will call your function for every row (this is why you pass it to the option only by name, jqGrid requires the actual function not its result) - for first row the val should be empty and for next rows it should be the result of previous call.
Set a summaryType in your colModel as your function name, and use these functions
function numberMatch(val, name, record) {
if (val || 0 - record[name] == 0) {
return "Match";
} else {
return "unmatched";
}
}
function textMatch(val, name, record) {
if (val || '' === record[name]) {
return "Match";
} else {
return "unmatched";
}
}

D3: Nest and excluding certain keys

I am new to d3 and trying to plot some data in one box for each of four specific states, similar to this page but with states not continents, and a lot more data points. I have a json dataset of more than 42,000 entries supposedly from just those 4 states.
To key by state, I used this:
d3.json("data/business.json",function(json) {
var data=d3.nest()
.key(function(d) {return d.state;})
.sortKeys(d3.ascending)
.entries(json);
Then later make one box for each state:
// One cell for each state
var g=svg.selectAll("g").data(data).enter()
.append("g")
(attributes, etc)
Fine, but I soon found that the dataset includes some data from several states I don't want to consider so it was plotting more boxes than I wanted.
I would like a way to exclude the data that isn't from the four states without altering the original data file. What is the best way to go about this?
Filter your json:
var keep = ["state1", "state2", "state3", "state4"];
json = json.filter(function(d) { return keep.indexOf(d.state) > -1; });
It's possible to filter the output from d3.nest rather than the original array:
function trim(nested, f) {
return nested
.filter(function (e) {
return f(e.key)
})
.map(function (e) {
if (e && (typeof e =='object') && Array.isArray(e.values) && ('key' in e)) {
return { key: e.key, values: trim(e.values, f)}
}
else return e
})
}
For instance:
function isNewEngland(st) {
return ["ME","VT","NH","MA", "CT", "RI"].indexOf(st)>=0
}
data = trim(data, isNewEngland)

Categories

Resources