3
\$\begingroup\$

I would like to chart my data. However, the data I get from server needs to be completely transformed, and I would like to transform it as quick as possible. I believed reduce would be the best way to go about this in linear time. However, I am not sure if I am fully exploiting it.

  • The data from server needs to be reorganized into three categories: country, occupation, and age
  • When I encounter a country, occupation, or age, I need to group them under their appropriate category and increment their count.

In my code, I am using reduce to loop over the items once. I am hashing to find duplicates. I am using some also hashing to locate the category titles in the nested arrays in order to increment the count.

This is the original data:

var object = {
  dataset: [
    {
      name: "Sarah",
      age: "23",
      gender: "female",
      country: "australia",
      occupation: "student"
    },
    {
      name: "Randy",
      age: "19",
      gender: "male",
      country: "america",
      occupation: "student"
    },
    {
      name: "Roger",
      age: "32",
      gender: "male",
      country: "germany",
      occupation: "software professional"
    },
    {
      name: "Maverick",
      age: "10",
      gender: "male",
      country: "america",
      occupation: "student"
    },
    {
      name: "Riya",
      age: "25",
      gender: "female",
      country: "australia",
      occupation: "software professional"
    },
    {
      name: "Glade",
      age: "30",
      gender: "female",
      country: "India",
      occupation: "teacher"
    }
  ]
};

This is the desired transformation:

[
  {
    "name": "country",
    "dataset": [
      {
        "value": "australia",
        "count": 2,
        "color": "#878BB6"
      },
      {
        "value": "america",
        "count": 2,
        "color": "#878BB6"
      },
      {
        "value": "germany",
        "count": 1,
        "color": "#878BB6"
      },
      {
        "value": "India",
        "count": 1,
        "color": "#878BB6"
      }
    ]
  }
] [
  {
    "name": "occupation",
    "dataset": [
      {
        "value": "student",
        "count": 3,
        "color": "#878BB6"
      },
      {
        "value": "software professional",
        "count": 2,
        "color": "#878BB6"
      },
      {
        "value": "teacher",
        "count": 1,
        "color": "#878BB6"
      }
    ]
  }
] [
  {
    "name": "age",
    "dataset": [
      {
        "value": "23",
        "count": 1,
        "color": "#878BB6"
      },
      {
        "value": "19",
        "count": 1,
        "color": "#878BB6"
      },
      {
        "value": "32",
        "count": 1,
        "color": "#878BB6"
      },
      {
        "value": "10",
        "count": 1,
        "color": "#878BB6"
      },
      {
        "value": "25",
        "count": 1,
        "color": "#878BB6"
      },
      {
        "value": "30",
        "count": 1,
        "color": "#878BB6"
      }
    ]
  }
]

This is my working code:

const object = {
  dataset: [{
      name: "Sarah",
      age: "23",
      gender: "female",
      country: "australia",
      occupation: "student"
    },
    {
      name: "Randy",
      age: "19",
      gender: "male",
      country: "america",
      occupation: "student"
    },
    {
      name: "Roger",
      age: "32",
      gender: "male",
      country: "germany",
      occupation: "software professional"
    },
    {
      name: "Maverick",
      age: "10",
      gender: "male",
      country: "america",
      occupation: "student"
    },
    {
      name: "Riya",
      age: "25",
      gender: "female",
      country: "australia",
      occupation: "software professional"
    },
    {
      name: "Glade",
      age: "30",
      gender: "female",
      country: "India",
      occupation: "teacher"
    }
  ]
};




var mapping = Object.values(object.dataset).reduce(
  (function(graph) {
    graph = {
      hash: {},
      index: {}
    };
    let {
      hash,
      index
    } = graph;
    hash = {
      "graph-data": [
        [{
          name: "country",
          dataset: []
        }],
        [{
          name: "occupation",
          dataset: []
        }],
        [{
          name: "age",
          dataset: []
        }]
      ]
    };
    return function(acc, item) {
      hash[item.country] && hash["graph-data"][0][0].dataset[index[item.country]].count++;
      if (!hash[item.country]) {
        hash[item.country] = true;
        hash["graph-data"][0][0].dataset.push({
          value: item.country,
          count: 1,
          color: "#878BB6"
        });
        index[item.country] = hash["graph-data"][0][0].dataset.length - 1;
      }
      hash[item.occupation] && hash["graph-data"][1][0].dataset[index[item.occupation]].count++;
      if (!hash[item.occupation]) {
        hash[item.occupation] = true;
        hash["graph-data"][1][0].dataset.push({
          value: item.occupation,
          count: 1,
          color: "#878BB6"
        });
        index[item.occupation] = hash["graph-data"][1][0].dataset.length - 1;
      }
      hash[item.age] && hash["graph-data"][2][0].dataset[index[item.age]].count++;
      if (!hash[item.age]) {
        hash[item.age] = true;
        hash["graph-data"][2][0].dataset.push({
          value: item.age,
          count: 1,
          color: "#878BB6"
        });
        index[item.age] = hash["graph-data"][2][0].dataset.length - 1;
      }
      acc = hash;
      return acc;
    };
  })(Object.create(null)), {}
);
console.log(...mapping["graph-data"]);

