Use full group record within title in dc-js geoChoropleth chart - dc.js

I have a group for which elements after reduction look like this pseudocode :
{
key:"somevalue",
value: {
sum: the_total,
names:{
a: a_number,
b: b_number,
c:c_number
}
}
}
In my dc-js geoChoropleth graph the valueAccessor is (d) => d.value.sum
In my title, I would like to use the names component of my reduction. But when I use .title((d) => {...}), I can onjly access the key and the value resulting from the valueAccessor function instead of the original record.
Is that meant to be ?

This is a peculiarity of the geoChoropleth chart.
Most charts bind the group data directly to chart elements, but since the geoChoropleth chart has two sources of data, the map and the group, it binds the map data and hides the group data.
Here is the direct culprit:
_renderTitles (regionG, layerIndex, data) {
if (this.renderTitle()) {
regionG.selectAll('title').text(d => {
const key = this._getKey(layerIndex, d);
const value = data[key];
return this.title()({key: key, value: value});
});
}
}
It is creating key/value objects itself, and the value, as you deduced, comes from the valueAccessor:
_generateLayeredData () {
const data = {};
const groupAll = this.data();
for (let i = 0; i < groupAll.length; ++i) {
data[this.keyAccessor()(groupAll[i])] = this.valueAccessor()(groupAll[i]);
}
return data;
}
Sorry this is not a complete answer, but I would suggest adding a pretransition handler that replaces the titles, or alternately, using the key passed to the title accessor to lookup the data you need.
As I noted in the issue linked above, I think this is a pretty serious design bug.

Related

How to filter record by custom value from any dimension in dc.js?

How to remove custom records from any dimension. In the below case how do I filter only category 'S' and allow rest of them in dimension ?
Example
let data = [
{category:'A',value:10},
{category:'B',value:11},
{category:'S',value:12},
{category:'A',value:14},
{category:'B',value:12},
]
let ndx = crossfilter(data);
let dim= ndx.dimension(function(d){
if(d.category != "S") return d.category;
})
This above code runs into loop and the application crashes. I don't want to create separate data for this dimension rather link it with other cross filters.
I guess its pretty simple, I did little research after posting the question.
Just manipulate the group parameter being passed to the chart. The code goes something like this.
Since I am trying to remove the value by key lets first write a function for further uses as well.
function removeByKey(source_group, value) {
return {
all: function() {
return source_group.all().filter(function(d) {
return d.key != value;
});
}
};
}
Once this is done the place where you call the group method for the charts call this method. The first parameter of removeByKey method is the group itself the second is the key value which is supposed to be removed from the chart.
chart
.dimension(dimension_data)
.group(removeByKey(dimension_data_group, 'S'))
Thanks :)

How to get multiple properties from objects in JXA?

