@rokkit/chart 1.0.0-next.38 → 1.0.0-next.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -7
- package/src/lib/funnel.js +108 -75
- package/src/lib/summary.js +30 -143
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rokkit/chart",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.42",
|
|
4
4
|
"description": "Components for making interactive charts.",
|
|
5
5
|
"author": "Jerry Thomas <me@jerrythomas.name>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"typescript": "^5.1.6",
|
|
24
24
|
"vite": "^4.4.7",
|
|
25
25
|
"vitest": "~0.33.0",
|
|
26
|
-
"shared-config": "1.0.0-next.
|
|
26
|
+
"shared-config": "1.0.0-next.42"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"d3-array": "^3.2.4",
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"date-fns": "^2.30.0",
|
|
36
36
|
"ramda": "^0.29.0",
|
|
37
37
|
"yootils": "^0.3.1",
|
|
38
|
-
"@rokkit/
|
|
39
|
-
"@rokkit/
|
|
40
|
-
"@rokkit/
|
|
41
|
-
"@rokkit/stores": "1.0.0-next.
|
|
38
|
+
"@rokkit/core": "1.0.0-next.42",
|
|
39
|
+
"@rokkit/molecules": "1.0.0-next.42",
|
|
40
|
+
"@rokkit/atoms": "1.0.0-next.42",
|
|
41
|
+
"@rokkit/stores": "1.0.0-next.42"
|
|
42
42
|
},
|
|
43
43
|
"files": [
|
|
44
44
|
"src/**/*.js",
|
|
@@ -61,6 +61,6 @@
|
|
|
61
61
|
"test": "vitest",
|
|
62
62
|
"coverage": "vitest run --coverage",
|
|
63
63
|
"latest": "pnpm upgrade --latest && pnpm test:ci",
|
|
64
|
-
"release": "
|
|
64
|
+
"release": "pnpm publish --access public"
|
|
65
65
|
}
|
|
66
66
|
}
|
package/src/lib/funnel.js
CHANGED
|
@@ -1,34 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { max, cumsum } from 'd3-array'
|
|
2
2
|
import { nest } from 'd3-collection'
|
|
3
|
-
import {
|
|
3
|
+
import { flatten } from 'ramda'
|
|
4
4
|
import { area, curveBasis, curveBumpX, curveBumpY } from 'd3-shape'
|
|
5
5
|
import { scaleLinear } from 'd3-scale'
|
|
6
|
-
|
|
7
|
-
const aggregate = {
|
|
8
|
-
count: (values) => values.length,
|
|
9
|
-
sum: (values) => sum(values),
|
|
10
|
-
min: (values) => min(values),
|
|
11
|
-
max: (values) => max(values),
|
|
12
|
-
mean: (values) => mean(values),
|
|
13
|
-
median: (values) => median(values),
|
|
14
|
-
q1: (values) => quantile(values, 0.25),
|
|
15
|
-
q3: (values) => quantile(values, 0.75)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function summarize(data, by, attr, stat = 'count') {
|
|
19
|
-
const stats = Array.isArray(stat) ? stat : [stat]
|
|
20
|
-
const grouped = nest()
|
|
21
|
-
.key((d) => by.map((f) => d[f]).join('|'))
|
|
22
|
-
.rollup((rows) => {
|
|
23
|
-
let agg = pick(by, rows[0])
|
|
24
|
-
stats.map(
|
|
25
|
-
(stat) => (agg[stat] = aggregate[stat](rows.map((d) => d[attr])))
|
|
26
|
-
)
|
|
27
|
-
return [agg]
|
|
28
|
-
})
|
|
29
|
-
.entries(data)
|
|
30
|
-
return flatten(grouped.map((group) => group.value))
|
|
31
|
-
}
|
|
6
|
+
import { summarize } from './summary'
|
|
32
7
|
|
|
33
8
|
export function getUniques(input, aes) {
|
|
34
9
|
const attrs = ['x', 'y', 'fill']
|
|
@@ -58,76 +33,58 @@ export function fillMissing(fill, rows, key, aes) {
|
|
|
58
33
|
return filled
|
|
59
34
|
}
|
|
60
35
|
|
|
61
|
-
|
|
62
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Determines if the layout is vertical based on the given parameters.
|
|
38
|
+
* @param {Object} uniques - The unique values.
|
|
39
|
+
* @param {Object} aes - The aes object.
|
|
40
|
+
* @returns {boolean} - Returns true if the layout is vertical, false otherwise.
|
|
41
|
+
*/
|
|
42
|
+
function determineLayout(uniques, aes) {
|
|
63
43
|
let vertical = 'y' in uniques && uniques.y.some(isNaN)
|
|
64
44
|
const horizontal = 'x' in uniques && uniques.x.some(isNaN)
|
|
65
45
|
|
|
66
|
-
let summary = []
|
|
67
|
-
|
|
68
46
|
if (horizontal && vertical) {
|
|
69
47
|
if ((aes.stat || 'count') === 'count') {
|
|
70
48
|
vertical = false
|
|
71
|
-
console.warn('Assuming horizontal layout
|
|
49
|
+
console.warn('Assuming horizontal layout because stat is count')
|
|
72
50
|
} else {
|
|
73
51
|
console.error(
|
|
74
|
-
'
|
|
52
|
+
'Cannot plot without at least one axis having numeric values'
|
|
75
53
|
)
|
|
76
|
-
return
|
|
54
|
+
return null
|
|
77
55
|
}
|
|
78
56
|
}
|
|
57
|
+
return vertical
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Converts to phases.
|
|
62
|
+
* @param {Array} input - The input data.
|
|
63
|
+
* @param {Object} aes - The aes object.
|
|
64
|
+
* @returns {Object} - The phases, uniques, and vertical properties.
|
|
65
|
+
*/
|
|
66
|
+
export function convertToPhases(input, aes) {
|
|
67
|
+
const uniques = getUniques(input, aes)
|
|
68
|
+
const vertical = determineLayout(uniques, aes)
|
|
69
|
+
if (vertical === null) return { uniques, vertical }
|
|
79
70
|
|
|
80
71
|
const key = vertical ? aes.y : aes.x
|
|
81
72
|
const value = vertical ? aes.x : aes.y
|
|
82
73
|
|
|
83
74
|
let by = [key]
|
|
84
|
-
if ('fill' in aes)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
summary = summarize(input, by, value, aes.stat)
|
|
75
|
+
if ('fill' in aes) by.push(aes.fill)
|
|
76
|
+
|
|
77
|
+
const summary = summarize(input, by, value, aes.stat)
|
|
88
78
|
const phases = nest()
|
|
89
79
|
.key((d) => d[key])
|
|
90
|
-
.rollup((rows) =>
|
|
91
|
-
|
|
92
|
-
|
|
80
|
+
.rollup((rows) =>
|
|
81
|
+
'fill' in aes ? fillMissing(uniques.fill, rows, key, aes) : rows
|
|
82
|
+
)
|
|
93
83
|
.entries(summary)
|
|
94
84
|
|
|
95
85
|
return { phases, uniques, vertical }
|
|
96
86
|
}
|
|
97
87
|
|
|
98
|
-
export function mirror(input, aes) {
|
|
99
|
-
let domain = 0
|
|
100
|
-
|
|
101
|
-
const stats = input.phases.map((phase) => {
|
|
102
|
-
const stat = cumsum(phase.value.map((row) => row[aes.stat]))
|
|
103
|
-
const midpoint = max(stat) / 2
|
|
104
|
-
domain = Math.max(domain, midpoint)
|
|
105
|
-
|
|
106
|
-
const rows = phase.value.map((row, index) => {
|
|
107
|
-
if (input.vertical) {
|
|
108
|
-
return {
|
|
109
|
-
...row,
|
|
110
|
-
y: input.uniques.y.indexOf(row[aes.y]),
|
|
111
|
-
x1: stat[index] - midpoint,
|
|
112
|
-
x0: stat[index] - midpoint - row[aes.stat]
|
|
113
|
-
}
|
|
114
|
-
} else {
|
|
115
|
-
return {
|
|
116
|
-
...row,
|
|
117
|
-
x: input.uniques.x.indexOf(row[aes.x]),
|
|
118
|
-
y1: stat[index] - midpoint,
|
|
119
|
-
y0: stat[index] - midpoint - row[aes.stat]
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
phase.value = rows
|
|
125
|
-
return phase
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
return { ...input, stats, domain }
|
|
129
|
-
}
|
|
130
|
-
|
|
131
88
|
export function getScales(input, width, height) {
|
|
132
89
|
let scale
|
|
133
90
|
if (input.vertical) {
|
|
@@ -178,6 +135,82 @@ export function getPaths(vertical, scale, curve) {
|
|
|
178
135
|
.y1((d) => scale.y(d.y1))
|
|
179
136
|
.curve(curve)
|
|
180
137
|
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Mirrors the input based on the provided aesthetic mappings.
|
|
141
|
+
*
|
|
142
|
+
* @param {Object} input - The input data with phases and uniques.
|
|
143
|
+
* @param {Object} aes - The aesthetic mappings.
|
|
144
|
+
* @returns {Object} The mirrored input with updated stats and domain.
|
|
145
|
+
*/
|
|
146
|
+
export function mirror(input, aes) {
|
|
147
|
+
const domain = calculateDomain(input)
|
|
148
|
+
const stats = calculateStats(input, aes, domain)
|
|
149
|
+
|
|
150
|
+
return { ...input, stats, domain }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Calculates the domain for the mirror operation.
|
|
155
|
+
*
|
|
156
|
+
* @param {Object} input - The input data with phases.
|
|
157
|
+
* @returns {number} The calculated domain.
|
|
158
|
+
*/
|
|
159
|
+
function calculateDomain(input) {
|
|
160
|
+
return input.phases.reduce((maxDomain, phase) => {
|
|
161
|
+
const stat = cumsum(phase.value.map((row) => row[aes.stat]))
|
|
162
|
+
const midpoint = max(stat) / 2
|
|
163
|
+
return Math.max(maxDomain, midpoint)
|
|
164
|
+
}, 0)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Calculates the stats for the mirror operation.
|
|
169
|
+
*
|
|
170
|
+
* @param {Object} input - The input data with phases and uniques.
|
|
171
|
+
* @param {Object} aes - The aesthetic mappings.
|
|
172
|
+
* @param {number} domain - The domain for the mirror operation.
|
|
173
|
+
* @returns {Array} The calculated stats.
|
|
174
|
+
*/
|
|
175
|
+
function calculateStats(input, aes, domain) {
|
|
176
|
+
return input.phases.map((phase) => {
|
|
177
|
+
const stat = cumsum(phase.value.map((row) => row[aes.stat]))
|
|
178
|
+
const midpoint = max(stat) / 2
|
|
179
|
+
|
|
180
|
+
phase.value = phase.value.map((row, index) => {
|
|
181
|
+
const position = calculatePosition(input, aes, row, stat, index, midpoint)
|
|
182
|
+
return { ...row, ...position }
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
return phase
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Calculates the position for the mirror operation.
|
|
191
|
+
*
|
|
192
|
+
* @param {Object} input - The input data with uniques.
|
|
193
|
+
* @param {Object} aes - The aesthetic mappings.
|
|
194
|
+
* @param {Object} row - The current row.
|
|
195
|
+
* @param {Array} stat - The stat data.
|
|
196
|
+
* @param {number} index - The current index.
|
|
197
|
+
* @param {number} midpoint - The midpoint for the mirror operation.
|
|
198
|
+
* @returns {Object} The calculated position.
|
|
199
|
+
*/
|
|
200
|
+
function calculatePosition(input, aes, row, stat, index, midpoint) {
|
|
201
|
+
const axis = input.vertical ? 'y' : 'x'
|
|
202
|
+
const oppositeAxis = input.vertical ? 'x' : 'y'
|
|
203
|
+
const axisValue = input.uniques[axis].indexOf(row[aes[axis]])
|
|
204
|
+
const position1 = stat[index] - midpoint
|
|
205
|
+
const position0 = position1 - row[aes.stat]
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
[axis]: axisValue,
|
|
209
|
+
[`${oppositeAxis}1`]: position1,
|
|
210
|
+
[`${oppositeAxis}0`]: position0
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
181
214
|
export function funnel(input, aes, width, height) {
|
|
182
215
|
let data = convertToPhases(input, aes)
|
|
183
216
|
data = mirror(data, aes)
|
package/src/lib/summary.js
CHANGED
|
@@ -1,143 +1,30 @@
|
|
|
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
|
-
// export function getUniques(input, aes) {
|
|
32
|
-
// const attrs = ['x', 'y', 'fill']
|
|
33
|
-
// let values = {}
|
|
34
|
-
|
|
35
|
-
// attrs.map((attr) => {
|
|
36
|
-
// if (attr in aes) {
|
|
37
|
-
// values[attr] = [...new Set(input.map((d) => d[aes[attr]]))]
|
|
38
|
-
// }
|
|
39
|
-
// })
|
|
40
|
-
// // x: 'x' in aes ? [...new Set(input.map((d) => d[aes.x]))] : [],
|
|
41
|
-
// // y: 'y' in aes ? [...new Set(input.map((d) => d[aes.y]))] : [],
|
|
42
|
-
// // fill: 'fill' in aes ? [...new Set(input.map((d) => d[aes.fill]))] : [],
|
|
43
|
-
// // }
|
|
44
|
-
// return values
|
|
45
|
-
// }
|
|
46
|
-
|
|
47
|
-
// export function fillMissing(fill, rows, key, aes) {
|
|
48
|
-
// const filled = fill.map((f) => {
|
|
49
|
-
// let matched = rows.filter((r) => r[aes.fill] === f)
|
|
50
|
-
// if (matched.length == 0) {
|
|
51
|
-
// let row = {}
|
|
52
|
-
// row[key] = rows[0][key]
|
|
53
|
-
// row[aes.fill] = f
|
|
54
|
-
// row[aes.stat] = 0
|
|
55
|
-
// return row
|
|
56
|
-
// } else {
|
|
57
|
-
// return matched[0]
|
|
58
|
-
// }
|
|
59
|
-
// })
|
|
60
|
-
// return filled
|
|
61
|
-
// }
|
|
62
|
-
|
|
63
|
-
// export function convertToPhases(input, aes) {
|
|
64
|
-
// const uniques = getUniques(input, aes)
|
|
65
|
-
// let vertical = 'y' in uniques && uniques.y.some(isNaN)
|
|
66
|
-
// const horizontal = 'x' in uniques && uniques.x.some(isNaN)
|
|
67
|
-
|
|
68
|
-
// let summary = []
|
|
69
|
-
|
|
70
|
-
// if (horizontal && vertical) {
|
|
71
|
-
// if ((aes.stat || 'count') === 'count') {
|
|
72
|
-
// vertical = false
|
|
73
|
-
// console.warn('Assuming horizontal layout becuse stat is count')
|
|
74
|
-
// } else {
|
|
75
|
-
// console.error(
|
|
76
|
-
// 'cannot plot without at least one axis having numeric values'
|
|
77
|
-
// )
|
|
78
|
-
// return { uniques, vertical }
|
|
79
|
-
// }
|
|
80
|
-
// }
|
|
81
|
-
|
|
82
|
-
// const key = vertical ? aes.y : aes.x
|
|
83
|
-
// const value = vertical ? aes.x : aes.y
|
|
84
|
-
|
|
85
|
-
// let by = [key]
|
|
86
|
-
// if ('fill' in aes) {
|
|
87
|
-
// by.push(aes.fill)
|
|
88
|
-
// }
|
|
89
|
-
// summary = summarize(input, by, value, aes.stat)
|
|
90
|
-
// const phases = nest()
|
|
91
|
-
// .key((d) => d[key])
|
|
92
|
-
// .rollup((rows) => {
|
|
93
|
-
// return 'fill' in aes ? fillMissing(uniques.fill, rows, key, aes) : rows
|
|
94
|
-
// })
|
|
95
|
-
// .entries(summary)
|
|
96
|
-
|
|
97
|
-
// return { phases, uniques, vertical }
|
|
98
|
-
// }
|
|
99
|
-
|
|
100
|
-
// export function mirror(input, aes) {
|
|
101
|
-
// let domain = 0
|
|
102
|
-
|
|
103
|
-
// const stats = input.phases.map((phase) => {
|
|
104
|
-
// const stat = cumsum(phase.value.map((row) => row[aes.stat]))
|
|
105
|
-
// const midpoint = max(stat) / 2
|
|
106
|
-
// domain = Math.max(domain, midpoint)
|
|
107
|
-
|
|
108
|
-
// const rows = phase.value.map((row, index) => {
|
|
109
|
-
// if (input.vertical) {
|
|
110
|
-
// return {
|
|
111
|
-
// ...row,
|
|
112
|
-
// y: input.uniques.y.indexOf(row[aes.y]),
|
|
113
|
-
// x1: stat[index] - midpoint,
|
|
114
|
-
// x0: stat[index] - midpoint - row[aes.stat],
|
|
115
|
-
// }
|
|
116
|
-
// } else {
|
|
117
|
-
// return {
|
|
118
|
-
// ...row,
|
|
119
|
-
// x: input.uniques.x.indexOf(row[aes.x]),
|
|
120
|
-
// y1: stat[index] - midpoint,
|
|
121
|
-
// y0: stat[index] - midpoint - row[aes.stat],
|
|
122
|
-
// }
|
|
123
|
-
// }
|
|
124
|
-
// })
|
|
125
|
-
// phase.value = rows
|
|
126
|
-
// return phase
|
|
127
|
-
// })
|
|
128
|
-
// return { ...input, stats }
|
|
129
|
-
// }
|
|
130
|
-
|
|
131
|
-
// export function funnel(input, aes) {
|
|
132
|
-
// let data = convertToPhases(input, aes)
|
|
133
|
-
// data = mirror(data, aes)
|
|
134
|
-
|
|
135
|
-
// if ('fill' in aes) {
|
|
136
|
-
// let stats = flatten(data.stats.map((phase) => phase.value))
|
|
137
|
-
// data.stats = nest()
|
|
138
|
-
// .key((d) => d[aes.fill])
|
|
139
|
-
// .rollup((rows) => [...rows, { ...rows[rows.length - 1], x: rows.length }])
|
|
140
|
-
// .entries(stats)
|
|
141
|
-
// }
|
|
142
|
-
// return data
|
|
143
|
-
// }
|
|
1
|
+
import { sum, median, max, mean, min, quantile, cumsum } from 'd3-array'
|
|
2
|
+
import { nest } from 'd3-collection'
|
|
3
|
+
import { pick, flatten } from 'ramda'
|
|
4
|
+
|
|
5
|
+
export const aggregate = {
|
|
6
|
+
count: (values) => values.length,
|
|
7
|
+
sum: (values) => sum(values),
|
|
8
|
+
min: (values) => min(values),
|
|
9
|
+
max: (values) => max(values),
|
|
10
|
+
mean: (values) => mean(values),
|
|
11
|
+
median: (values) => median(values),
|
|
12
|
+
cumsum: (values) => cumsum(values),
|
|
13
|
+
q1: (values) => quantile(values, 0.25),
|
|
14
|
+
q3: (values) => quantile(values, 0.75)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function summarize(data, by, attr, stat = 'count') {
|
|
18
|
+
const stats = Array.isArray(stat) ? stat : [stat]
|
|
19
|
+
const grouped = nest()
|
|
20
|
+
.key((d) => by.map((f) => d[f]).join('|'))
|
|
21
|
+
.rollup((rows) => {
|
|
22
|
+
let agg = pick(by, rows[0])
|
|
23
|
+
stats.map(
|
|
24
|
+
(stat) => (agg[stat] = aggregate[stat](rows.map((d) => d[attr])))
|
|
25
|
+
)
|
|
26
|
+
return [agg]
|
|
27
|
+
})
|
|
28
|
+
.entries(data)
|
|
29
|
+
return flatten(grouped.map((group) => group.value))
|
|
30
|
+
}
|