\$\endgroup\$
3
  • \$\begingroup\$ 1. Use a utility function like ramda's groupBy for the grouping transforms (quick, incomplete example code. 2. Don't couple your graph coloring logic to the grouping logic. Do it in a separate pass. You'll still have linear performance. \$\endgroup\$ – Jonah Jul 16 '17 at 3:03
  • \$\begingroup\$ @Jonah thanks. I am not a fan of libraries. However, this code could definitely use some refactoring and optimize, I might change out the object for a Map or a Set, I am really not sure. This is where I am currently stuck right now :( . You are right, I should do the colors in a separate pass. \$\endgroup\$ – Rick Jul 16 '17 at 3:24
  • \$\begingroup\$ I'd strongly advise you to reconsider that opinion. You'll be using libraries whether you want to or not -- the only question is how good they'll be. It's a great exercise to write things like this yourself, but it should be just that. At some point, you should be re-using high-level functions to solve your problems. You'll want a light, well-designed, well-tested library when you do. And btw, reduce, map, some, etc are just library functions that have been standardized into the language. \$\endgroup\$ – Jonah Jul 16 '17 at 3:29
2
\$\begingroup\$

I suggest to use a splitted approach, one for collecting a group with a given key and another for mapping the results into an array in the wanted form.

Basically groupBy(array, key, pattern) uses a given array with the data, the key for the value for grouping and an optional pattern for the result set of the grouped and counted values.

Main part is a Map for collecting counts of specified group with a key of the object.

In a loop all groups are either used or if not exist, created. After that, a simple increment takes place.

For the wanted result, the wanted keys for grouping are iterated and a new data structure for any key is created in the wanted style.

function groupBy(array, key, pattern) {
    var map = new Map;

    array.forEach(function (o) {
        var group = map.get(o[key]) || Object.assign({ value: o[key], count: 0 }, pattern);
        if (!map.has(o[key])) {
            map.set(o[key], group);
        }
        group.count++;
    });
    return [...map.values()];
}

var object = { dataset: [{ name: "Sarah", age: "23", gender: "female", country: "australia", occupation: "student" }, { name: "Randy", age: "19", gender: "male", country: "america", occupation: "student" }, { name: "Roger", age: "32", gender: "male", country: "germany", occupation: "software professional" }, { name: "Maverick", age: "10", gender: "male", country: "america", occupation: "student" }, { name: "Riya", age: "25", gender: "female", country: "australia", occupation: "software professional" }, { name: "Glade", age: "30", gender: "female", country: "India", occupation: "teacher" }] },
    result = ['country', 'occupation', 'age'].map(key => ({
        name: key,
        dataset: groupBy(object.dataset, key, { color: '#878BB6' })
    }));

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

\$\endgroup\$
1
  • \$\begingroup\$ I hate how simple, clean, and short this answer is. I hate it!!! : ) \$\endgroup\$ – Rick Jul 16 '17 at 19:30
3
\$\begingroup\$

You need to abstract your code, which is not as difficult as it sounds. The idea is to first bucket your data based on a specified list of properties, then to flatten it into an appropriate form.

See the simple implementation below. For your data, the following will produce the desired output:

new DataPlucker(['age', 'country', 'occupation'], object.dataset).pluckAndFlatten()

Read the comments to better understand how it works.

class DataPlucker {
  // `properties` is a list of strings ['prop1', 'prop2', 'prop3']
  // `data` is of the form [obj1, obj2, obj3], where objN is of the form
  // `{ prop1: val1, prop2: val2, ... }`
  constructor (properties, data) {
    this.properties = properties
    this.data = data

    // Given the above list, we want `this.bucket` to be an object of the form
    // { prop1: {}, prop2: {}, prop3: {} }, we do this by mapping each property
    // specified in the list to an object of the form { prop1: {} }, before
    // aggregating into a single object via `Object.assign`.
    this.bucket = Object.assign.apply(null, properties.map(prop => {
      return { [prop]: {} }
    }))
  }

  pluckAndFlatten () {
    // `pluck` the data first, so that `this.bucket` is of the correct form.
    this._pluck()

    // Recall, `this.bucket` is an object with keys equal to the entries of
    // `this.properties`, the value associated with each key is another object.
    //
    // For example, suppose `this.bucket` is of the form
    // {
    //   prop1: {
    //     val1: { value: val1, count: num1, /* ... */ },
    //     val2: { value: val2, count: num2, /* ... */ },
    //   },
    //   /* ... */
    // }
    // Our goal is to transform each propN in the above object into another
    // object of the form (*)
    // { name: prop1, dataset: [ prop1.val1, prop1.val2, /* etc */ ] }
    // and to then collect these transformed objects into an array.
    //
    // Hence, for each key in `this.bucket` (alternatively: each property in
    // `this.properties`), map each key to an object an the form (*):
    return Object.keys(this.bucket).map(key => {
      // `key` will be equal to `prop1`, `prop2`, and so on. All we need to do
      // is grab the values of `this.bucket[key]`:
      return { name: key, dataset: Object.values(this.bucket[key]) }      
    })
  }

  _pluck () {
    // For each element in the data set and for each property in the property
    // list, either update the count in `this.bucket` or create a new object.
    this.data.forEach(datum => {
      this.properties.forEach(property => {
        this._updateOrCreate(property, datum[property])
      })
    })
  }

  _updateOrCreate (key, val) {
    if (this.bucket[key][val]) {
      this.bucket[key][val].count += 1
    } else {
      this.bucket[key][val] = { value: val, count: 1 }
    }
  }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.