1
votes

My Goal: I'm trying to create a pie chart, Every 'slice' will associate with a label. There will be multiple pie charts on the page, and each pie might have a different number of labels, but I no matter what, I want each label to have its own color, regardless of how many total labels there per pie.

Example, Pie one has Label 1, 2, 3, 4, and Pie two has Label 1, 3, 4. Pie one colors will be, red, blue, green, purple, and Pie 2 slices should be red, green, purple.

My Methodology: According to d3 documentation, I'm lead to believe that the following configuration would work.

let color = d3.scaleOrdinal()
    .range(['red','blue','green','purple'])
    .domain('Label 1','Label 2', 'Label 3','Label 4')

Label 3 will ALWAYS return green if I

console.log(color('Label 3')

Issue:

Now, I scaled it up, and used dummy data (the data object will be bigger). Instead of manually typing the domain, I created an Array. In order to account for difference in label possibilities, I 'hashed' each value to a larger index (between 0-10000), same for colors.

My theory is this, if I have an array of 10k, and Label 1 is in index 9015, I would expect that running color('Label 1') would return purple because (9015 % 4) = 3 which means index 9015 in the colors array should be purple (which I verified with console.log).

Instead it returns blue, and I do not understand why.

Below is my source code

// Create dummy data
var data = {
    "Label 1": {count: "1"},
    "Label 2": {count: "5"},
    "Label 3": {count: "5"},
    "Label 4": {count: "39"},
    "Label 5": {count: "32"},
    "Label 6": {count: "5"},
    "Label 7": {count: "7"},
    "Label 8": {count: "1"}
}

// This will create an Array of 10k colors looping the base 4 colors
// example: ['red', 'blue', 'green', 'purple','red', 'blue', 'green'...]
let availableColors = createColorArray() 

// An empty array of 10k, Labels will get a new index, and be placed here.
let hash_array = new Array(10000)
hash_array.fill("")

var color = d3.scaleOrdinal()
    .range(availableColors)
    .domain(hash_array)
Object.keys(data).forEach(element => {

    // Map Hash Stuff
    let hash = computeHash(element)
    hash_array[hash] = element;
    console.log(element + ' = ' + hash)
})

console.log("======================================================================")
Object.keys(data).forEach(element => {
    console.log(element + ' = ' + color(element) + " | Color at index: " + availableColors[computeHash(element)])
})

My output:

Label 1 = blue | Color at index: purple
Label 2 = green | Color at index: green
Label 3 = purple | Color at index: blue
Label 4 = red | Color at index: red
Label 5 = blue | Color at index: purple
Label 6 = green | Color at index: green
Label 7 = purple | Color at index: blue
Label 8 = red | Color at index: red

My Expected output I expect the color returned from color() to be equal to the color found at the index, of where that label is.

Label 1 = purple| Color at index: purple
Label 2 = green | Color at index: green
Label 3 = blue| Color at index: blue
Label 4 = red | Color at index: red
Label 5 = purple| Color at index: purple
Label 6 = green | Color at index: green
Label 7 = blue| Color at index: blue
Label 8 = red | Color at index: red
1

1 Answers

2
votes

There is a misunderstanding regarding how an ordinal scale works in D3. The main point is this: domain values must be unique.

Internally, D3 ordinal scale uses a JavaScript Map for creating the domain/range table. While you can use an empty string ("") as a Map key, the result of using several empty strings is that all the subsequent ones will be ignored.

As the API explains (emphasis mine):

Domain values are stored internally in a map from stringified value to index; the resulting index is then used to retrieve a value from the range. Thus, an ordinal scale’s values must be coercible to a string, and the stringified version of the domain value uniquely identifies the corresponding range value.

Have a look at this basic demo (paying attention to the empty strings in the domain array): according to your logic, the returned values for Label 1 (third position, index 2) and Label 2 (last position, index 7) should be green and purple (3rd and 8th positions on the range array)

const scale = d3.scaleOrdinal()
  .range(['red', 'blue', 'green', 'purple', 'red', 'blue', 'green', 'purple'])
  .domain(['', '', 'Label 1', '', '', '', '', 'Label 2']);

console.log(scale('Label 1'));
console.log(scale('Label 2'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

As you can clearly see, that's not the result. What is happening here is that the first empty string is associated to red, as expected. However, the second empty string does not get the next range value (blue), and the same happens to all other empty strings: they are ignored. So, the next unique value, Label 1, gets the next range value (blue), while the unique value after that (Label 2) gets the subsequent range value (green), and so on.

Finally, regarding how to fix your approach: for avoiding the classic XY problem, please post a new question (do not edit this one) with your desired outcome, so we can help you with that.