├── .gitignore ├── README.md ├── bench ├── bench-dfs.js ├── bench-evil.js └── bench.js ├── license.md ├── package.json ├── snap.js ├── sort.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules/* 16 | *.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # snap-points-2d 2 | 3 | Runs iterative snap rounding on a set of 2D coordinates to produce a hierarchical level of detail for optimizing online rendering of huge 2D plots. 4 | 5 | # Install 6 | 7 | ``` 8 | npm i snap-points-2d 9 | ``` 10 | 11 | # API 12 | 13 | #### `{levels, ids, weights, points} = require('snap-points-2d')(points, bounds?)` 14 | 15 | Reorders the `points` hierarchically such that those which are drawn at the same pixel coordinate are grouped together. 16 | 17 | ##### Inputs 18 | * `points` is an input array of 2*n coordinate values. It is kept untouched. 19 | * `bounds` is an optional array of 4 bounding box values of the points. 20 | 21 | ##### Outputs 22 | * `points` is an output float64 array with reordered an normalized to `bounds` point values. 23 | * `ids` is an output uint32 array which gets the reordered index of the points. 24 | * `weights` is an output uint32 array of point weights (number of points at the same pixel), which can be used for transparent rendering. 25 | * `levels` is an array of LOD scales. Each record is an object with the following properties: 26 | * `pixelSize` the pixel size of this level of detail in data units 27 | * `offset` the offset of this lod within the output array 28 | * `count` the number of items in the lod 29 | 30 | # License 31 | (c) 2015 Mikola Lysenko. MIT License 32 | -------------------------------------------------------------------------------- /bench/bench-dfs.js: -------------------------------------------------------------------------------- 1 | var gaussRandom = require('gauss-random') 2 | var snapPoints = require('../snap-dfs') 3 | 4 | var NUM_POINTS = (process.argv[2])|0 5 | 6 | console.log('building qt for', NUM_POINTS, 'points') 7 | 8 | var points = new Float32Array(2*NUM_POINTS) 9 | var levelQT = new Float32Array(2*NUM_POINTS) 10 | var ids = new Int32Array(NUM_POINTS) 11 | 12 | for(var i=0; i<2*NUM_POINTS; ++i) { 13 | points[i] = gaussRandom() 14 | } 15 | 16 | var timeStart = Date.now() 17 | var levels = snapPoints(points, ids) 18 | console.log(Date.now() - timeStart) 19 | console.log(levels) 20 | -------------------------------------------------------------------------------- /bench/bench-evil.js: -------------------------------------------------------------------------------- 1 | var gaussRandom = require('gauss-random') 2 | var snapPoints = require('../snap-dfs') 3 | 4 | var NUM_POINTS = (process.argv[2])|0 5 | 6 | console.log('building qt for', NUM_POINTS, 'points') 7 | 8 | var points = new Float32Array(2*NUM_POINTS) 9 | var levelQT = new Float32Array(2*NUM_POINTS) 10 | var ids = new Int32Array(NUM_POINTS) 11 | 12 | for(var i=0; i<2*NUM_POINTS; ++i) { 13 | points[i] = i * Math.pow(2,-1024) 14 | } 15 | 16 | var timeStart = Date.now() 17 | var levels = snapPoints(points, levelQT, ids) 18 | console.log(Date.now() - timeStart) 19 | 20 | console.log(levels) 21 | -------------------------------------------------------------------------------- /bench/bench.js: -------------------------------------------------------------------------------- 1 | var gaussRandom = require('gauss-random') 2 | var snapPoints = require('../snap') 3 | 4 | var NUM_POINTS = (process.argv[2])|0 5 | 6 | console.log('building qt for', NUM_POINTS, 'points') 7 | 8 | var points = new Float32Array(2*NUM_POINTS) 9 | var levelQT = new Float32Array(2*NUM_POINTS) 10 | var ids = new Int32Array(NUM_POINTS) 11 | var weights = new Int32Array(NUM_POINTS) 12 | 13 | for(var i=0; i<2*NUM_POINTS; ++i) { 14 | points[i] = gaussRandom() 15 | } 16 | 17 | var timeStart = Date.now() 18 | var levels = snapPoints(points, levelQT, ids, weights) 19 | console.log(Date.now() - timeStart) 20 | 21 | console.log(levels) 22 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2013 Mikola Lysenko 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snap-points-2d", 3 | "version": "3.2.0", 4 | "description": "snap round 2d points", 5 | "main": "snap.js", 6 | "scripts": { 7 | "test": "tape test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/mikolalysenko/snap-points-2d.git" 12 | }, 13 | "keywords": [ 14 | "snap", 15 | "round", 16 | "iterated", 17 | "plot", 18 | "2d", 19 | "data", 20 | "vis" 21 | ], 22 | "author": "Mikola Lysenko", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/mikolalysenko/snap-points-2d/issues" 26 | }, 27 | "homepage": "https://github.com/mikolalysenko/snap-points-2d#readme", 28 | "devDependencies": { 29 | "almost-equal": "^1.0.0", 30 | "gauss-random": "^1.0.1", 31 | "math-float64-bits": "^1.0.1", 32 | "tape": "^4.2.0", 33 | "typedarray-pool": "^1.1.0" 34 | }, 35 | "dependencies": { 36 | "array-bounds": "^1.0.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /snap.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var getBounds = require('array-bounds') 4 | var sort = require('./sort') 5 | 6 | module.exports = snapPoints 7 | 8 | function snapPoints(srcPoints, bounds) { 9 | var n = srcPoints.length >>> 1 10 | if(n < 1) { 11 | return {levels: [], ids: null, weights: null, points: srcPoints} 12 | } 13 | 14 | var points = new Float64Array(n * 2) 15 | 16 | if (!bounds) bounds = [] 17 | 18 | var ids = new Uint32Array(n) 19 | var weights = new Uint32Array(n) 20 | var levels = new Uint8Array(n) 21 | 22 | for(var i=0; i < n; ++i) { 23 | ids[i] = i 24 | } 25 | 26 | // empty bounds or invalid bounds are considered as undefined and require recalc 27 | if (!bounds.length || bounds.length < 4 || bounds[0] >= bounds[2] || bounds[1] >= bounds[3]) { 28 | var b = getBounds(srcPoints, 2) 29 | 30 | if(b[0] === b[2]) { 31 | b[2] += 1 32 | } 33 | if(b[1] === b[3]) { 34 | b[3] += 1 35 | } 36 | 37 | bounds[0] = b[0] 38 | bounds[1] = b[1] 39 | bounds[2] = b[2] 40 | bounds[3] = b[3] 41 | } 42 | 43 | var lox = bounds[0] 44 | var loy = bounds[1] 45 | var hix = bounds[2] 46 | var hiy = bounds[3] 47 | 48 | // Calculate diameter 49 | var scaleX = 1.0 / (hix - lox) 50 | var scaleY = 1.0 / (hiy - loy) 51 | var diam = Math.max(hix - lox, hiy - loy) 52 | 53 | // normalize values 54 | for (var i = 0; i < n; i++) { 55 | points[2*i] = (srcPoints[2*i] - lox) * scaleX 56 | points[2*i+1] = (srcPoints[2*i+1] - loy) * scaleY 57 | } 58 | 59 | // Rearrange in quadtree order 60 | var ptr = 0 61 | snapRec(0, 0, 1, 0, n, 0) 62 | 63 | function snapRec(x, y, diam, start, end, level) { 64 | var diam_2 = diam * 0.5 65 | var offset = start + 1 66 | var count = end - start 67 | weights[ptr] = count 68 | levels[ptr++] = level 69 | for(var i=0; i<2; ++i) { 70 | for(var j=0; j<2; ++j) { 71 | var nx = x+i*diam_2 72 | var ny = y+j*diam_2 73 | var nextOffset = partition( 74 | points 75 | , ids 76 | , offset 77 | , end 78 | , nx, ny 79 | , nx+diam_2, ny+diam_2) 80 | if(nextOffset === offset) { 81 | continue 82 | } 83 | snapRec(nx, ny, diam_2, offset, nextOffset, level+1) 84 | offset = nextOffset 85 | } 86 | } 87 | } 88 | 89 | function partition(points, ids, start, end, lox, loy, hix, hiy) { 90 | var mid = start 91 | for(var i=start; i < end; ++i) { 92 | var x = points[2*i] 93 | var y = points[2*i+1] 94 | var s = ids[i] 95 | if(lox <= x && x <= hix && 96 | loy <= y && y <= hiy) { 97 | if(i === mid) { 98 | mid += 1 99 | } else { 100 | points[2*i] = points[2*mid] 101 | points[2*i+1] = points[2*mid+1] 102 | ids[i] = ids[mid] 103 | points[2*mid] = x 104 | points[2*mid+1] = y 105 | ids[mid] = s 106 | mid += 1 107 | } 108 | } 109 | } 110 | return mid 111 | } 112 | 113 | // sort by levels with accordance to x-coordinate 114 | var result = sort(levels, points, ids, weights, n) 115 | 116 | // form levels of details 117 | var lod = [] 118 | var lastLevel = 0 119 | var prevOffset = n 120 | for(var ptr=n-1; ptr>=0; --ptr) { 121 | var level = result.levels[ptr] 122 | if(level === lastLevel) { 123 | continue 124 | } 125 | 126 | lod.push({ 127 | pixelSize: diam * Math.pow(0.5, level), 128 | offset: ptr+1, 129 | count: prevOffset - (ptr+1) 130 | }) 131 | prevOffset = ptr+1 132 | 133 | lastLevel = level 134 | } 135 | 136 | lod.push({ 137 | pixelSize: diam * Math.pow(0.5, level+1), 138 | offset: 0, 139 | count: prevOffset 140 | }) 141 | 142 | result.levels = lod 143 | 144 | return result 145 | } 146 | -------------------------------------------------------------------------------- /sort.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = global.Float64Array ? nativeSort : sortLevels 4 | 5 | 6 | function nativeSort(levels, points, ids, weights, n) { 7 | // pack levels: uint8, x-coord: uint16 and id: uint32 to float64 8 | var packed = new Float64Array(n) 9 | var packedInt = new Uint32Array(packed.buffer) 10 | 11 | for (var i = 0; i < n; i++) { 12 | packedInt[i * 2] = i 13 | packedInt[i * 2 + 1] = (0x3ff00000 & (levels[i] << 20) | 0x0000ffff & ((1 - points[i * 2]) * 0xffff)) 14 | } 15 | 16 | // do native sort 17 | packed.sort() 18 | 19 | // unpack data back 20 | var sortedLevels = new Uint8Array(n) 21 | var sortedWeights = new Uint32Array(n) 22 | var sortedIds = new Uint32Array(n) 23 | var sortedPoints = new Float64Array(n * 2) 24 | for (var i = 0; i < n; i++) { 25 | var id = packedInt[(n - i - 1) * 2] 26 | sortedLevels[i] = levels[id] 27 | sortedWeights[i] = weights[id] 28 | sortedIds[i] = ids[id] 29 | sortedPoints[i * 2] = points[id * 2] 30 | sortedPoints[i * 2 + 1] = points[id * 2 + 1] 31 | } 32 | 33 | return { 34 | levels: sortedLevels, 35 | points: sortedPoints, 36 | ids: sortedIds, 37 | weights: sortedWeights 38 | } 39 | } 40 | 41 | 42 | var INSERT_SORT_CUTOFF = 32 43 | 44 | function sortLevels(data_levels, data_points, data_ids, data_weights, n0) { 45 | if (n0 <= 4*INSERT_SORT_CUTOFF) { 46 | insertionSort(0, n0 - 1, data_levels, data_points, data_ids, data_weights) 47 | } else { 48 | quickSort(0, n0 - 1, data_levels, data_points, data_ids, data_weights) 49 | } 50 | 51 | return { 52 | levels: data_levels, 53 | points: data_points, 54 | ids: data_ids, 55 | weights: data_weights 56 | } 57 | } 58 | 59 | function insertionSort(left, right, data_levels, data_points, data_ids, data_weights) { 60 | for(var i=left+1; i<=right; ++i) { 61 | var a_level = data_levels[i] 62 | var a_x = data_points[2*i] 63 | var a_y = data_points[2*i+1] 64 | var a_id = data_ids[i] 65 | var a_weight = data_weights[i] 66 | 67 | var j = i 68 | while(j > left) { 69 | var b_level = data_levels[j-1] 70 | var b_x = data_points[2*(j-1)] 71 | if(((b_level - a_level) || (a_x - b_x)) >= 0) { 72 | break 73 | } 74 | data_levels[j] = b_level 75 | data_points[2*j] = b_x 76 | data_points[2*j+1] = data_points[2*j-1] 77 | data_ids[j] = data_ids[j-1] 78 | data_weights[j] = data_weights[j-1] 79 | j -= 1 80 | } 81 | 82 | data_levels[j] = a_level 83 | data_points[2*j] = a_x 84 | data_points[2*j+1] = a_y 85 | data_ids[j] = a_id 86 | data_weights[j] = a_weight 87 | } 88 | } 89 | 90 | function swap(i, j, data_levels, data_points, data_ids, data_weights) { 91 | var a_level = data_levels[i] 92 | var a_x = data_points[2*i] 93 | var a_y = data_points[2*i+1] 94 | var a_id = data_ids[i] 95 | var a_weight = data_weights[i] 96 | 97 | data_levels[i] = data_levels[j] 98 | data_points[2*i] = data_points[2*j] 99 | data_points[2*i+1] = data_points[2*j+1] 100 | data_ids[i] = data_ids[j] 101 | data_weights[i] = data_weights[j] 102 | 103 | data_levels[j] = a_level 104 | data_points[2*j] = a_x 105 | data_points[2*j+1] = a_y 106 | data_ids[j] = a_id 107 | data_weights[j] = a_weight 108 | } 109 | 110 | function move(i, j, data_levels, data_points, data_ids, data_weights) { 111 | data_levels[i] = data_levels[j] 112 | data_points[2*i] = data_points[2*j] 113 | data_points[2*i+1] = data_points[2*j+1] 114 | data_ids[i] = data_ids[j] 115 | data_weights[i] = data_weights[j] 116 | } 117 | 118 | function rotate(i, j, k, data_levels, data_points, data_ids, data_weights) { 119 | var a_level = data_levels[i] 120 | var a_x = data_points[2*i] 121 | var a_y = data_points[2*i+1] 122 | var a_id = data_ids[i] 123 | var a_weight = data_weights[i] 124 | 125 | data_levels[i] = data_levels[j] 126 | data_points[2*i] = data_points[2*j] 127 | data_points[2*i+1] = data_points[2*j+1] 128 | data_ids[i] = data_ids[j] 129 | data_weights[i] = data_weights[j] 130 | 131 | data_levels[j] = data_levels[k] 132 | data_points[2*j] = data_points[2*k] 133 | data_points[2*j+1] = data_points[2*k+1] 134 | data_ids[j] = data_ids[k] 135 | data_weights[j] = data_weights[k] 136 | 137 | data_levels[k] = a_level 138 | data_points[2*k] = a_x 139 | data_points[2*k+1] = a_y 140 | data_ids[k] = a_id 141 | data_weights[k] = a_weight 142 | } 143 | 144 | function shufflePivot( 145 | i, j, 146 | a_level, a_x, a_y, a_id, a_weight, 147 | data_levels, data_points, data_ids, data_weights) { 148 | 149 | data_levels[i] = data_levels[j] 150 | data_points[2*i] = data_points[2*j] 151 | data_points[2*i+1] = data_points[2*j+1] 152 | data_ids[i] = data_ids[j] 153 | data_weights[i] = data_weights[j] 154 | 155 | data_levels[j] = a_level 156 | data_points[2*j] = a_x 157 | data_points[2*j+1] = a_y 158 | data_ids[j] = a_id 159 | data_weights[j] = a_weight 160 | } 161 | 162 | function compare(i, j, data_levels, data_points, data_ids) { 163 | return ((data_levels[i] - data_levels[j]) || 164 | (data_points[2*j] - data_points[2*i]) || 165 | (data_ids[i] - data_ids[j])) < 0 166 | } 167 | 168 | function comparePivot(i, level, x, y, id, data_levels, data_points, data_ids) { 169 | return ((level - data_levels[i]) || 170 | (data_points[2*i] - x) || 171 | (id - data_ids[i])) < 0 172 | } 173 | 174 | function quickSort(left, right, data_levels, data_points, data_ids, data_weights) { 175 | var sixth = (right - left + 1) / 6 | 0, 176 | index1 = left + sixth, 177 | index5 = right - sixth, 178 | index3 = left + right >> 1, 179 | index2 = index3 - sixth, 180 | index4 = index3 + sixth, 181 | el1 = index1, 182 | el2 = index2, 183 | el3 = index3, 184 | el4 = index4, 185 | el5 = index5, 186 | less = left + 1, 187 | great = right - 1, 188 | tmp = 0 189 | if(compare(el1, el2, data_levels, data_points, data_ids, data_weights)) { 190 | tmp = el1 191 | el1 = el2 192 | el2 = tmp 193 | } 194 | if(compare(el4, el5, data_levels, data_points, data_ids, data_weights)) { 195 | tmp = el4 196 | el4 = el5 197 | el5 = tmp 198 | } 199 | if(compare(el1, el3, data_levels, data_points, data_ids, data_weights)) { 200 | tmp = el1 201 | el1 = el3 202 | el3 = tmp 203 | } 204 | if(compare(el2, el3, data_levels, data_points, data_ids, data_weights)) { 205 | tmp = el2 206 | el2 = el3 207 | el3 = tmp 208 | } 209 | if(compare(el1, el4, data_levels, data_points, data_ids, data_weights)) { 210 | tmp = el1 211 | el1 = el4 212 | el4 = tmp 213 | } 214 | if(compare(el3, el4, data_levels, data_points, data_ids, data_weights)) { 215 | tmp = el3 216 | el3 = el4 217 | el4 = tmp 218 | } 219 | if(compare(el2, el5, data_levels, data_points, data_ids, data_weights)) { 220 | tmp = el2 221 | el2 = el5 222 | el5 = tmp 223 | } 224 | if(compare(el2, el3, data_levels, data_points, data_ids, data_weights)) { 225 | tmp = el2 226 | el2 = el3 227 | el3 = tmp 228 | } 229 | if(compare(el4, el5, data_levels, data_points, data_ids, data_weights)) { 230 | tmp = el4 231 | el4 = el5 232 | el5 = tmp 233 | } 234 | 235 | var pivot1_level = data_levels[el2] 236 | var pivot1_x = data_points[2*el2] 237 | var pivot1_y = data_points[2*el2+1] 238 | var pivot1_id = data_ids[el2] 239 | var pivot1_weight = data_weights[el2] 240 | 241 | var pivot2_level = data_levels[el4] 242 | var pivot2_x = data_points[2*el4] 243 | var pivot2_y = data_points[2*el4+1] 244 | var pivot2_id = data_ids[el4] 245 | var pivot2_weight = data_weights[el4] 246 | 247 | var ptr0 = el1 248 | var ptr2 = el3 249 | var ptr4 = el5 250 | var ptr5 = index1 251 | var ptr6 = index3 252 | var ptr7 = index5 253 | 254 | var level_x = data_levels[ptr0] 255 | var level_y = data_levels[ptr2] 256 | var level_z = data_levels[ptr4] 257 | data_levels[ptr5] = level_x 258 | data_levels[ptr6] = level_y 259 | data_levels[ptr7] = level_z 260 | 261 | for (var i1 = 0; i1 < 2; ++i1) { 262 | var x = data_points[2*ptr0+i1] 263 | var y = data_points[2*ptr2+i1] 264 | var z = data_points[2*ptr4+i1] 265 | data_points[2*ptr5+i1] = x 266 | data_points[2*ptr6+i1] = y 267 | data_points[2*ptr7+i1] = z 268 | } 269 | 270 | var id_x = data_ids[ptr0] 271 | var id_y = data_ids[ptr2] 272 | var id_z = data_ids[ptr4] 273 | data_ids[ptr5] = id_x 274 | data_ids[ptr6] = id_y 275 | data_ids[ptr7] = id_z 276 | 277 | var weight_x = data_weights[ptr0] 278 | var weight_y = data_weights[ptr2] 279 | var weight_z = data_weights[ptr4] 280 | data_weights[ptr5] = weight_x 281 | data_weights[ptr6] = weight_y 282 | data_weights[ptr7] = weight_z 283 | 284 | move(index2, left, data_levels, data_points, data_ids, data_weights) 285 | move(index4, right, data_levels, data_points, data_ids, data_weights) 286 | for (var k = less; k <= great; ++k) { 287 | if (comparePivot(k, 288 | pivot1_level, pivot1_x, pivot1_y, pivot1_id, 289 | data_levels, data_points, data_ids)) { 290 | if (k !== less) { 291 | swap(k, less, data_levels, data_points, data_ids, data_weights) 292 | } 293 | ++less; 294 | } else { 295 | if (!comparePivot(k, 296 | pivot2_level, pivot2_x, pivot2_y, pivot2_id, 297 | data_levels, data_points, data_ids)) { 298 | while (true) { 299 | if (!comparePivot(great, 300 | pivot2_level, pivot2_x, pivot2_y, pivot2_id, 301 | data_levels, data_points, data_ids)) { 302 | if (--great < k) { 303 | break; 304 | } 305 | continue; 306 | } else { 307 | if (comparePivot(great, 308 | pivot1_level, pivot1_x, pivot1_y, pivot1_id, 309 | data_levels, data_points, data_ids)) { 310 | rotate(k, less, great, data_levels, data_points, data_ids, data_weights) 311 | ++less; 312 | --great; 313 | } else { 314 | swap(k, great, data_levels, data_points, data_ids, data_weights) 315 | --great; 316 | } 317 | break; 318 | } 319 | } 320 | } 321 | } 322 | } 323 | shufflePivot(left, less-1, pivot1_level, pivot1_x, pivot1_y, pivot1_id, pivot1_weight, data_levels, data_points, data_ids, data_weights) 324 | shufflePivot(right, great+1, pivot2_level, pivot2_x, pivot2_y, pivot2_id, pivot2_weight, data_levels, data_points, data_ids, data_weights) 325 | if (less - 2 - left <= INSERT_SORT_CUTOFF) { 326 | insertionSort(left, less - 2, data_levels, data_points, data_ids, data_weights) 327 | } else { 328 | quickSort(left, less - 2, data_levels, data_points, data_ids, data_weights) 329 | } 330 | if (right - (great + 2) <= INSERT_SORT_CUTOFF) { 331 | insertionSort(great + 2, right, data_levels, data_points, data_ids, data_weights) 332 | } else { 333 | quickSort(great + 2, right, data_levels, data_points, data_ids, data_weights) 334 | } 335 | if (great - less <= INSERT_SORT_CUTOFF) { 336 | insertionSort(less, great, data_levels, data_points, data_ids, data_weights) 337 | } else { 338 | quickSort(less, great, data_levels, data_points, data_ids, data_weights) 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var t = require('tape') 4 | var snap = require('./snap') 5 | var approxEqual = require('almost-equal') 6 | 7 | 8 | t('snap-points-2d', t => { 9 | function verifySnap(srcPoints) { 10 | var numPoints = srcPoints.length>>>1 11 | var bounds = [] 12 | 13 | var {levels, ids, weights, points} = snap(srcPoints, bounds) 14 | var npoints = points 15 | 16 | var sx = bounds[0] 17 | var sy = bounds[1] 18 | var sw = bounds[2] - bounds[0] 19 | var sh = bounds[3] - bounds[1] 20 | 21 | for(var i=0; i < numPoints; ++i) { 22 | var id = ids[i] 23 | t.ok(approxEqual(sx + sw*npoints[2*i], srcPoints[2*id], approxEqual.FLT_EPSILON), 24 | 'id perm ok: ' + id + ' ' + srcPoints[2*id] + ' = ' + (sx + sw*npoints[2*i])) 25 | t.ok(approxEqual(sy + sh*npoints[2*i+1], srcPoints[2*id+1], approxEqual.FLT_EPSILON), 'id perm ok: ' + id + ' ' + srcPoints[2*id+1] + ' = ' + (sy + sh*npoints[2*i+1])) 26 | } 27 | 28 | t.equals(levels[levels.length-1].offset, 0, 'last item') 29 | t.equals(levels[0].offset+levels[0].count, numPoints, 'first item') 30 | 31 | for(var i=0; i < levels.length; ++i) { 32 | var s = levels[i] 33 | 34 | var r = s.pixelSize 35 | var offs = s.offset 36 | var count = s.count 37 | 38 | console.log('level=', i, r, offs, count) 39 | 40 | if(i > 0) { 41 | t.equals(offs+count, levels[i-1].offset, 'offset for ' + i) 42 | t.ok(r < levels[i-1].pixelSize, 'test scales ok') 43 | } 44 | k_loop: 45 | for(var k=offs-1; k>=0; --k) { 46 | var ax = npoints[2*k] 47 | var ay = npoints[2*k+1] 48 | 49 | var mind = Infinity 50 | 51 | for(var j=offs; j < offs+count; ++j) { 52 | var x = npoints[2*j] 53 | var y = npoints[2*j+1] 54 | 55 | mind = Math.min(mind, Math.max(Math.abs(ax-x), Math.abs(ay-y))) 56 | } 57 | 58 | t.ok(mind <= 2.0 * r, k + ':' + ax + ',' + ay + ' is not covered - closest pt = ' + mind) 59 | } 60 | } 61 | } 62 | 63 | verifySnap([ 64 | 1, 1, 65 | 2, 2, 66 | 3, 3, 67 | 4, 4, 68 | 5, 5 69 | ]) 70 | 71 | verifySnap([ 72 | 0,0, 73 | 0,0, 74 | 0,0, 75 | 0,0 76 | ]) 77 | 78 | verifySnap([ 79 | 1, 2, 80 | 2, 5, 81 | 3, 6, 82 | 4, -1 83 | ]) 84 | 85 | var pts = new Array(100) 86 | for(var i=0; i < 100; ++i) { 87 | pts[i] = Math.random() 88 | } 89 | verifySnap(pts) 90 | 91 | t.end() 92 | }) 93 | 94 | 95 | t('basics', t => { 96 | let {levels, points, ids, weights} = snap([1,1,2,2,3,3,4,4,5,5]) 97 | 98 | t.deepEqual(ids, [2, 4, 1, 3, 0]) 99 | t.deepEqual(points, [ 0.5, 0.5, 1, 1, 0.25, 0.25, 0.75, 0.75, 0, 0 ]) 100 | t.deepEqual(weights, [1, 1, 2, 2, 5]) 101 | t.deepEqual(levels, [ 102 | { count: 1, offset: 4, pixelSize: 2 }, 103 | { count: 2, offset: 2, pixelSize: 1 }, 104 | { count: 2, offset: 0, pixelSize: 0.5 } 105 | ]) 106 | 107 | t.end() 108 | }) 109 | 110 | 111 | t('no arguments', t => { 112 | var levels = snap([0,0, 1,1, 2,2]) 113 | 114 | t.end() 115 | }) 116 | 117 | t('larger bounds', t => { 118 | var pos = [0,0, 1,1, 2,2, 3,3, 4,4] 119 | 120 | var {levels} = snap(pos.slice(), [0,0,4,4]) 121 | t.deepEqual(levels, [ 122 | {pixelSize: 2, offset: 4, count: 1}, 123 | {pixelSize: 1, offset: 2, count: 2}, 124 | {pixelSize: 0.5, offset: 0, count: 2} 125 | ]) 126 | 127 | var {levels} = snap(pos.slice(), [0,0,40,40]) 128 | 129 | t.deepEqual(levels, [ 130 | {pixelSize: 20, offset: 4, count: 1}, 131 | {pixelSize: 10, offset: 3, count: 1}, 132 | {pixelSize: 5, offset: 2, count: 1}, 133 | {pixelSize: 2.5, offset: 1, count: 1}, 134 | {pixelSize: 1.25, offset: 0, count: 1} 135 | ]) 136 | 137 | t.end() 138 | }) 139 | 140 | 141 | t('performance', t => { 142 | let N = 1e6 143 | let points = new Float64Array(N) 144 | 145 | for (let i = 0; i < N; i++) { 146 | points[i] = Math.random() 147 | } 148 | 149 | console.time(1) 150 | snap(points) 151 | console.timeEnd(1) 152 | 153 | t.end() 154 | }) 155 | --------------------------------------------------------------------------------