├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.html ├── index.js ├── package.json ├── src └── jnd.js └── test └── jnd-test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | parserOptions: 2 | sourceType: module 3 | 4 | env: 5 | node: true 6 | 7 | extends: 8 | "eslint:recommended" 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/ 3 | node_modules/ 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build/*.zip 2 | test/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Connor Gramazio 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the author nor the names of contributors may be used to 15 | endorse or promote products derived from this software without specific prior 16 | written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # d3-jnd 2 | 3 | To access the latest version of d3-jnd, grab it on 4 | [unpkg](https://unpkg.com/d3-jnd). Note that d3-jnd depends on 5 | [d3-color](https://github.com/d3/d3-color). 6 | 7 | This module extends D3 to support color just-noticeable difference (JND) 8 | research by Maureen Stone, Danielle Albers Szafir, and Vidya Setlur ([link](https://research.tableau.com/paper/engineering-model-color-difference-function-size)). 9 | 10 | ``` 11 | @inproceedings{stone-2014-emc, 12 | title={An engineering model for color difference as a function of size}, 13 | author={Stone, Maureen and Szafir, Danielle Albers and Setlur, Vidya}, 14 | booktitle={Color and Imaging Conference}, 15 | volume={2014}, 16 | number={2014}, 17 | pages={253--258}, 18 | year={2014}, 19 | organization={Society for Imaging Science and Technology} 20 | } 21 | ``` 22 | 23 | d3-jnd has two functions: ``d3.noticeablyDifferent()`` and ``d3.jndInterval``. 24 | Both take ``size`` and ``percent`` arguments, which alter how conservative JND 25 | estimations are. 26 | 27 | Note that the recommendations that are provided by this library are predicted 28 | JND intervals, and should be treated as guidelines rather than absolute truth. 29 | See the [d3-jnd website](https://connorgr.github.io/d3-jnd) for more information 30 | about how to manipulate d3-jnd function arguments appropriately. 31 | 32 | ```js 33 | var c1 = d3.lab('black'), 34 | c2 = d3.rgb('white'), 35 | legible = d3.noticeablyDifferent(c1, c2); // true 36 | 37 | var intervals = d3.jndInterval(0.5, 0.3); 38 | 39 | var strictlyLegible = d3.noticeablyDifferent(c1, c2, 0.95, 0.1); 40 | ``` 41 | 42 | # Installation 43 | 44 | After downloading the repo, run ``npm install``, which will install any 45 | dependencies. You can optionally install from npm opposed to cloning directly 46 | from GitHub. Make sure to load d3-cam02 after d3-color. 47 | 48 | **Dependencies:** [d3-color](https://github.com/d3/d3-color) 49 | 50 | ## API Reference 51 | 52 | # d3.noticeablyDifferent(color, color[, percent, size]) [<>](https://github.com/connorgr/d3-jnd/blob/master/src/jnd.js#L78 "Source")
53 | 54 | A function that returns true/false based on whether the two provided colors are 55 | noticeably different. Percent controls the percent of audience that will likely 56 | see a difference between the two colors quickly and easily. 57 | Size refers to the estimated visual angle of color area (smaller areas are 58 | harder to discriminate). 59 | 60 | # d3.jndInterval(percent, size) [<>](https://github.com/connorgr/d3-jnd/blob/master/src/jnd.js#L64 "Source")
61 | 62 | As d3.noticeablyDifferent except it returns the intervals along L*, a*, and b* 63 | channels of CIELAB that produce differences equal to "1 JND". 64 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 119 | 120 | 127 | 128 | 129 | 130 |
131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 |
154 | 177 | 178 |

179 | Just-Noticeable Differences 180 |

181 | Connor Gramazio 182 |
183 | 184 |
185 |
186 |

d3-jnd is a 187 | D3 extension 188 | that supports 189 | just-noticeable 190 | difference calculations for color based on work done by Maureen Stone, 191 | Danielle Albers Szafir, and Vidya Setlur [1].

192 | 193 |

d3-jnd is open sourced 194 | here. If you want to 195 | look at or use the compiled extension without downloading the source code, 196 | check it out on unpkg.

197 | 198 |

The just-noticeable difference functions in this library can be used to 199 | determine whether two colors are too similar to be easily differentiated. 200 | This is often critical for visualization color design because information 201 | legibility directly affects usability. 202 |

203 | 204 |

The two functions in d3-jnd are: 205 |

216 | 217 | The conservativeness of JND distance predictions can be manipulated by 218 | changing optional arguments that roughly correspond to what percentage 219 | of people can easily see a difference and how large the colored area 220 | is in terms of 221 | visual angle. 222 | The functions' defaults are respectively 50% and 0.1°. 223 |

224 | 225 |

Be sure to check that colors that meet the recommended L*, a*, and b* 226 | intervals still fall within displayable RGB color space when considering the 227 | intervals that jndLabInterval provides.

228 | 229 | 230 | Table of contents 231 |
    232 |
  1. 233 | Installation 234 |
  2. 235 |
  3. 236 | What is a JND? 237 |
      238 |
    1. Size affects discriminability
    2. 239 |
    3. Individual differences: percent of 240 | people that see a difference
    4. 241 |
    5. Err on the side of caution: display 242 | and environmental differences
    6. 243 |
    244 |
  4. 245 |
  5. Examples
  6. 246 |
  7. Further reading
  8. 247 |
248 |
249 | 250 |

Installation 📎

251 |
252 |

To access the latest version of d3-jnd, grab it on 253 | unpkg. Be sure to load d3-jnd 254 | after d3-color is loaded (be it through d3.v4.js, d3-color.js, or 255 | otherwise).

256 |

If installing and building from scratch, download the repo either through 257 | GitHub or through 258 | npm. 259 | Once you've downloaded it, just run npm install, which will 260 | install the required dependencies. 261 | Note that building requires 262 | node.js and 263 | npm.

264 | 265 |

D3 module dependencies: 266 | d3-color.

267 |
268 | 269 |

What is a JND? 📎

270 |
271 |

A just-noticeable difference (JND) is, "the amount [that a color] must be 272 | changed in order for a difference to be noticeable" 273 | [2]. 274 | Or, phrased another way, how different do two colors need to be in order for 275 | humans to recognize them as different? 276 | Given that this differentiability is directly dependent on human color vision, 277 | d3-jnd performs JND calculations using CIELAB, which is a perceptually 278 | modeled color space.

279 |

280 | 281 |
282 | Aside: CIELAB and perceptual color space 283 |

CIELAB defines color in terms of its perceptual lightness (L*), 284 | redness-to-greenness (a*), and blueness-to-yellowness (b*). 285 | d3-jnd does color conversion for you, so it's not critical to become an 286 | expert if you're just using the noticeablyDifferent function. 287 | If you're interested in learning more, check out Wikipedia's entry on 288 | CIELAB [3] or the 289 | d3-cam02 library to learn 290 | more about CIECAM02, which is an updated and more accurate perceptual 291 | color space. 292 | In this library we use CIELAB because it is what Stone, Szafir, and Setlur 293 | used in their experiments to derive regression-based JND formulas. 294 |

295 |
296 | 297 |

Size affects discriminability 📎

298 |

Unfortunately, determining whether two colors are discriminable is an 299 | involved process in part because it is harder to discriminate colors that 300 | are smaller in area. You can see this for yourself in the example below.

301 | 302 |
303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 |
315 | 316 |

Because size in this context is a physical property that can greatly vary 317 | with viewing conditions (e.g., dpi or viewing distance with 318 | phones compared to laptops), 319 | d3-jnd uses visual angle as a measurement of size rather than pixels. 320 | At arms length, the width of a thumb's knuckle is approximately 2° and an 321 | index fingernail is approximately 1°. 322 | d3-jnd uses a default colored area size assumption of 0.1°, which was 323 | selected as a conservatively small size without resulting in overly 324 | restrictive color selection; however, the default assumed size can be 325 | overridden to create more generous or strict JND distances. 326 |

327 | 328 |
329 | Aside: size 330 |

The physical size of visualizations can affect usability in a 331 | number of ways that include, but are not limited to, color legibility. 332 | If you're interested in 333 | learning more, we suggest Connor C. Gramazio, Karen B. Schloss, David H. 334 | Laidlaw's paper on size's affect on visualization search 335 | [4] and Maureen Stone's IEEE Viewpoint 336 | article [5]. 337 |

338 |
339 | 340 |

Individual differences: percent of people that see a difference 341 | 📎

342 |

Due to differences between people's perceptual acuity, the JND intervals 343 | that d3-jnd provides are predictions, not absolute truth. 344 | To accomodate this ambiguity, the d3-jnd functions' percentage argument can 345 | be used to incorporate what percentage of your audience should quickly and 346 | easily notice a difference in color. 347 | The default value used by d3-jnd is 50%, given that using 50% to refer to 348 | JNDs is standard practice in perception research; however, whether that 349 | threshold is sufficient for a visualization is contextually dependent. 350 | We encourage d3-jnd users to carefully consider whether they need to use 351 | a more conservative number (e.g., require JND intervals where an estimated 352 | 95% of users could reliably differentiate colors). 353 |

354 | 355 |
356 | Aside: atypical color vision 357 |

Another important individual difference to consider is people's different 358 | perceptions of color (e.g., red-green or blue-yellow color vision 359 | deficiencies). Given that a large portion of the population perceive color 360 | differently, consider whether the recommended L*, a*, or *b intervals you 361 | choose to use are sufficient. For example, a* modulates the 362 | redness-to-greeness of color and differences along only this channel might 363 | not be visible to all. 364 |

365 |
366 | 367 |

Err on the side of caution: display and environmental differences 368 | 📎

369 |

Although two colors might appear discriminable on your own monitor, 370 | consider whether that discriminability would be visible on your audience's 371 | displays. Developers and designers often use bright monitors that support 372 | wide color gamuts (e.g., any Apple device), which is not representative of 373 | displays used by the general population. Further, color can be aversely 374 | affected by environment (e.g., indoor vs. outdoor lighting). 375 | It is important to treat d3-jnd as only a prediction or recommendation of 376 | what may be noticeably different rather than an absolute truth.

377 |
378 | 379 |

Examples 📎

380 |
381 |

Interactive demo of how changing percent and size JND arguments can alter L*, a*, and b* intervals.

382 |
383 | 384 |

Further reading 📎

385 |
386 |

Citations

387 |
    388 |
  1. Stone, Szafir, Setlur. "An Engineering Model for Color Difference as a Function of Size," 22nd IS&T Color and Imaging Conference (Boston, November 3-7, 2014). 2014.
  2. 389 |
  3. Wikipedia's entry on just-noticeable differences
  4. 390 |
  5. Wikipedia's entry on CIELAB color space
  6. 391 |
  7. Connor C. Gramazio, Karen B. Schloss, David H. Laidlaw. "The relation between visualization size, grouping, and user performance," IEEE TVCG (Proceedings of Information Visualization). 2014.
  8. 392 |
  9. Maureen Stone. "In Color Perception, Size Matters," IEEE CG&A Viewpoints. 2012.
  10. 393 |
394 | 395 |

Other useful links

396 |
    397 |
  1. Colorgorical, a color palette 398 | tool by Connor Gramazio et al., that uses Stone, Szafir, and Setlur's JND 399 | function as one way to accomodate color discriminability
  2. 400 |
401 |
402 |
403 | 404 |

Provided free-to-use and open-sourced. See Github for specific licensing information.

405 | 406 | 407 | 408 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export {default as jndLabInterval, noticeablyDifferent} from "./src/jnd"; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3-jnd", 3 | "version": "0.1.3", 4 | "description": "Just-noticeable differences for colors in CEILAB color space", 5 | "keywords": [ 6 | "d3", 7 | "d3-module", 8 | "color", 9 | "jnd", 10 | "legibility", 11 | "discriminability", 12 | "just noticeable difference", 13 | "CIELAB", 14 | "Lab" 15 | ], 16 | "homepage": "https://github.com/connorgr/d3-jnd", 17 | "license": "BSD-3-Clause", 18 | "author": "Connor Gramazio (http://gramaz.io)", 19 | "main": "build/d3-jnd.js", 20 | "module": "index", 21 | "jsnext:main": "index", 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/connorgr/d3-jnd.git" 25 | }, 26 | "scripts": { 27 | "pretest": "rm -rf build && mkdir build && rollup --banner \"$(preamble)\" -f umd -g d3-color:d3 -n d3 -o build/d3-jnd.js -- index.js", 28 | "test": "tape 'test/**/*-test.js' && eslint index.js src test", 29 | "prepublish": "npm run test && uglifyjs --preamble \"$(preamble)\" build/d3-jnd.js -c -m -o build/d3-jnd.min.js", 30 | "postpublish": "git push && git push --tags && zip -j build/d3-jnd.zip -- LICENSE README.md build/d3-jnd.js build/d3-jnd.min.js" 31 | }, 32 | "dependencies": { 33 | "d3-color": "1" 34 | }, 35 | "devDependencies": { 36 | "eslint": "3", 37 | "package-preamble": "0.0", 38 | "rollup": "0.34", 39 | "tape": "4", 40 | "uglify-js": "2" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/connorgr/d3-jnd/issues" 44 | }, 45 | "directories": { 46 | "test": "test" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/jnd.js: -------------------------------------------------------------------------------- 1 | // Implementation based on Maureen Stone, Danielle Albers Szafir, and Vidya 2 | // Setlur's paper "An Engineering Model for Color Difference as a Function of 3 | // Size" presented at the Color Imaging Conference, and can be found online at 4 | // https://research.tableau.com/sites/default/files/2014CIC_48_Stone_v3.pdf 5 | // 6 | // Their paper examines target sizes (visual angle) ranging from 6 to 1/3 7 | // degree, so note that extrapolations outside that range contain additional 8 | // untesteed assumptions about color appearence. 9 | 10 | // To calculate whether colors are noticeably different, colors are translated 11 | // into CIELAB perceptual color space. Further, users must specifiy a visual 12 | // angle for how large the colored elements are (e.g., bars in a bar chart) 13 | // along their smallest dimension (e.g., width for 25px wide x 100px tall bars). 14 | 15 | // Variable definitions: 16 | // nd: noticeable difference 17 | // p: a threshold defined as the percentage of observers who see two colors 18 | // separated by a particular color space interval (e.g., along L*) as 19 | // different. 20 | // s: size, specified in degrees of visual angle 21 | 22 | //----------------------------------------. 23 | // PREDICTING DISCRIMINABILITY THRESHOLDS \___________________________________ 24 | //=============================================================================| 25 | // // p = V(s)*Delta_D + e (i.e., y=ax+b), where 26 | // s: size, 27 | // V(s) and D: vector values of L*, a*, b* 28 | // e: error term 29 | // Delta_D: a step in CIELAB space 30 | // V(s): a vector of three slopes, which differ along L*, a*, and b* 31 | // 32 | // Therefore, Delta_D = nd(p) = p / V(s) 33 | // 34 | // For calculating just noticeable differences (JND), we'll assume that p should 35 | // be fixed at 50%, which then leaves size as the only free variable for 36 | // calculating discriminability intervals along L*, a*, and b* color channels. 37 | // 38 | // ND(50, s) = C(50) + K(50)/s, where C and K are regression coefficients 39 | // 40 | // Stone et al. also provide a generalized formula that can support p and s both 41 | // as free variables based on additional regressions (see paper): 42 | // 43 | // ND(p,s) = p(A+B/s), where 44 | // s: size, 45 | // p: % of observers who see colors as different ([0,1]) 46 | // A and B: preset values that differ for each channel 47 | // 48 | import {lab} from "d3-color"; 49 | 50 | 51 | 52 | 53 | function nd(p,s) { 54 | var A = {l: 10.16, a: 10.68, b: 10.70}, 55 | B = {l: 1.50, a: 3.08, b: 5.74}; 56 | 57 | return { 58 | l: p * (A.l + B.l / s), 59 | a: p * (A.a + B.a / s), 60 | b: p * (A.b + B.b / s) 61 | }; 62 | } 63 | 64 | export default function jndLabInterval(p, s) { 65 | if(typeof s === "string") { 66 | if(s === "thin") s = 0.1; 67 | else if(s === "medium") s = 0.5; 68 | else if(s === "wide") s = 1.0; 69 | else s = 0.1; 70 | } 71 | if(typeof p === "string") { 72 | if(s === "conservative") p = 0.8; 73 | else p = 0.5; 74 | } 75 | return nd(p, s); 76 | } 77 | 78 | export function noticeablyDifferent(c1, c2, s, p) { 79 | if(arguments.length < 3) s = 0.1; 80 | if(arguments.length < 4) p = 0.5; 81 | 82 | var jnd = jndLabInterval(p, s); 83 | c1 = lab(c1); 84 | c2 = lab(c2); 85 | 86 | return (Math.abs(c1.l-c2.l) >= jnd.l) || (Math.abs(c1.a-c2.a) >= jnd.a) || (Math.abs(c1.b-c2.b) >= jnd.b); 87 | } 88 | -------------------------------------------------------------------------------- /test/jnd-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3_color = require("d3-color"), 3 | d3_jnd = require("../"); 4 | 5 | tape("noticeablyDifferent(…) returns correct responses", function(test) { 6 | var c1 = d3_color.lab(100, 0, 0), 7 | c2 = d3_color.lab(0,0,0); 8 | test.equal(d3_jnd.noticeablyDifferent(c1, c2), true); 9 | c2 = c1; 10 | test.equal(d3_jnd.noticeablyDifferent(c1, c2), false); 11 | test.end(); 12 | }); 13 | 14 | // There are no tests for jndInterval, because published interval values are for 15 | // Stone et al.'s ND(size) function rather than their generalized 16 | // ND(percent, size) function, which is used in d3-jnd. 17 | --------------------------------------------------------------------------------