/
renderUtils.ts
132 lines (114 loc) · 4.47 KB
/
renderUtils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import * as _ from 'lodash';
import { Interval, Ticks, TickFormat } from './interfaces';
export interface IndexBounds {
firstIndex: number;
lastIndex: number;
}
export type ValueAccessor<T> = string | ((value: T) => number);
function adjustBounds(firstIndex: number, lastIndex: number, dataLength: number): IndexBounds {
if (firstIndex === dataLength || lastIndex === 0) {
// No data is visible!
return { firstIndex, lastIndex };
} else {
// We want to include the previous and next data points so that e.g. lines drawn across the canvas
// boundary still have somewhere to go.
return {
firstIndex: Math.max(0, firstIndex - 1),
lastIndex: Math.min(dataLength, lastIndex + 1)
};
}
}
// This is cause sortedIndexBy prefers to have the same shape for the array items and the searched thing. We don't
// know what that shape is, so we have a sentinel + accompanying function to figure out when it's asking for this value.
type BoundSentinel = { __boundSentinelBrand: string };
const LOWER_BOUND_SENTINEL: BoundSentinel = (() => {}) as any;
const UPPER_BOUND_SENTINEL: BoundSentinel = (() => {}) as any;
// Assumption: data is sorted by `xValuePath` acending.
export function getIndexBoundsForPointData<T>(data: T[], xValueBounds: Interval, xValueAccessor: ValueAccessor<T>): IndexBounds {
let lowerBound;
let upperBound;
let accessor;
if (_.isString(xValueAccessor)) {
lowerBound = _.set({}, xValueAccessor, xValueBounds.min);
upperBound = _.set({}, xValueAccessor, xValueBounds.max);
accessor = xValueAccessor;
} else {
lowerBound = LOWER_BOUND_SENTINEL;
upperBound = UPPER_BOUND_SENTINEL;
accessor = (value: T | BoundSentinel) => {
if (value === LOWER_BOUND_SENTINEL) {
return xValueBounds.min;
} else if (value === UPPER_BOUND_SENTINEL) {
return xValueBounds.max;
} else {
return xValueAccessor(value as T);
}
};
}
const firstIndex = _.sortedIndexBy(data, lowerBound, accessor);
const lastIndex = _.sortedLastIndexBy(data, upperBound, accessor);
return adjustBounds(firstIndex, lastIndex, data.length);
}
// Assumption: data is sorted by `minXValuePath` ascending.
export function getIndexBoundsForSpanData<T>(data: T[], xValueBounds: Interval, minXValueAccessor: ValueAccessor<T>, maxXValueAccessor: ValueAccessor<T>): IndexBounds {
let upperBound;
let upperBoundAccessor;
// Note that this purposely mixes the min accessor/max value. Think about it.
if (_.isString(minXValueAccessor)) {
upperBound = _.set({}, minXValueAccessor, xValueBounds.max);
upperBoundAccessor = minXValueAccessor;
} else {
upperBound = UPPER_BOUND_SENTINEL;
upperBoundAccessor = (value: T | BoundSentinel) => {
if (value === UPPER_BOUND_SENTINEL) {
return xValueBounds.max;
} else {
return minXValueAccessor(value as T);
}
};
}
const lowerBoundAccessor = _.isString(maxXValueAccessor)
? (value: T) => _.get(value, maxXValueAccessor)
: maxXValueAccessor;
// Also note that this is a loose bound -- there could be spans that start later and end earlier such that
// they don't actually fit inside the bounds, but this still saves us work in the end.
const lastIndex = _.sortedLastIndexBy(data, upperBound, upperBoundAccessor);
let firstIndex;
for (firstIndex = 0; firstIndex < lastIndex; ++firstIndex) {
if (lowerBoundAccessor(data[firstIndex]) >= xValueBounds.min) {
break;
}
}
return adjustBounds(firstIndex, lastIndex, data.length);
}
const DEFAULT_TICK_AMOUNT = 5;
export function computeTicks(scale: any, ticks?: Ticks, tickFormat?: TickFormat) {
let outputTicks: number[];
if (ticks) {
if (_.isFunction(ticks)) {
const [ min, max ] = scale.domain();
const maybeOutputTicks = ticks({ min, max });
if (_.isNumber(maybeOutputTicks)) {
outputTicks = scale.ticks(maybeOutputTicks);
} else {
outputTicks = maybeOutputTicks;
}
} else if (_.isArray<number>(ticks)) {
outputTicks = ticks;
} else if (_.isNumber(ticks)) {
outputTicks = scale.ticks(ticks);
} else {
throw new Error('ticks must be a function, array or number');
}
} else {
outputTicks = scale.ticks(DEFAULT_TICK_AMOUNT);
}
let format: Function;
if (_.isFunction(tickFormat)) {
format = tickFormat;
} else {
const tickCount = _.isNumber(ticks) ? ticks : DEFAULT_TICK_AMOUNT;
format = scale.tickFormat(tickCount, tickFormat);
}
return { ticks: outputTicks, format };
}