1 | import { head, indexOf, last, map, size } from '@antv/util';
|
2 |
|
3 | export const DEFAULT_Q = [1, 5, 2, 2.5, 4, 3];
|
4 |
|
5 | export const ALL_Q = [1, 5, 2, 2.5, 4, 3, 1.5, 7, 6, 8, 9];
|
6 |
|
7 | const eps = Number.EPSILON * 100;
|
8 |
|
9 |
|
10 | function mod(n: number, m: number) {
|
11 | return ((n % m) + m) % m;
|
12 | }
|
13 |
|
14 | function simplicity(q: number, Q: number[], j: number, lmin: number, lmax: number, lstep: number) {
|
15 | const n = size(Q);
|
16 | const i = indexOf(Q, q);
|
17 | let v = 0;
|
18 | const m = mod(lmin, lstep);
|
19 | if ((m < eps || lstep - m < eps) && lmin <= 0 && lmax >= 0) {
|
20 | v = 1;
|
21 | }
|
22 | return 1 - i / (n - 1) - j + v;
|
23 | }
|
24 |
|
25 | function simplicityMax(q: number, Q: number[], j: number) {
|
26 | const n = size(Q);
|
27 | const i = indexOf(Q, q);
|
28 | const v = 1;
|
29 | return 1 - i / (n - 1) - j + v;
|
30 | }
|
31 |
|
32 | function density(k: number, m: number, dmin: number, dmax: number, lmin: number, lmax: number) {
|
33 | const r = (k - 1) / (lmax - lmin);
|
34 | const rt = (m - 1) / (Math.max(lmax, dmax) - Math.min(dmin, lmin));
|
35 | return 2 - Math.max(r / rt, rt / r);
|
36 | }
|
37 |
|
38 | function densityMax(k: number, m: number) {
|
39 | if (k >= m) {
|
40 | return 2 - (k - 1) / (m - 1);
|
41 | }
|
42 | return 1;
|
43 | }
|
44 |
|
45 | function coverage(dmin: number, dmax: number, lmin: number, lmax: number) {
|
46 | const range = dmax - dmin;
|
47 | return 1 - (0.5 * (Math.pow(dmax - lmax, 2) + Math.pow(dmin - lmin, 2))) / Math.pow(0.1 * range, 2);
|
48 | }
|
49 |
|
50 | function coverageMax(dmin: number, dmax: number, span: number) {
|
51 | const range = dmax - dmin;
|
52 | if (span > range) {
|
53 | const half = (span - range) / 2;
|
54 | return 1 - Math.pow(half, 2) / Math.pow(0.1 * range, 2);
|
55 | }
|
56 | return 1;
|
57 | }
|
58 |
|
59 | function legibility() {
|
60 | return 1;
|
61 | }
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 | export default function extended(
|
74 | dmin: number,
|
75 | dmax: number,
|
76 | m: number = 5,
|
77 | onlyLoose: boolean = true,
|
78 | Q: number[] = DEFAULT_Q,
|
79 | w: [number, number, number, number] = [0.25, 0.2, 0.5, 0.05]
|
80 | ): { min: number; max: number; ticks: number[] } {
|
81 |
|
82 | if (typeof dmin !== 'number' || typeof dmax !== 'number') {
|
83 | return {
|
84 | min: 0,
|
85 | max: 0,
|
86 | ticks: [],
|
87 | };
|
88 | }
|
89 |
|
90 | if (dmin === dmax || m === 1) {
|
91 | return {
|
92 | min: dmin,
|
93 | max: dmax,
|
94 | ticks: [dmin],
|
95 | };
|
96 | }
|
97 |
|
98 | const best = {
|
99 | score: -2,
|
100 | lmin: 0,
|
101 | lmax: 0,
|
102 | lstep: 0,
|
103 | };
|
104 | let j = 1;
|
105 | while (j < Infinity) {
|
106 | for (const q of Q) {
|
107 | const sm = simplicityMax(q, Q, j);
|
108 | if (Number.isNaN(sm)) {
|
109 | throw new Error('NaN');
|
110 | }
|
111 | if (w[0] * sm + w[1] + w[2] + w[3] < best.score) {
|
112 | j = Infinity;
|
113 | break;
|
114 | }
|
115 | let k = 2;
|
116 | while (k < Infinity) {
|
117 | const dm = densityMax(k, m);
|
118 | if (w[0] * sm + w[1] + w[2] * dm + w[3] < best.score) {
|
119 | break;
|
120 | }
|
121 |
|
122 | const delta = (dmax - dmin) / (k + 1) / j / q;
|
123 | let z = Math.ceil(Math.log10(delta));
|
124 |
|
125 | while (z < Infinity) {
|
126 | const step = j * q * Math.pow(10, z);
|
127 | const cm = coverageMax(dmin, dmax, step * (k - 1));
|
128 |
|
129 | if (w[0] * sm + w[1] * cm + w[2] * dm + w[3] < best.score) {
|
130 | break;
|
131 | }
|
132 |
|
133 | const minStart = Math.floor(dmax / step) * j - (k - 1) * j;
|
134 | const maxStart = Math.ceil(dmin / step) * j;
|
135 |
|
136 | if (minStart > maxStart) {
|
137 | z = z + 1;
|
138 | continue;
|
139 | }
|
140 | for (let start = minStart; start <= maxStart; start = start + 1) {
|
141 | const lmin = start * (step / j);
|
142 | const lmax = lmin + step * (k - 1);
|
143 | const lstep = step;
|
144 |
|
145 | const s = simplicity(q, Q, j, lmin, lmax, lstep);
|
146 | const c = coverage(dmin, dmax, lmin, lmax);
|
147 | const g = density(k, m, dmin, dmax, lmin, lmax);
|
148 | const l = legibility();
|
149 |
|
150 | const score = w[0] * s + w[1] * c + w[2] * g + w[3] * l;
|
151 | if (score > best.score && (!onlyLoose || (lmin <= dmin && lmax >= dmax))) {
|
152 | best.lmin = lmin;
|
153 | best.lmax = lmax;
|
154 | best.lstep = lstep;
|
155 | best.score = score;
|
156 | }
|
157 | }
|
158 | z = z + 1;
|
159 | }
|
160 | k = k + 1;
|
161 | }
|
162 | }
|
163 | j = j + 1;
|
164 | }
|
165 |
|
166 | const toFixed = Number.isInteger(best.lstep) ? 0 : Math.ceil(Math.abs(Math.log10(best.lstep)));
|
167 | const range = [];
|
168 | for (let tick = best.lmin; tick <= best.lmax; tick += best.lstep) {
|
169 | range.push(tick);
|
170 | }
|
171 | const ticks = toFixed ? map(range, (x: number) => Number.parseFloat(x.toFixed(toFixed))) : range;
|
172 |
|
173 | return {
|
174 | min: Math.min(dmin, head(ticks)),
|
175 | max: Math.max(dmax, last(ticks)),
|
176 | ticks,
|
177 | };
|
178 | }
|