UNPKG

5.04 kBPlain TextView Raw
1import { head, indexOf, last, map, size } from '@antv/util';
2
3export const DEFAULT_Q = [1, 5, 2, 2.5, 4, 3];
4
5export const ALL_Q = [1, 5, 2, 2.5, 4, 3, 1.5, 7, 6, 8, 9];
6
7const eps = Number.EPSILON * 100;
8
9// https://stackoverflow.com/questions/4467539/javascript-modulo-gives-a-negative-result-for-negative-numbers
10function mod(n: number, m: number) {
11 return ((n % m) + m) % m;
12}
13
14function 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
25function 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
32function 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
38function densityMax(k: number, m: number) {
39 if (k >= m) {
40 return 2 - (k - 1) / (m - 1);
41 }
42 return 1;
43}
44
45function 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
50function 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
59function legibility() {
60 return 1;
61}
62
63/**
64 * An Extension of Wilkinson's Algorithm for Position Tick Labels on Axes
65 * https://www.yuque.com/preview/yuque/0/2019/pdf/185317/1546999150858-45c3b9c2-4e86-4223-bf1a-8a732e8195ed.pdf
66 * @param dmin 最小值
67 * @param dmax 最大值
68 * @param m tick个数
69 * @param onlyLoose 是否允许扩展min、max,不绝对强制,例如[3, 97]
70 * @param Q nice numbers集合
71 * @param w 四个优化组件的权重
72 */
73export 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 // 异常数据情况下,直接返回,防止 oom
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}