Source
188 chart lines · 3 files · 4.8 kBBenchmark harness excluded · shared/mount.ts
Apple daily stock prices1,260 records · CSV · 92.4 kB
@charts-poc/demo-data/aaplSelection: Complete published snapshot
- Date
- Date
- Open
- number
- High
- number
- Low
- number
- Close
- number
- Adj Close
- number
- Volume
- number
Yahoo! Finance@observablehq/sample-datasets@1.0.1 · revision 732c0148de74 · aapl.csv · ISC distribution; upstream source credited · SHA-256 18dc8bf6542dPinned snapshot
cases/33-difference-chart/tanstack.ts150 lines · entry
cases/33-difference-chart/tanstack.ts
import { aapl } from '@charts-poc/demo-data/aapl'
import { areaY, defineChart, lineY } from '@tanstack/charts'
import { scaleLinear, scaleUtc } from 'd3-scale'
import { formatDifferenceMonth } from './model'
import { rollingCloseAverage } from './transform'
import type { DifferencePoint } from './transform'
import { tanstackMount } from '../../shared/mount'
import type { ConformanceInput } from '../../types'
interface DifferenceAreaPoint extends DifferencePoint {
id: string
segment: string
sign: 'positive' | 'negative'
}
const definition = (input: ConformanceInput) => {
const rows = rollingCloseAverage(
aapl.slice(input.revision * 10, input.revision * 10 + 120),
20,
)
const areaRows = differenceAreas(rows)
return defineChart({
marks: [
areaY(areaRows, {
x: 'Date',
y1: 'average',
y2: 'Close',
z: 'segment',
color: 'sign',
fillOpacity: 0.35,
}),
lineY(rows, {
x: 'Date',
y: 'Close',
stroke: '#166534',
strokeWidth: 2,
}),
lineY(rows, {
x: 'Date',
y: 'average',
stroke: '#475569',
strokeWidth: 2,
}),
],
x: {
scale: scaleUtc,
axis: { ticks: { count: 9, format: formatDifferenceMonth } },
},
y: { scale: scaleLinear, grid: true, axis: { ticks: { count: 6 } } },
color: {
domain: ['positive', 'negative'],
range: ['#16a34a', '#dc2626'],
},
margin: { top: 20, right: 20, bottom: 30, left: 80 },
})
}
export const mount = tanstackMount(
definition,
'Apple closing price versus its twenty-day average',
)
function differenceAreas(
rows: readonly DifferencePoint[],
): readonly DifferenceAreaPoint[] {
const first = rows[0]
if (!first) return []
const points: DifferenceAreaPoint[] = []
let sign = firstNonZeroSign(rows) ?? 'positive'
let segmentIndex = 0
let segment = `${sign}-${segmentIndex}`
pushPoint(first, 'start')
for (let index = 1; index < rows.length; index++) {
const previous = rows[index - 1]
const row = rows[index]
if (!previous || !row) continue
const nextSign = signOf(row) ?? sign
if (crosses(previous, row)) {
const boundary = crossing(previous, row)
pushPoint(boundary, `crossing-${index}:end`)
startSegment(nextSign)
pushPoint(boundary, `crossing-${index}:start`)
} else if (nextSign !== sign) {
startSegment(nextSign)
pushPoint(previous, `boundary-${index}`)
}
pushPoint(row, `row-${index}`)
}
return points
function startSegment(nextSign: DifferenceAreaPoint['sign']) {
sign = nextSign
segmentIndex += 1
segment = `${sign}-${segmentIndex}`
}
function pushPoint(row: DifferencePoint, suffix: string) {
points.push({
...row,
id: `${segment}:${suffix}`,
segment,
sign,
})
}
}
function firstNonZeroSign(
rows: readonly DifferencePoint[],
): DifferenceAreaPoint['sign'] | undefined {
for (const row of rows) {
const sign = signOf(row)
if (sign) return sign
}
return undefined
}
function signOf(row: DifferencePoint): DifferenceAreaPoint['sign'] | undefined {
const difference = row.Close - row.average
if (difference === 0) return undefined
return difference > 0 ? 'positive' : 'negative'
}
function crosses(left: DifferencePoint, right: DifferencePoint): boolean {
return (left.Close - left.average) * (right.Close - right.average) < 0
}
function crossing(
left: DifferencePoint,
right: DifferencePoint,
): DifferencePoint {
const leftDifference = left.Close - left.average
const rightDifference = right.Close - right.average
const t = -leftDifference / (rightDifference - leftDifference)
const time =
left.Date.getTime() + (right.Date.getTime() - left.Date.getTime()) * t
const value = left.Close + (right.Close - left.Close) * t
return {
...left,
Date: new Date(time),
Close: value,
average: value,
}
}cases/33-difference-chart/model.ts18 lines · support
cases/33-difference-chart/model.ts
const monthLabels = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
] as const
export function formatDifferenceMonth(value: Date): string {
return monthLabels[value.getUTCMonth()] ?? ''
}cases/33-difference-chart/transform.ts20 lines · support
cases/33-difference-chart/transform.ts
import { mean } from 'd3-array'
import type { AaplRow } from '@charts-poc/demo-data/aapl'
export interface DifferencePoint extends AaplRow {
average: number
}
export function rollingCloseAverage(
rows: readonly AaplRow[],
window: number,
): readonly DifferencePoint[] {
return rows.flatMap((row, index) => {
if (index < window - 1) return []
const average = mean(
rows.slice(index - window + 1, index + 1),
(observation) => observation.Close,
)
return average === undefined ? [] : [{ ...row, average }]
})
}