Skip to content

Commit

Permalink
core: add support for candlestick charts
Browse files Browse the repository at this point in the history
  • Loading branch information
Bogdanp committed Nov 5, 2023
1 parent 99c2cef commit 1802f6c
Show file tree
Hide file tree
Showing 6 changed files with 287 additions and 56 deletions.
42 changes: 36 additions & 6 deletions FranzCocoa/Backend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public enum ChartScaleType: Readable, Writable {

public enum ChartStyle: Readable, Writable {
case bar
case candlestick(UVarint)
case line
case scatter

Expand All @@ -104,8 +105,12 @@ public enum ChartStyle: Readable, Writable {
case 0x0000:
return .bar
case 0x0001:
return .line
return .candlestick(
UVarint.read(from: inp, using: &buf)
)
case 0x0002:
return .line
case 0x0003:
return .scatter
default:
preconditionFailure("ChartStyle: unexpected tag \(tag)")
Expand All @@ -116,42 +121,67 @@ public enum ChartStyle: Readable, Writable {
switch self {
case .bar:
UVarint(0x0000).write(to: out)
case .line:
case .candlestick(let width):
UVarint(0x0001).write(to: out)
case .scatter:
width.write(to: out)
case .line:
UVarint(0x0002).write(to: out)
case .scatter:
UVarint(0x0003).write(to: out)
}
}
}

public enum ChartValue: Readable, Writable {
case candlestick(Float64, Float64, Float64, Float64)
case categorical(String)
case numerical(Float64)
case timestamp(UVarint)

public static func read(from inp: InputPort, using buf: inout Data) -> ChartValue {
let tag = UVarint.read(from: inp, using: &buf)
switch tag {
case 0x0000:
return .candlestick(
Float64.read(from: inp, using: &buf),
Float64.read(from: inp, using: &buf),
Float64.read(from: inp, using: &buf),
Float64.read(from: inp, using: &buf)
)
case 0x0001:
return .categorical(
String.read(from: inp, using: &buf)
)
case 0x0001:
case 0x0002:
return .numerical(
Float64.read(from: inp, using: &buf)
)
case 0x0003:
return .timestamp(
UVarint.read(from: inp, using: &buf)
)
default:
preconditionFailure("ChartValue: unexpected tag \(tag)")
}
}

public func write(to out: OutputPort) {
switch self {
case .categorical(let s):
case .candlestick(let o, let h, let l, let c):
UVarint(0x0000).write(to: out)
o.write(to: out)
h.write(to: out)
l.write(to: out)
c.write(to: out)
case .categorical(let s):
UVarint(0x0001).write(to: out)
s.write(to: out)
case .numerical(let n):
UVarint(0x0001).write(to: out)
UVarint(0x0002).write(to: out)
n.write(to: out)
case .timestamp(let t):
UVarint(0x0003).write(to: out)
t.write(to: out)
}
}
}
Expand Down
76 changes: 69 additions & 7 deletions FranzCocoa/ResultDetail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,21 @@ fileprivate struct ChartResult: View {
}

private func chartView() -> any View {
let view = Charts.Chart(pairs(chart.xs, chart.ys)) { p in
let ps = pairs(chart.xs, chart.ys)
let view = Charts.Chart(ps) { p in
switch chart.style {
case .bar:
p.barMark(xLabel: chart.xLabel, yLabel: chart.yLabel)
try? p.barMark(xLabel: chart.xLabel, yLabel: chart.yLabel)
case .candlestick:
try? p.candlestickMark(
xLabel: chart.xLabel,
yLabel: chart.yLabel,
previous: p.id > 0 && p.id < ps.endIndex ? ps[p.id-1] : nil
)
case .line:
p.lineMark(xLabel: chart.xLabel, yLabel: chart.yLabel)
try? p.lineMark(xLabel: chart.xLabel, yLabel: chart.yLabel)
case .scatter:
p.pointMark(xLabel: chart.xLabel, yLabel: chart.yLabel)
try? p.pointMark(xLabel: chart.xLabel, yLabel: chart.yLabel)
}
}
switch (chart.xScale, chart.yScale) {
Expand All @@ -80,12 +87,34 @@ fileprivate struct ChartResult: View {
}
}

enum ChartError: Swift.Error {
case badMarks
}

private enum CandlestickDirection {
case up
case down
}

private struct CandlestickMark<X: Plottable, Y: Plottable>: ChartContent {
let x: PlottableValue<X>
let o: PlottableValue<Y>
let h: PlottableValue<Y>
let l: PlottableValue<Y>
let c: PlottableValue<Y>

var body: some ChartContent {
RectangleMark(x: x, yStart: l, yEnd: h, width: 1)
RectangleMark(x: x, yStart: o, yEnd: c, width: 14)
}
}

private struct Pair: Identifiable {
let id: Int
let x: ChartValue
let y: ChartValue

func barMark(xLabel: String, yLabel: String) -> BarMark {
func barMark(xLabel: String, yLabel: String) throws -> BarMark {
switch (x, y) {
case (.categorical(let xcat), .categorical(let ycat)):
return BarMark(x: .value(xLabel, xcat), y: .value(yLabel, ycat))
Expand All @@ -95,10 +124,39 @@ fileprivate struct ChartResult: View {
return BarMark(x: .value(xLabel, x), y: .value(yLabel, ycat))
case (.numerical(let x), .numerical(let y)):
return BarMark(x: .value(xLabel, x), y: .value(yLabel, y))
default:
throw ChartError.badMarks
}
}

func candlestickMark(
xLabel: String,
yLabel: String,
previous: Pair?
) throws -> some ChartContent {
switch (x, y) {
case (.timestamp(let ts), .candlestick(let o, let h, let l, let c)):
var direction: CandlestickDirection
switch previous?.y {
case .some(.candlestick(_, _, _, let oc)):
direction = oc > c ? .down : .up
default:
direction = .up
}
return CandlestickMark(
x: .value("Date", Date(timeIntervalSince1970: Double(ts))),
o: .value("Open", o),
h: .value("High", h),
l: .value("Low", l),
c: .value("Close", c)
)
.foregroundStyle(direction == .down ? .red : .green)
default:
throw ChartError.badMarks
}
}

func lineMark(xLabel: String, yLabel: String) -> LineMark {
func lineMark(xLabel: String, yLabel: String) throws -> LineMark {
switch (x, y) {
case (.categorical(let xcat), .categorical(let ycat)):
return LineMark(x: .value(xLabel, xcat), y: .value(yLabel, ycat))
Expand All @@ -108,10 +166,12 @@ fileprivate struct ChartResult: View {
return LineMark(x: .value(xLabel, x), y: .value(yLabel, ycat))
case (.numerical(let x), .numerical(let y)):
return LineMark(x: .value(xLabel, x), y: .value(yLabel, y))
default:
throw ChartError.badMarks
}
}

func pointMark(xLabel: String, yLabel: String) -> PointMark {
func pointMark(xLabel: String, yLabel: String) throws -> PointMark {
switch (x, y) {
case (.categorical(let xcat), .categorical(let ycat)):
return PointMark(x: .value(xLabel, xcat), y: .value(yLabel, ycat))
Expand All @@ -121,6 +181,8 @@ fileprivate struct ChartResult: View {
return PointMark(x: .value(xLabel, x), y: .value(yLabel, ycat))
case (.numerical(let x), .numerical(let y)):
return PointMark(x: .value(xLabel, x), y: .value(yLabel, y))
default:
throw ChartError.badMarks
}
}
}
Expand Down
63 changes: 42 additions & 21 deletions FranzCross/result-detail.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@

(define (ChartValue-> v)
(match v
[(ChartValue.candlestick o h l c) (list o h l c)]
[(ChartValue.categorical category) category]
[(ChartValue.numerical n) n]))
[(ChartValue.numerical n) n]
[(ChartValue.timestamp t) t]))

(define (chart-view c)
(hpanel
Expand All @@ -49,26 +51,45 @@
(ChartScale-> (Chart-x-scale c)))
(define-values (y-min y-max)
(ChartScale-> (Chart-y-scale c)))
(apply
plot-snip
#:width width
#:height height
#:x-min x-min
#:x-max x-max
#:x-label (Chart-x-label c)
#:y-min y-min
#:y-max y-max
#:y-label (Chart-y-label c)
(list
((match (Chart-style c)
[(ChartStyle.bar) discrete-histogram]
[(ChartStyle.line) lines]
[(ChartStyle.scatter) points])
(for/list ([x (in-list (Chart-xs c))]
[y (in-list (Chart-ys c))])
(list
(ChartValue-> x)
(ChartValue-> y))))))))))
(define x-date-ticks?
(ormap ChartValue.timestamp? (Chart-xs c)))
(define y-date-ticks?
(ormap ChartValue.timestamp? (Chart-ys c)))
(parameterize ([candlestick-width
(match (Chart-style c)
[(ChartStyle.candlestick width) width]
[_ 1])]
[plot-x-ticks
(if x-date-ticks?
(date-ticks)
(linear-ticks))]
[plot-y-ticks
(if y-date-ticks?
(date-ticks)
(linear-ticks))])
(apply
plot-snip
#:width width
#:height height
#:x-min x-min
#:x-max x-max
#:x-label (Chart-x-label c)
#:y-min y-min
#:y-max y-max
#:y-label (Chart-y-label c)
(list
((match (Chart-style c)
[(ChartStyle.bar) discrete-histogram]
[(ChartStyle.candlestick _) candlesticks]
[(ChartStyle.line) lines]
[(ChartStyle.scatter) points])
(for/list ([x (in-list (Chart-xs c))]
[y (in-list (Chart-ys c))])
((match (Chart-style c)
[(ChartStyle.candlestick _) list*]
[_ list])
(ChartValue-> x)
(ChartValue-> y)))))))))))

(define (text-view s)
(hpanel
Expand Down
Loading

0 comments on commit 1802f6c

Please sign in to comment.