javascriptjitter

Add and reduce on duplicated values


I have a scatter chart (using apexchart) and I am trying to prepare my data (objects in an array) before adding it to the chart.

My problem is that I often have some data that can have the exact same values on the x and y axis. This means that they will overlap eachother and only one item will be shown.

I first added a simple solution by using Math.random() to each duplicated value, but this means that they will be repositioning slightly for each time the chart is updated, which will make the users confused.

So what I want to achieve is to add the same "jitter"/changes for each items all the time. So my thought is to do it like this:

       //1. The first three duplicates should add 0.05 to the x value. So it will say:
        
    item1.x = 10 //original value
    item2.x = 10.05
    item3.x = 10.1
    item4.x = 10.15
        
    //2. Then, If more duplicates than 3, the next 3 should reduce 0.05 of the original value:
    item5.x = 9.95
    item6.x = 9.90
    item7.x = 9.85

//If more than 7 items with the same value, then just use the original value.

How could be the best way to achieve this? Right now I have the duplicated values and I can add "jitter", but how can I make it so it keeps count and do as I want above?

I managed to filter out the duplicates, but then I got stuck:

 const duplicates = AllItems.filter((val, i, self) => self.findIndex(item => item.x == val.x && item.y == val.y) != i);

  

Any ideas?


Solution

  • Consider using Map built-in. It performs better in this case, since there are frequent additions and removals of key-value pairs.

    This said, it's worth mentioning it acts like a hash table implementation. The idea is to keep track of the number of times a given x occurs by adding one to its count each time you find one occurrence. For the points example in the snippet, consider the following Map table:

    ╔═══════╦═══════════════════════════════╗
    ║   x   ║ 1  2  3  4  5  6  7  8  9  10 ║
    ╠═══════╬═══════════════════════════════╣
    ║ count ║ 5  1  1  1  7  1  1  1  1  10 ║
    ╚═══════╩═══════════════════════════════╝
    

    Note that the y value of points whose x is 1 won't change because its count is 5. Whereas 5 and 10 will, due to their count being greater or equal than MIN_TIMES (7). The logic follows:

    1. Construct the Map in order to store how many times a given x point repeats its y coordinate.
    2. Store each x only those that happens more or equal than MIN_TIMES in array
    3. At the same time, change count to 0 to start counting
    4. Map each point in the points array and convert those who are in xRepeatedAtLeastMinTimes to increment or decrement, based on your logic.

    const points=[{x:1,y:1},{x:1,y:1},{x:1,y:1},{x:1,y:1},{x:1,y:1},{x:2,y:2},{x:3,y:3},{x:4,y:4},{x:5,y:5},{x:5,y:5},{x:5,y:5},{x:5,y:5},{x:5,y:5},{x:5,y:5},{x:5,y:5},{x:6,y:6},{x:7,y:7},{x:8,y:8},{x:9,y:9},{x:10,y:10},{x:10,y:10},{x:10,y:10},{x:10,y:10},{x:10,y:10},{x:10,y:10},{x:10,y:10},{x:10,y:10},{x:10,y:10},{x:10,y:10}];
    
    const m = new Map(),
        xRepeatedAtLeastMinTimes = new Array(),
        INTERVAL = 0.05,
        MIN_TIMES = 7
    
    // Construct map
    points.forEach(point => m.set(point.x, m.has(point.x) ? (m.get(point.x) + 1) : 1))
    
    // Get x coordinates that happens at least MIN_TIMES and change its count to 0
    for (let [x, count] of m.entries()) 
       count >= MIN_TIMES && xRepeatedAtLeastMinTimes.push(x), m.set(x, 0)
    
    // Map each point and check condition based on count stored on Map
    let result = points.map(point => {
        let x = point.x
        if (xRepeatedAtLeastMinTimes.includes(x)) {
            let count = m.get(x)
            m.set(x, count + 1)
            return count === 0 ? point :
                   count <= 3 ? { ...point, x: x + count*INTERVAL } : 
                   count <= 6 ? { ...point, x: x - (count - 3)*INTERVAL } : point
        }
        return point
    })
    
    console.log(result)