Is there a way in JXA to get multiple properties from multiple objects with a single call?
For example, I want to get name and enabled property from menu items which can be done for each individual property as follows:
Application("System Events").processes.byName('Finder').menuBars[0].menuBarItems.name()
Application("System Events").processes.byName('Finder').menuBars[0].menuBarItems.enabled()
but is it possible to get them with a single function call? Something like:
Application("System Events").processes.byName('Finder').menuBars[0].menuBarItems.select('name', 'enabled')
I know, that I can iterate through the menuBarItems and collect properties from .properties() method, but this approach is too slow, that's why I'm looking for other options.
UPDATE
I'm looking for better performance, not for nicer syntax, i.e. I want properties to be retrieved in a single call to System Events.
I'd probably do it like this:
sys = Application('com.apple.systemevents');
FinderProc = sys.processes['Finder'];
FinderMenuBarItems = FinderProc.menuBars[0].menuBarItems();
Array.from(FinderMenuBarItems,x=>[x.name(),x.enabled()]);
By first converting the object to an array, this allows one to map each element and retrieve the desired properties for all in one go. The code is split over several lines for ease of reading.
EDIT: added on 2019-07-27
Following on from your comment regarding Objective-C implementation, I had a bit of time today to write a JSObjc script. It does the same thing as the vanilla JXA version above, and, yes, it clearly makes multiple function calls, which is necessary. But it's performing these functions at a lower level than System Events (which isn't involved at all here), so hopefully you'll find it more performant.
ObjC.import('ApplicationServices');
ObjC.import('CoreFoundation');
ObjC.import('Foundation');
ObjC.import('AppKit');
var err = {
'-25211':'APIDisabled',
'-25206':'ActionUnsupported',
'-25205':'AttributeUnsupported',
'-25204':'CannotComplete',
'-25200':'Failure',
'-25201':'IllegalArgument',
'-25202':'InvalidUIElement',
'-25203':'InvalidUIElementObserver',
'-25212':'NoValue',
'-25214':'NotEnoughPrecision',
'-25208':'NotImplemented',
'-25209':'NotificationAlreadyRegistered',
'-25210':'NotificationNotRegistered',
'-25207':'NotificationUnsupported',
'-25213':'ParameterizedAttributeUnsupported',
'0':'Success'
};
var unwrap = ObjC.deepUnwrap.bind(ObjC);
var bind = ObjC.bindFunction.bind(ObjC);
bind('CFMakeCollectable', [ 'id', [ 'void *' ] ]);
Ref.prototype.nsObject = function() {
return unwrap($.CFMakeCollectable(this[0]));
}
function getAttrValue(AXUIElement, AXAttrName) {
var e;
var _AXAttrValue = Ref();
e = $.AXUIElementCopyAttributeValue(AXUIElement,
AXAttrName,
_AXAttrValue);
if (err[e]!='Success') return err[e];
return _AXAttrValue.nsObject();
}
function getAttrValues(AXUIElement, AXAttrNames){
var e;
var _AXAttrValues = Ref();
e = $.AXUIElementCopyMultipleAttributeValues(AXUIElement,
AXAttrNames,
0,
_AXAttrValues);
if (err[e]!='Success') return err[e];
return _AXAttrValues.nsObject();
}
function getAttrNames(AXUIElement) {
var e;
var _AXAttrNames = Ref();
e = $.AXUIElementCopyAttributeNames(AXUIElement, _AXAttrNames);
if (err[e]!='Success') return err[e];
return _AXAttrNames.nsObject();
}
(() => {
const pid_1 = $.NSWorkspace.sharedWorkspace
.frontmostApplication
.processIdentifier;
const appElement = $.AXUIElementCreateApplication(pid_1);
const menuBar = getAttrValue(appElement,"AXMenuBar");
const menuBarItems = getAttrValue(menuBar, "AXChildren");
return menuBarItems.map(x => {
return getAttrValues(x, ["AXTitle", "AXEnabled"]);
});
})();

dc.js filtered table export using filesaver.js

I'm trying to export dc.js filtered table data using FileSaver.js.
I use the code below based on this which is fine except it export all fields (but filtered ok) whereas I would just need table specific fields which are are only a few of the fields plus 2 calculated.
d3.select('#download')
.on('click', function() {
var blob = new Blob([d3.csv.format(dateDim.top(Infinity))], {type: "text/csv;charset=utf-8"});
saveAs(blob, DateT + '.csv');
});
Is there a way I can point to the table rather that dimension?
Thanks.
EDIT: Working code below
d3.select('#download')
.on('click', function() {
var data = MYTABLEDIM.top(Infinity);
{
data = data.map(function(d) {
var row = {};
MYTABLENAME.columns().forEach(function(c) {
row[MYTABLENAME._doColumnHeaderFormat(c)] = MYTABLENAME._doColumnValueFormat(c, d);
});
return row;
});
}
var blob = new Blob([d3.csv.format(data)], {type: "text/csv;charset=utf-8"});
saveAs(blob, 'data.csv');
});
Good question.
It is actually possible to format the data according to the column definitions, by using some undocumented methods of the data table.
I've updated the example with a radio button to choose which data to download.
Here is the code that transforms and download the data as it is encoded in the table:
d3.select('#download')
.on('click', function() {
var data = nameDim.top(Infinity);
data = data.map(function(d) {
var row = {};
table.columns().forEach(function(c, i) {
// if you're using the "original method" for specifying columns,
// use i to index an array of names, instead of table._doColumnHeaderFormat(c)
row[table._doColumnHeaderFormat(c)] = table._doColumnValueFormat(c, d);
});
return row;
});
var blob = new Blob([d3.csv.format(data)], {type: "text/csv;charset=utf-8"});
saveAs(blob, 'data.csv');
});
Basically, when the table radio is selected, we'll transform the data row-by-row using the same functions that the table uses to format its data.
The rows will be in the order of the original data, not sorted like the table. (And strictly speaking, the columns may not be in the same order either). That would be a bigger endeavor, and might require new features in dc.js. But this works without any changes. Hope it helps!

