forked from manifoldmarkets/manifold
-
Notifications
You must be signed in to change notification settings - Fork 0
/
chart.ts
133 lines (113 loc) · 4.07 KB
/
chart.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
133
import { ScaleContinuousNumeric, ScaleTime } from 'd3-scale'
import { Dispatch, SetStateAction } from 'react'
import { removeUndefinedProps } from './util/object'
import { first, last, mapValues, meanBy } from 'lodash'
export type Point<X, Y, T = unknown> = { x: X; y: Y; obj?: T }
export type HistoryPoint<T = unknown> = Point<number, number, T>
export type DistributionPoint<T = unknown> = Point<number, number, T>
export type ValueKind = 'Ṁ' | 'percent' | 'amount'
/** [x, [y0, y1, ...]] */
export type MultiSerializedPoints = { [answerId: string]: [number, number][] }
/** [x, y, obj] */
export type SerializedPoint<T = unknown> =
| Readonly<[number, number]>
| Readonly<[number, number, T | undefined]>
export const unserializePoints = <T>(points: SerializedPoint<T>[]) => {
return points.map(([x, y, obj]) => removeUndefinedProps({ x, y, obj }))
}
export const unserializeMultiPoints = (data: MultiSerializedPoints) => {
return mapValues(data, (points) => points.map(([x, y]) => ({ x, y })))
}
export const serializeMultiPoints = (data: {
[answerId: string]: HistoryPoint[]
}) => {
return mapValues(data, (points) =>
points.map(({ x, y }) => [x, y] as [number, number])
)
}
export type viewScale = {
viewXScale: ScaleTime<number, number, never> | undefined
setViewXScale: Dispatch<
SetStateAction<ScaleTime<number, number, never> | undefined>
>
viewYScale: ScaleContinuousNumeric<number, number, never> | undefined
setViewYScale: Dispatch<
SetStateAction<ScaleContinuousNumeric<number, number, never> | undefined>
>
}
export type AxisConstraints = {
min?: number
max?: number
minExtent?: number
}
export const maxMinBin = <P extends HistoryPoint>(
points: P[],
bins: number
) => {
if (points.length < 2 || bins <= 0) return points
const min = points[0].x
const max = points[points.length - 1].x
const binWidth = Math.ceil((max - min) / bins)
// for each bin, get the max, min, and median in that bin
const result = []
let lastInBin = points[0]
for (let i = 0; i < bins; i++) {
const binStart = min + i * binWidth
const binEnd = binStart + binWidth
const binPoints = points.filter((p) => p.x >= binStart && p.x < binEnd)
if (binPoints.length === 0) {
// insert a synthetic point at the start of the bin to prevent long diagonal lines
result.push({ ...lastInBin, x: binEnd })
} else if (binPoints.length <= 3) {
lastInBin = binPoints[binPoints.length - 1]
result.push(...binPoints)
} else {
lastInBin = binPoints[binPoints.length - 1]
binPoints.sort((a, b) => a.y - b.y)
const min = binPoints[0]
const max = binPoints[binPoints.length - 1]
const median = binPoints[Math.floor(binPoints.length / 2)]
result.push(...[min, max, median].sort((a, b) => a.x - b.x))
}
}
return result
}
// compresses points within a visible range, so as you zoom there's more detail.
export const compressPoints = <P extends HistoryPoint>(
points: P[],
min: number,
max: number
) => {
// add buffer of 100 points on each side for nice panning.
const smallIndex = Math.max(points.findIndex((p) => p.x >= min) - 100, 0)
const bigIndex = Math.min(
points.findIndex((p) => p.x >= max) + 100,
points.length
)
const toCompress = points.slice(smallIndex, bigIndex)
if (toCompress.length < 1500) {
return { points: toCompress, isCompressed: false }
}
return { points: maxMinBin(toCompress, 500), isCompressed: true }
}
export function binAvg<P extends HistoryPoint>(sorted: P[], limit = 100) {
const length = sorted.length
if (length <= limit) {
return sorted
}
const min = first(sorted)?.x ?? 0
const max = last(sorted)?.x ?? 0
const binWidth = Math.ceil((max - min) / limit)
const newPoints = []
let lastAvgY = sorted[0].y
for (let i = 0; i < limit; i++) {
const binStart = min + i * binWidth
const binEnd = binStart + binWidth
const binPoints = sorted.filter((p) => p.x >= binStart && p.x < binEnd)
if (binPoints.length > 0) {
lastAvgY = meanBy(binPoints, 'y')
}
newPoints.push({ x: binEnd, y: lastAvgY })
}
return newPoints
}