From 4de1c1f4ded5b67868bf5229b21e48fd1f190dd6 Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Mon, 6 Nov 2023 22:20:47 +0200 Subject: [PATCH] core: add support for area charts --- FranzCocoa/Backend.swift | 19 +++++++----- FranzCocoa/ResultDetail.swift | 57 +++++++++++++++++++++++++++++++---- FranzCross/result-detail.rkt | 9 ++++++ core/extlib/render.lua | 1 + core/script.rkt | 5 ++- manual/index.scrbl | 19 ++++++++---- 6 files changed, 90 insertions(+), 20 deletions(-) diff --git a/FranzCocoa/Backend.swift b/FranzCocoa/Backend.swift index f0d0fbf..fc5c52a 100644 --- a/FranzCocoa/Backend.swift +++ b/FranzCocoa/Backend.swift @@ -94,6 +94,7 @@ public enum ChartScaleType: Readable, Writable { } public enum ChartStyle: Readable, Writable { + case area case bar case candlestick(UVarint?) case line @@ -103,14 +104,16 @@ public enum ChartStyle: Readable, Writable { let tag = UVarint.read(from: inp, using: &buf) switch tag { case 0x0000: - return .bar + return .area case 0x0001: + return .bar + case 0x0002: return .candlestick( UVarint?.read(from: inp, using: &buf) ) - case 0x0002: - return .line case 0x0003: + return .line + case 0x0004: return .scatter default: preconditionFailure("ChartStyle: unexpected tag \(tag)") @@ -119,15 +122,17 @@ public enum ChartStyle: Readable, Writable { public func write(to out: OutputPort) { switch self { - case .bar: + case .area: UVarint(0x0000).write(to: out) - case .candlestick(let width): + case .bar: UVarint(0x0001).write(to: out) + case .candlestick(let width): + UVarint(0x0002).write(to: out) width.write(to: out) case .line: - UVarint(0x0002).write(to: out) - case .scatter: UVarint(0x0003).write(to: out) + case .scatter: + UVarint(0x0004).write(to: out) } } } diff --git a/FranzCocoa/ResultDetail.swift b/FranzCocoa/ResultDetail.swift index ab4d334..dfedc13 100644 --- a/FranzCocoa/ResultDetail.swift +++ b/FranzCocoa/ResultDetail.swift @@ -68,8 +68,16 @@ fileprivate struct ChartResult: View { private func chartView() -> any View { let view = Charts.Chart(pairs) { p in switch chart.style { + case .area: + try? p.areaMark( + xLabel: chart.xLabel, + yLabel: chart.yLabel + ) case .bar: - try? p.barMark(xLabel: chart.xLabel, yLabel: chart.yLabel) + try? p.barMark( + xLabel: chart.xLabel, + yLabel: chart.yLabel + ) case .candlestick(let width): try? p.candlestickMark( xLabel: chart.xLabel, @@ -77,9 +85,15 @@ fileprivate struct ChartResult: View { width: width ) case .line: - try? p.lineMark(xLabel: chart.xLabel, yLabel: chart.yLabel) + try? p.lineMark( + xLabel: chart.xLabel, + yLabel: chart.yLabel + ) case .scatter: - try? p.pointMark(xLabel: chart.xLabel, yLabel: chart.yLabel) + try? p.pointMark( + xLabel: chart.xLabel, + yLabel: chart.yLabel + ) } } switch (chart.xScale, chart.yScale) { @@ -98,15 +112,17 @@ fileprivate struct ChartResult: View { @MainActor private func export(frame: CGRect) { - guard let image = ImageRenderer(content: AnyView( + guard let data = ImageRenderer(content: AnyView( chartView() .padding(.all, 20) .background() ).frame( width: frame.width * 2, height: frame.height * 2 - )).nsImage else { return } - guard let data = image.tiffRepresentation else { return } + )).nsImage?.tiffRepresentation else { + return + } + let dialog = NSSavePanel() dialog.allowedContentTypes = [.init(filenameExtension: "tiff")!] dialog.isExtensionHidden = false @@ -142,6 +158,20 @@ fileprivate struct ChartResult: View { let x: ChartValue let y: ChartValue + func areaMark(xLabel: String, yLabel: String) throws -> AreaMark { + switch (x, y) { + case (.categorical(let xcat), .numerical(let y)): + return AreaMark(x: .value(xLabel, xcat), y: .value(yLabel, y)) + case (.timestamp(let ts), .numerical(let y)): + return AreaMark( + x: .value(xLabel, Date(timeIntervalSince1970: Double(ts))), + y: .value(yLabel, y) + ) + default: + throw ChartError.badMarks + } + } + func barMark(xLabel: String, yLabel: String) throws -> BarMark { switch (x, y) { case (.categorical(let xcat), .categorical(let ycat)): @@ -152,6 +182,11 @@ 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)) + case (.timestamp(let ts), .numerical(let y)): + return BarMark( + x: .value(xLabel, Date(timeIntervalSince1970: Double(ts))), + y: .value(yLabel, y) + ) default: throw ChartError.badMarks } @@ -188,6 +223,11 @@ 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)) + case (.timestamp(let ts), .numerical(let y)): + return LineMark( + x: .value(xLabel, Date(timeIntervalSince1970: Double(ts))), + y: .value(yLabel, y) + ) default: throw ChartError.badMarks } @@ -203,6 +243,11 @@ 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)) + case (.timestamp(let ts), .numerical(let y)): + return PointMark( + x: .value(xLabel, Date(timeIntervalSince1970: Double(ts))), + y: .value(yLabel, y) + ) default: throw ChartError.badMarks } diff --git a/FranzCross/result-detail.rkt b/FranzCross/result-detail.rkt index b02c44b..4609e5b 100644 --- a/FranzCross/result-detail.rkt +++ b/FranzCross/result-detail.rkt @@ -80,6 +80,7 @@ #:y-label (Chart-y-label c) (list ((match (Chart-style c) + [(ChartStyle.area) area] [(ChartStyle.bar) discrete-histogram] [(ChartStyle.candlestick _) candlesticks] [(ChartStyle.line) lines] @@ -91,6 +92,14 @@ (ChartValue-> (ChartPair-x p)) (ChartValue-> (ChartPair-y p)))))))))))) +(define (area ps) + (define min-y + (apply min (map cadr ps))) + (lines-interval + (for/list ([p (in-list ps)]) + (list (car p) min-y)) + ps)) + (define (text-view s) (hpanel #:margin '(20 20) diff --git a/core/extlib/render.lua b/core/extlib/render.lua index 8089fbd..994f12c 100644 --- a/core/extlib/render.lua +++ b/core/extlib/render.lua @@ -86,6 +86,7 @@ local function makeChartClass(name) return Chart end +render.AreaChart = makeChartClass("AreaChart") render.BarChart = makeChartClass("BarChart") render.CandlestickChart = makeChartClass("CandlestickChart") render.LineChart = makeChartClass("LineChart") diff --git a/core/script.rkt b/core/script.rkt index 69bf387..079e8d3 100644 --- a/core/script.rkt +++ b/core/script.rkt @@ -46,6 +46,7 @@ {type : ChartScaleType}]) (define-enum ChartStyle + [area] [bar] [candlestick {width : (Optional UVarint)}] @@ -107,6 +108,8 @@ (define (->ChartStyle type v) (case type + [(#"AreaChart") + (ChartStyle.area)] [(#"BarChart") (ChartStyle.bar)] [(#"CandlestickChart") @@ -158,7 +161,7 @@ (define type (table-ref v #"__type")) (case type - [(#"BarChart" #"CandlestickChart" #"LineChart" #"ScatterChart") + [(#"AreaChart" #"BarChart" #"CandlestickChart" #"LineChart" #"ScatterChart") (ReduceResult.chart (make-Chart #:style (->ChartStyle type v) diff --git a/manual/index.scrbl b/manual/index.scrbl index 6820cf6..ecb6105 100644 --- a/manual/index.scrbl +++ b/manual/index.scrbl @@ -816,10 +816,17 @@ data to a window when @tech[#:key "apply"]{applying} a script. @(define (chart-ref . content) (apply elemref '("lua" "Chart:push") content)) +@deflua[render.AreaChart (xlabel ylabel) Chart]{ + Returns an instance of an area @chart-ref{chart renderer}. The first + argument represents the x axis label and the second argument, the y + axis label. The values of the x axis must be @lua[render.Timestamp]s + or @tt{string}s and the y axis values must be @tt{number}s. +} + @deflua[render.BarChart (xlabel ylabel) Chart]{ Returns an instance of a bar @chart-ref{chart renderer}. The first - argument represents the x-axis label and the second argument, the - y-axis. + argument represents the x axis label and the second argument, the + y axis. } @deflua[render.CandlestickChart (xlabel ylabel) Chart]{ @@ -854,14 +861,14 @@ data to a window when @tech[#:key "apply"]{applying} a script. @deflua[render.LineChart (xlabel ylabel) Chart]{ Returns an instance of a line @chart-ref{chart renderer}. The first - argument represents the x-axis label and the second argument, the - y-axis. + argument represents the x axis label and the second argument, the + y axis. } @deflua[render.ScatterChart (xlabel ylabel) Chart]{ Returns an instance of a scatter @chart-ref{chart renderer}. The first - argument represents the x-axis label and the second argument, the - y-axis. + argument represents the x axis label and the second argument, the + y axis. } @deflua[render.Table (columns ...) Table]{