|
| 1 | + |
| 2 | +var Plotly = require('../../../lib/index'); |
| 3 | + |
| 4 | +var createGraphDiv = require('../assets/create_graph_div'); |
| 5 | +var destroyGraphDiv = require('../assets/destroy_graph_div'); |
| 6 | + |
| 7 | +// Boilerplate taken from axes_test.js |
| 8 | +describe('tickmode proportional', function() { |
| 9 | + var gd; |
| 10 | + |
| 11 | + beforeEach(function() { |
| 12 | + gd = createGraphDiv(); |
| 13 | + }); |
| 14 | + |
| 15 | + afterEach(destroyGraphDiv); |
| 16 | + |
| 17 | + // These enums are `ticklen`s- it's how DOM analysis differentiates wrt major/minor |
| 18 | + // Passed as tickLen argument to specify major or minor tick config |
| 19 | + const MAJOR = 202, MINOR = 101; |
| 20 | + function generateTickConfig(tickLen){ |
| 21 | + // Intentionally configure to produce a single `(x|y)tick` class per tick |
| 22 | + // labels and tick marks each produce one, so one or the other |
| 23 | + standardConfig = {tickmode: 'proportional', ticklen: tickLen, showticklabels: false}; |
| 24 | + |
| 25 | + // Tick values will be random: |
| 26 | + var n = Math.floor(Math.random() * 100); |
| 27 | + tickVals = []; |
| 28 | + |
| 29 | + for(let i = 0; i <= n; i++){ |
| 30 | + intermediate = (Math.trunc(Math.random()*150) - 25) / 100; // Number between -.25 and 1.25 w/ 2 decimals max |
| 31 | + tickVals.push(Math.min(Math.max(intermediate, 0), 1)); // 2 decimal number between 0 and 1 w/ higher odds of 0 or 1 |
| 32 | + } |
| 33 | + standardConfig['tickvals'] = tickVals; |
| 34 | + return standardConfig; |
| 35 | + } |
| 36 | + |
| 37 | + function binaryToString(bin) { |
| 38 | + if (bin == 0b1) return "xMajor"; |
| 39 | + if (bin == 0b10) return "xMinor"; |
| 40 | + if (bin == 0b100) return "yMajor"; |
| 41 | + if (bin == 0b1000) return "yMinor"; |
| 42 | + } |
| 43 | + // the var `tickConfig` represents every possible configuration. It is in an int 0-15. |
| 44 | + // The binary is 0001, 0010, 0011, etc. IE every possible combination of 4 booleans. |
| 45 | + for(let tickConfig = 1; tickConfig <= 15; tickConfig++) { |
| 46 | + (function(tickConfig) { // tickConfig needs to be a closure otherwise it won't get the parameterized value |
| 47 | + it('maps proportional values to correct range values', function(done) { |
| 48 | + var xMajor = tickConfig & 0b0001; // check if xMajor position is 1 (True) |
| 49 | + var xMinor = tickConfig & 0b0010; |
| 50 | + var yMajor = tickConfig & 0b0100; |
| 51 | + var yMinor = tickConfig & 0b1000; |
| 52 | + xMajorConfig = xMajor ? generateTickConfig(MAJOR) : {}; // generate separate configs for each |
| 53 | + xMinorConfig = xMinor ? generateTickConfig(MAJOR) : {}; |
| 54 | + yMajorConfig = yMajor ? generateTickConfig(MINOR) : {}; |
| 55 | + yMinorConfig = yMinor ? generateTickConfig(MINOR) : {}; |
| 56 | + var configInfo = "" |
| 57 | + configInfo += xMajor ? "\n" + `xMajor: ${xMajorConfig['tickvals'].length} non-unique vals` : ""; |
| 58 | + configInfo += xMinor ? "\n" + `xMinor: ${xMinorConfig['tickvals'].length} non-unique vals` : ""; |
| 59 | + configInfo += yMajor ? "\n" + `yMajor: ${yMajorConfig['tickvals'].length} non-unique vals` : ""; |
| 60 | + configInfo += yMinor ? "\n" + `yMinor: ${yMinorConfig['tickvals'].length} non-unique vals` : ""; |
| 61 | + Plotly.newPlot(gd, { |
| 62 | + data: [{ |
| 63 | + x: [0, 1], |
| 64 | + y: [0, 1] |
| 65 | + }], |
| 66 | + layout: { |
| 67 | + width: 400, |
| 68 | + height: 400, |
| 69 | + margin: { t: 40, b: 40, l: 40, r: 40, }, |
| 70 | + xaxis: { |
| 71 | + range: [0, 10], |
| 72 | + ...xMajorConfig, // explode config into this key |
| 73 | + minor: xMinorConfig, // set config to this key |
| 74 | + }, |
| 75 | + yaxis: { // same as above |
| 76 | + autorange: true, |
| 77 | + ...yMajorConfig, |
| 78 | + minor: yMinorConfig, |
| 79 | + }, |
| 80 | + }}).then(function() { |
| 81 | + // This regex is for extracting geometric position of... should have used getBoundingClientRect() |
| 82 | + // |
| 83 | + // regex: `.source` converts to string, laid out this way to make for easier reading |
| 84 | + const funcName = "translate" + /\(/.source; // literally simplest way to regex '(' |
| 85 | + const integerPart = /\d+/.source; // numbers left of decimal |
| 86 | + const fractionalPart = /(?:\.\d+)?/.source; // decimal + numbers to right |
| 87 | + const floatNum = integerPart + fractionalPart; // all together |
| 88 | + const any = /.+/.source; |
| 89 | + const close = /\)/.source; |
| 90 | + const re = new RegExp(funcName + '(' + floatNum + '),' + any + close); // parens are capture not fn() |
| 91 | + for(let runNumber = 0b1; runNumber <= 0b1000; runNumber <<= 0b1) { // Check all ticks on all axes ☺ |
| 92 | + var elementName = ""; |
| 93 | + var targetConfig; |
| 94 | + var runInfo = "\n Checking: " + binaryToString(runNumber); |
| 95 | + if(runNumber & xMajor) { // ie. this run wants xMajor and xMajor was set in config above |
| 96 | + elementName = "xtick"; |
| 97 | + targetConfig = xMajorConfig; |
| 98 | + } else if (runNumber & xMinor) { |
| 99 | + elementName = "xtick"; |
| 100 | + targetConfig = xMinorConfig; |
| 101 | + } else if (runNumber & yMajor) { |
| 102 | + elementName = "ytick"; |
| 103 | + targetConfig = yMajorConfig; |
| 104 | + } else if (runNumber & yMinor) { |
| 105 | + elementName = "ytick"; |
| 106 | + targetConfig = yMinorConfig; |
| 107 | + } else continue; // This test isn't doing that type of test |
| 108 | + |
| 109 | + var tickElements = document.getElementsByClassName(elementName); |
| 110 | + var tickValsUnique = [...new Set(targetConfig['tickvals'])]; |
| 111 | + var expectedTickLen = String(targetConfig['ticklen']) |
| 112 | + |
| 113 | + if(tickElements.length < 2) return; // Can't test proportions with < 2 ticks (since no fixed reference) |
| 114 | + |
| 115 | + // Filter out major/minor and grab geometry |
| 116 | + transformVals = []; // "transform" ie the positional property |
| 117 | + for(let i = 0; i < tickElements.length; i++) { |
| 118 | + if(!tickElements[i].getAttribute("d").endsWith(expectedTickLen)) continue; |
| 119 | + var translate = tickElements[i].getAttribute("transform"); |
| 120 | + transformVals.push(Number(translate.match(re)[1])); |
| 121 | + } |
| 122 | + |
| 123 | + var debugInfo = "\n " + `tickElements: (${tickElements.length}) ${tickElements}` + "\n" + |
| 124 | + `tickVals/Unique: (${targetConfig['tickvals'].length}/${tickValsUnique.length}) {tickValsUnique}`; |
| 125 | + |
| 126 | + expect(transformVals.length).toBe(tickValsUnique.length, |
| 127 | + "test html vs tickvals failed" + runInfo + configInfo + debugInfo); |
| 128 | + |
| 129 | + |
| 130 | + // To test geometries without using fixed point or data values |
| 131 | + // We can check consistency of y = mx+b |
| 132 | + // if x = 0 then y = b, but we may not have a 0 valued x |
| 133 | + // m = (y1 - y2) / (x1 - x2) |
| 134 | + // b = y1 - mx1 |
| 135 | + y = transformVals; |
| 136 | + x = tickValsUnique; |
| 137 | + var m, b; |
| 138 | + var b_index = x.indexOf(0); |
| 139 | + |
| 140 | + m = (y[0] - y[1]) / (x[0] - x[1]); |
| 141 | + b = (b_index != -1) ? b = y[b_index] : y[0] - m*x[0]; |
| 142 | + |
| 143 | + for(let i = 0; i < x.length; i++) { |
| 144 | + expect(y[i]).toBeCloseTo(m * x[i] + b, 1, "y=mx+b test failed" + runInfo + configInfo + debugInfo) // not sure about toBeCloseTo |
| 145 | + } |
| 146 | + } |
| 147 | + }).then(done, done.fail); |
| 148 | + }); |
| 149 | + })(tickConfig); |
| 150 | + } |
| 151 | +}); |
0 commit comments