1

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?

Hejhejhej123
  • 875
  • 4
  • 12
  • 25

3 Answers3

0

Supposing to have this input vector:

coordinates = [{x:1,y:2},{x:1,y:2},{x:10,y:1},{x:1,y:2},{x:10,y:1}]

and this vector of deltas:

deltas = [0.05,0.1,0.15,-0.05,-0.1,-0.15]

this code retrieves the duplicates and then add the deltas to them (if there are more than 6 duplicates it add 0):

coordinates.map(
  (e,i,v) => {
    ret = !e.d
      ? v.reduce(
          (acc,d,j) => {
            if (!d.d && j != i && d.x === e.x && d.y === e.y) {
              d.d=1
              acc.push(d)
            } 
            return acc
          },
          []
        )
      : []
    e.d =1
    return ret
  }
)
.filter(e => e.length)
.forEach(e => e.forEach((c,i) => c.x += deltas[i] || 0))

The 'd' property, added to track duplicates, shall be removed:

coordinates.forEach(e => delete e.d)

The result is:

coordinates
(5) [{…}, {…}, {…}, {…}, {…}]
0: {x: 1, y: 2}
1: {x: 1.05, y: 2}
2: {x: 10, y: 1}
3: {x: 1.1, y: 2}
4: {x: 10.05, y: 1}
length: 5
Giancarlo
  • 9
  • 2
0

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)
testing_22
  • 2,340
  • 1
  • 12
  • 28
0

I think it's worth separating out the code that handles the jitter from the code that updates the points using that jitter. That would give you the ability to try out different jitter functions (an Archimedean spiral would be nice) but still keep the main logic intact. Here is one version that does this:

const updatePoints = (jitterAlgo) => (points) => 
  points .reduce (({res, dups}, {x, y}) => {
    const key = `${x}:${y}`
    const matches = dups [key] || (dups [key] = [])
    const val = jitterAlgo (matches .length, {x, y})
    matches .push (val)
    res .push (val)
    return {res, dups}
  }, {res: [], dups: {}}) .res

const customJitter = (len, {x, y, ...rest}) =>
  len == 0 || len > 6
    ? {x, y, ... rest}
  : len < 3
    ? {x: x + len * .05, y, ... rest}
  : {x: x - len * .05, y, ... rest}

const points = [{"x": 1, "y": 6}, {"x": 3, "y": 5}, {"x": 3, "y": 5}, {"x": 5, "y": 4}, {"x": 3, "y": 5}, {"x": 3, "y": 5}, {"x": 3, "y": 5}, {"x": 1, "y": 9}, {"x": 5, "y": 4}, {"x": 5, "y": 4}, {"x": 3, "y": 5}, {"x": 2, "y": 9}, {"x": 3, "y": 5}, {"x": 3, "y": 5}, {"x": 5, "y": 4}, {"x": 2, "y": 8}]

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

I do think that your current logic, which only jitters in the x direction would likely not be as pleasant as one which does both x and y.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103