Properly show aliased legend items in dc.js (glitchless transitions)

My data has a field (let's call it Kind) which for internal purposes is a short string (short-form), but which has a mapping to rather verbose strings (long-form), intended for presentation. For example:
// Not the real values, but you get the idea...
var kind_map = {'c2a': "Collected Compound Allocations",
'dee': "Digital Extrapolated Exponents", ...};
Among my visualization widgets there is a pie chart that summarizes the data with respect to Kind. Since the long-form strings are the ones I intend to display, I have arranged to display them out of the pie slices (see this question). However, as you will notice in this example fiddle, during transitions the legend items revert to their original filter values, which are the short-form strings. I want to avoid that, but have been unsuccessful so far. I have tried also modifying the filterPrinter, filterHandler and others, but the behavior persists.
My code is as follows:
cxf = crossfilter(raw_data); //raw_data comes from d3.csv, json, whatever
kind_D = cxf.dimension( function(d) { return d.Kind; } );
kind_G = kind_D.group().reduceSum( function(d) { return d.Value; });
kind_chart = dc.pieChart('#kind-chart');
kind_chart.width(a_width)
.height(a_height)
.radius(a_radius)
.dimension(kind_D)
.group(kind_G)
.legend( dc.legend().x(this.groups_base_dim).y(50).gap(10) );
// Expand the group's legend with verbose group names from the JSON config
var kind_name_render = function(chart) {
chart.selectAll(".dc-legend-item text")
.html( function (d, i) {
return kind_map[d.name];
});
}
kind_chart.on("postRedraw", kind_name_render);
kind_chart.on("postRender", kind_name_render);
You may be able to use the new data function in 2.0 to apply a transform to the keys returned by the group.

Loading D3.js data from a simple JSON string

Most of the examples in gallery load data from TSV files.
How can I convert the following to use a local json variable instead of TSV data?
d3.tsv("data.tsv", function(error, data) {
var myEntitiesJson = getEntitiesJson(); // <------ use this instead of "data"
data.forEach(function(d) {
d.frequency = +d.frequency;
});
x.domain(data.map(function(d) { return d.letter; }));
y.domain([0, d3.max(data, function(d) { return d.frequency; })]);
...
svg.selectAll(".bar")
.data(data) // <----- bind to myEntities instead
}
As far as I can tell, I just need to do something to my entitiesJson, in order to data-fy it so that the chart could bind to it.
UPDATE
I am making some progress. I plugged in my entities from JSON and the graph is starting to take new shape.
Currently the following code breaks:
svg.selectAll(".bar")
.data(myEntities) // <-- this is an array of objects
.enter().append("rect")
This is causing:
Error: Invalid value for attribute y="NaN"
Error: Invalid value for attribute height="NaN"
for remote data.json
replace :
d3.tsv("data.tsv", function(error, data) {...}
with :
d3.json("data.json", function(error, data) {
console.log(data); // this is your data
});
for local data:
var myData = { {date:'2013-05-01', frequency:99},
{date:'2013-05-02', frequency:24} };
function draw(data) {
console.log(data); // this is your data
}
draw(myData);
There isn't a simple way to data-fy any given json, because not all json objects are the same shape.
By shape, I mean the way that the data is organized. For example, both '{"foo" : 1, "bar" : 2}' and '{"names" : ["foo", "bar"], "values" : [1, 2]}' could be used to store the same data, but one stores everything in an object in which the object keys correspond to the names of data points, and one uses separate arrays to store names and values, with corresponding entries having a common array index.
There is, however, a general process you can go through to turn json into data. First, you'll need to parse your json. This can be done with the javascript-standard JSON object. USe JSON.parse(myJson) to obtain data from your json object if it's already uploaded to the client. d3.json(my/json/directory, fn(){}) can both load and parse your json, so if you're loading it from elsewhere on your server, this might be a better way to get the json into an object.
Once you have your json packed into a javascript object, you still need to data-fy it, which is the part that will depend on your data. What d3 is going it expect is some form of array: [dataPoint1, dataPoint2, ...]. For the two examples I gave above, the array you would want would look something like this:
[{'name' : 'foo', 'value' : 1}, {'name' : 'bar', 'value' : 2}]
I've got one element in my array for each data point, with two attributes: value and name. (In your example, you would want the attributes letter and frequency)
For each of my examples, I would use a different function to create the array. With this line in common:
var rawData = JSON.parse(myJson);
My first json could be packed with this function:
var key;
var data = [];
for(key in rawData){
if(rawData.hasOwnProperty(key)){
data.push({'name' : key, 'value' : rawData[key]});
}
}
For the second example, I would want to loop through each attribute of my object, names, and values. My code might look like this:
var i;
var data = [];
for(i = 0; i < rawData.names.length; i++){
data.push({'name' : rawData.names[i], 'value' : rawData.values[i]});
}
Both of these will yield a data-fied version of my original JSON that I can then use in d3.
For D3js v2 or v3 (not sure which one).
Declare your dataset
var dataset = {
"first-name": "Stack",
"last-name": "Overflow",
}; // JSON object
var dataset = [ 5, 10, 15, 20, 25 ]; // or array
As stated by the doc, you can use either:
an array of numbers or objects, or a function that returns an array of values
Bind it
d3.select("body").selectAll("p")
.data(dataset)
.enter()
.append("p")
.text("New paragraph!");
More explanation at Scott Murray's D3's tutorial#Binding data.
The data() function apply to a selection, more information can be found in the official documentation: selection.data([values[, key]]).
You can change the json into a javascript file that assigns the data to a global value. Taking https://bl.ocks.org/d3noob/5028304 as an example:
From:
<script>
.....
// load the data
d3.json("sankeygreenhouse.json", function(error, graph) {
var nodeMap = {};
graph.nodes.forEach(function(x) { nodeMap[x.name] = x; });
To:
<script src="graphData.js"></script>
<script>
.....
var nodeMap = {};
graph.nodes.forEach(function(x) { nodeMap[x.name] = x; });
Note that we've removed the need for the callback.
The json file was "sankeygreenhouse.json":
{
"links": [
{"source":"Agricultural Energy Use","target":"Carbon Dioxide","value":"1.4"},
Now, in "graphData.js":
var graph = {
"links": [
{"source":"Agricultural Energy Use","target":"Carbon Dioxide","value":"1.4"},
Just change data to an array of objects like this:
let data = [{"apples":53245,"oranges":200},{"apples":28479,"oranges":200},{"apples":19697,"oranges":200},{"apples":24037,"oranges":200},{"apples":40245,"oranges":200}]
and comment out the d3.tsv("data.tsv", function(error, data) {...
Why not simply transform your json to tsv as described by Rob here?
d3 expects the data or, said in another way, needs the data in a particular format: tsv.The easiest way to resolve your problem is simply formatting your data from json to tsv, which can be done easily using Rob's comments.

Resources