Published
Edited
Oct 10, 2022
12 forks
Importers
189 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
detectPeaks([2, 0, 10, 2, 1])
Insert cell
Insert cell
detectPeaks([{value: 2}, {value: 0}, {value: 10}, {value: 3}, {value: 1}], d => d.value)
Insert cell
Insert cell
detectPeaks([1, 3, 2, 0, 10, 3, 1, 4], {
lookaround: 2, // the number of neighbors to compare to on each side
sensitivity: 1, // sensitivity, in terms of standard deviations above the mean
coalesce: 2 // coalesce together peaks within this distance of each other
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function detectPeaks(data, accessor, options) {
let {lookaround, sensitivity, coalesce, full} = Object.assign({
lookaround: 2,
sensitivity: 1.4,
coalesce: 0,
full: false
}, options || accessor)
let values = typeof accessor == "function" ? data.map(accessor) : data

// Compute a peakiness score for every sample value in `data`
// We normalize the scale of the scores by mean-centering and dividing by the standard deviation
// to get a dimensionless quantity such that can be used as a sensitivity parameter
// across different scales of data (s. t. normalize(x) == normalize(k*x))
let scores = normalize(
values.map(
(value, index) => peakiness(
values.slice(max(0, index - lookaround), index),
value,
values.slice(index + 1, index + lookaround + 1)
)
)
)

// Candidate peaks are indices whose score is above the sensitivity threshold
let candidates = d3.range(scores.length).filter(index => scores[index] > sensitivity)

// If we have multiple peaks, coalesce those that are close together
let groups = candidates.length ? [[candidates[0]]] : []
d3.pairs(candidates).forEach(([a, b]) => {
if (b - a < coalesce) {
groups[groups.length - 1].push(b)
} else {
groups.push([b])
}
})

// Represent every group of peaks by the highest peak in the group
let peaks = groups.map(
group => group[d3.scan(group, (a, b) => values[b] - values[a])]
)

return full ? { data, values, scores, candidates, groups, peaks } : peaks
}
Insert cell
// Assigns a spikiness score to `value`, based on its left and right neighbors
peakiness = (left, value, right) => {
// assume zero outside the boundary
return value - d3.max([d3.min(left) || 0, d3.min(right) || 0]) // this can be max or mean.
}
Insert cell
normalize = xs => {
let mean = d3.mean(xs)
let stdev = d3.deviation(xs)
return xs.map(x => (x - mean) / stdev)
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more