2

I am using dc.js to create cross-filterable charts with an array of "changesets" that have a schema like the following:

  id,
  first_name, last_name, user_id,
  created_at,
  num_changes,
  hashtags: [str],
  total_add, total_mod, total_del,
  buildings_add, buildings_mod, buildings_del,
  pois_add, pois_mod, pois_del,
  roads_add, roads_mod, roads_del,
  road_km_add, road_km_mod, road_km_del,
  waterways_add, waterways_mod, waterways_del,
  waterway_km_add, waterway_km_mod, waterway_km_del
}

Question

I seek to create filterable stacked bar charts with the bars representing add/mod/del and stacks representing the types of data changed buildings/pois/roads/waterways/road_km/waterway_km.

any 1 changeset can have any combination of these fields and so you cannot pair each changeset with 1 modification type. Is there some better way to accomplish this grouping where I can apply filters to the chart?

code I've tried

I have the chart display working with the correct data, but the way I've set it up has made it so the chart cannot be filtered. Initially I had written the dimension as crossfilter.dimension(d => ['add', 'mod', 'del'], true) so that each changeset shows up in each bin. but because all changesets would share add/mod/del, nothing is filtered.

I then saw the filter stacks example here: https://github.com/dc-js/dc.js/blob/develop/web-src/examples/filter-stacks.html

And I tried to run a multi-key dimension for the edits

let editDim = ndx.dimension(d => {
  let rt = []
  stackKeys.forEach(key => {
    editStacks.forEach(stack => {
      if (d[sAcc(stack,key)]) {
        rt.push(key + '.' + stack)
      }
    })
  })
  return rt
}, true)

this method looks so close but filtering the stacks do not produce the correct results on other charts. It seems like no matter what I choose to filter out of this chart, the other charts produce 0.

Here is a jsfiddle where I have 1 stacked bar chart using the multi-key method, 1 stacked bar chart using the ['add', 'mod', 'del'] key method and 1 regular bar chart to compare results/filtering with.

To separate the changesets into groups, I have used a custom reducer which transforms the data to something that looks like

{
    key: 'add',
    value: {
        add: {
            buildings': 42,
            pois: 12,
            roads: 1,
            waterway: 2,
            waterway_km: 0.003,
            road_km: 0
        },
        mod: {...}
        del: {...}
    }
}

The dimension is grouped into ['add', 'mod', 'del'] and the stacks are created using

const editStacks = ['buildings', 'pois', 'roads', 'waterways']
editStacks.forEach((stack, i) => {
    // first is group, others are stacked
    let action = i ? 'stack' : 'group'
    chart[action](group, stack, d => d.value[d.key][stack])
})

in the multi-key method, the values are transformed to be just {key: 'add', value: {building, roads, pois, waterways}} using this function

all: function () {
  var all = group.all()
  var m = {}
  all.forEach(kv => {
    let [k,s] = kv.key.split('.')
    m[k] = m[k] || {}
    m[k][s] = kv.value[k][s]
  })
  return Object.keys(m).map(key => {
    return {key, value: m[key]}
  })
}
Shawn Pacarar
  • 414
  • 3
  • 12
  • 1
    I haven't looked closely at your fiddle yet (thanks - always helpful!) The main issue with complex structured rows is that you can only filter at row granularity. So it doesn't make so much sense to filter on add, mod, del, since any row can belong to all of those. The best you could do is filter on "rows that have an add in them" but that's not mutually exclusive with "rows that have a mod in them". – Gordon Jun 11 '20 at 17:00
  • 2
    Your question is similar to https://stackoverflow.com/questions/34299017/dc-js-creating-a-row-chart-from-multiple-columns-and-enabling-filtering and the previous question linked there. You end up with the weird behavior that clicking on a bar can change the size of other bars in the same chart, which is not how dc.js usually works. – Gordon Jun 11 '20 at 17:01
  • 2
    I remembered a question which I think is closer to what you're trying to do: https://stackoverflow.com/questions/58132895/plotting-aggregated-data-with-sub-columns-in-dc-js, since it's like a tag dimension but with values associated with the tags. – Gordon Jun 12 '20 at 14:30
  • 1
    after rewriting both the stacked bar chart code for the add/mod/del and the hashtags chart code I finally got it to be filterable between both charts! Thank you so much for the guidance and all the work on these libraries. – Shawn Pacarar Jun 15 '20 at 17:15
  • 2
    That's great! Thanks for following up. If you feel like it, you could answer your own question. It might be helpful to others. BTW, looks like OSM data - good stuff! – Gordon Jun 16 '20 at 14:45

1 Answers1

0

So to get the stacked bars filterable by each section I had to use a combination of the answers found in dc.js - Creating a row chart from multiple columns and enabling filtering and in Plotting aggregated data with sub-columns in dc.js

to construct a filterable dimension you must use the methods described in the first link to create a custom filterHandler on your dimension which is just grouped up by element

let dimension = ndx.dimension(d => d)

then I constructed my group by using groupAll and creating an object of { key.stack: value }} using the reduce functions. Then I converted it back to a standard group by creating an "all" method for the group as described in the second link.

function reduceAdd (p, v) {
    keys.forEach(k => {
        stacks.forEach(s => {
            p[`${k}.${s}`] += v[accessor(k, s)] || 0
        })
    })
    return p
}
function reduceRemove (p, v) {
    keys.forEach(k => {
        stacks.forEach(s => {
            p[`${k}.${s}`] -= v[accessor(k, s)] || 0
        })
    })
    return p
}
function reduceInit () {
    let p = {}
    keys.forEach(k => {
        stacks.forEach(s => {
            p[`${k}.${s}`] = 0
        })
    })
    return p
}

function stackedGroup (group) {
    return {
        all: function () {
            var all = Object.entries(group.value()).map(([key, value]) => ({ key, value }))
            var m = {}
            all.forEach(kv => {
                let [k, s] = kv.key.split('.')
                m[k] = m[k] || {}
                m[k][s] = kv.value
            })
            return Object.keys(m).map(key => {
                return { key, value: m[key] }
            })
        }
    }
}

let group = dimension.groupAll().reduce(reduceAdd, reduceRemove, reduceInit)
group = stackedGroup(group)

finally you must redefine the filter handler as mentioned in the first link. This filter is what I use to filter by "changesets that have at least 1 x" where "x" is some type of edit such as "add buildings" or "modified road"

chart.filterHandler((dim, filters) => {
    if (filters && filters.length) {
        dim.filterFunction((r) => {
            return filters.some((c) => {
                //the changeset must have a value in this field to be left in the chart
                let [stack, field] = c[0].split('.')
                return r[accessor(field, stack)] > 0
            })
        })
    } else {
        dim.filter(null)
    }
    return filters
})

After that, the hashtags chart was showing unexpected results, so I had to do a similar dimension & grouping for the hashtags chart as well. I believe this is because the hashtags chart was defined with an array dimension, but grouping it as d => d and defining a custom filterHandler has produced results I am pleased with. Both charts are filterable to find results to questions such as "how many changesets with the hashtag #Kaart have road modifications".

Thanks again @Gordon!

Shawn Pacarar
  • 414
  • 3
  • 12