From ea9e7a3dc9d1fff80e2484650f1de393de3ea8ea Mon Sep 17 00:00:00 2001 From: Dario Abdulrehman Date: Fri, 3 Oct 2025 13:22:26 +0100 Subject: [PATCH 1/5] Decouple chart dims from canvas with renderToFit and renderWithFit, update sandboxes, flake.nix cleanup, add .envrc (closes #17) --- README.md | 7 ++++ flake.nix | 3 -- .../florence/renderer/CanvasRenderer.scala | 35 +++++++++++++++++++ .../src/florence/sandbox/CustomChart.scala | 2 +- sandbox/src/florence/sandbox/js/Example.scala | 2 +- 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 29756da..7f11cc7 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,9 @@ val canvas = document.getElementById("my-canvas").asInstanceOf[HTMLCanvasElement // Render the chart styledChart.renderTo(canvas.getContext2D()) + +// Or fit logical style width/height to the canvas size +styledChart.renderToFit(canvas.getContext2D()) ``` ## Customising Charts @@ -176,3 +179,7 @@ Experimental ## License [License details] + +## Dev Environment + +- Nix + direnv: enable the dev shell by running `direnv allow` in the repo. The `.envrc` uses `use flake` to load the environment defined in `flake.nix` (JDK 17, Mill, Node/Yarn). diff --git a/flake.nix b/flake.nix index 0ad7a4f..1f7086f 100644 --- a/flake.nix +++ b/flake.nix @@ -7,18 +7,15 @@ let pkgs = import nixpkgs { inherit system; }; jdkToUse = pkgs.jdk17; - sbtWithJRE = pkgs.sbt.override { jre = jdkToUse; }; millWithJRE = pkgs.mill.override { jre = jdkToUse; }; in { devShells.default = pkgs.mkShell { packages = [ jdkToUse - sbtWithJRE millWithJRE pkgs.nodejs pkgs.yarn - pkgs.nodePackages_latest.http-server ]; }; } diff --git a/florence/src-js/florence/renderer/CanvasRenderer.scala b/florence/src-js/florence/renderer/CanvasRenderer.scala index d0e9e69..74c2e40 100644 --- a/florence/src-js/florence/renderer/CanvasRenderer.scala +++ b/florence/src-js/florence/renderer/CanvasRenderer.scala @@ -169,3 +169,38 @@ object CanvasRendererExtensions: def render(drawing: Drawing): Unit = val ctx = getContext2D() CanvasRenderer.render(drawing, ctx) + + import florence.core.dsl.styling.StyledChart + import florence.core.model.Chart + import florence.core.model.styling.ChartStyle + import florence.core.model.styling.WithCommonProps + + extension [C <: Chart, S <: ChartStyle](styled: StyledChart[C, S]) + + def renderToFit(ctx: CanvasRenderingContext2D)(using + interpreter: Interpreter[StyledChart[C, S], Drawing] + ): Unit = + val cw = ctx.canvas.width.toDouble + val ch = ctx.canvas.height.toDouble + val propsS = summon[WithCommonProps[ChartStyle]].getCommonProps(styled.style) + val w = propsS.width + val h = propsS.height + val sx = if w == 0 then 1.0 else cw / w.toDouble + val sy = if h == 0 then 1.0 else ch / h.toDouble + val drw = interpreter.interpret(styled) + CanvasRenderer.render(drw.transformed(sx = sx, sy = sy), ctx) + + extension [C <: Chart, S <: ChartStyle](chart: C) + + def renderWithFit(style: S, ctx: CanvasRenderingContext2D)(using + interpreter: Interpreter[(C, S), Drawing] + ): Unit = + val cw = ctx.canvas.width.toDouble + val ch = ctx.canvas.height.toDouble + val propsS = summon[WithCommonProps[ChartStyle]].getCommonProps(style) + val w = propsS.width + val h = propsS.height + val sx = if w == 0 then 1.0 else cw / w.toDouble + val sy = if h == 0 then 1.0 else ch / h.toDouble + val drw = interpreter.interpret((chart, style)) + CanvasRenderer.render(drw.transformed(sx = sx, sy = sy), ctx) diff --git a/sandbox-tyrian/src/florence/sandbox/CustomChart.scala b/sandbox-tyrian/src/florence/sandbox/CustomChart.scala index 0c1454f..8f3adfc 100644 --- a/sandbox-tyrian/src/florence/sandbox/CustomChart.scala +++ b/sandbox-tyrian/src/florence/sandbox/CustomChart.scala @@ -84,4 +84,4 @@ object CustomChart: def renderChart(canvas: HTMLCanvasElement): Unit = chart .withStyling(style) - .renderTo(canvas.getContext2D()) + .renderToFit(canvas.getContext2D()) diff --git a/sandbox/src/florence/sandbox/js/Example.scala b/sandbox/src/florence/sandbox/js/Example.scala index 5d6a27b..4030a88 100644 --- a/sandbox/src/florence/sandbox/js/Example.scala +++ b/sandbox/src/florence/sandbox/js/Example.scala @@ -186,6 +186,6 @@ object Example: .withHeight(400) .withMargins(Margins(40, 80, 50, 110)) val styledChart = chart.withStyling(style) - styledChart.renderTo(canvas.getContext2D()) + styledChart.renderToFit(canvas.getContext2D()) end renderChart end Example From 71e1bcdd305dc21c56b72f9c157973575b7b7fa7 Mon Sep 17 00:00:00 2001 From: Dario Abdulrehman Date: Fri, 3 Oct 2025 21:14:59 +0100 Subject: [PATCH 2/5] Add resize/redraw rendering helpers and switch sandboxes to renderToResize, keep renderToFit for non-uniform fill, flake.nix add git to dev shell --- README.md | 5 ++- flake.nix | 1 + .../florence/renderer/CanvasRenderer.scala | 37 +++++++++++++++++++ .../src/florence/sandbox/CustomChart.scala | 2 +- sandbox/src/florence/sandbox/js/Example.scala | 2 +- 5 files changed, 44 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7f11cc7..4d52655 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,10 @@ val canvas = document.getElementById("my-canvas").asInstanceOf[HTMLCanvasElement // Render the chart styledChart.renderTo(canvas.getContext2D()) -// Or fit logical style width/height to the canvas size +// Fit by redrawing at canvas size (no distortion) +styledChart.renderToResize(canvas.getContext2D()) + +// Or non-uniform scale to fill (may distort) styledChart.renderToFit(canvas.getContext2D()) ``` diff --git a/flake.nix b/flake.nix index 1f7086f..83d3df8 100644 --- a/flake.nix +++ b/flake.nix @@ -14,6 +14,7 @@ packages = [ jdkToUse millWithJRE + pkgs.git pkgs.nodejs pkgs.yarn ]; diff --git a/florence/src-js/florence/renderer/CanvasRenderer.scala b/florence/src-js/florence/renderer/CanvasRenderer.scala index 74c2e40..6d43b92 100644 --- a/florence/src-js/florence/renderer/CanvasRenderer.scala +++ b/florence/src-js/florence/renderer/CanvasRenderer.scala @@ -172,7 +172,9 @@ object CanvasRendererExtensions: import florence.core.dsl.styling.StyledChart import florence.core.model.Chart + import florence.core.model.Chart.LineChart import florence.core.model.styling.ChartStyle + import florence.core.model.styling.ChartStyle.LineChartStyle import florence.core.model.styling.WithCommonProps extension [C <: Chart, S <: ChartStyle](styled: StyledChart[C, S]) @@ -204,3 +206,38 @@ object CanvasRendererExtensions: val sy = if h == 0 then 1.0 else ch / h.toDouble val drw = interpreter.interpret((chart, style)) CanvasRenderer.render(drw.transformed(sx = sx, sy = sy), ctx) + + extension (styled: StyledChart[LineChart, LineChartStyle]) + def renderToResize(ctx: CanvasRenderingContext2D)(using + interpreter: Interpreter[StyledChart[LineChart, LineChartStyle], Drawing] + ): Unit = + val cw = ctx.canvas.width + val ch = ctx.canvas.height + val s = styled.style + val s2: LineChartStyle = LineChartStyle( + commonProps = s.commonProps.copy(width = cw, height = ch), + xAxis = s.xAxis, + yAxis = s.yAxis, + seriesStyles = s.seriesStyles, + defaultSeriesStyle = s.defaultSeriesStyle, + showPoints = s.showPoints + ) + val drw = interpreter.interpret(styled.copy(style = s2)) + CanvasRenderer.render(drw, ctx) + + extension (chart: LineChart) + def renderWithResize(style: LineChartStyle, ctx: CanvasRenderingContext2D)(using + interpreter: Interpreter[(LineChart, LineChartStyle), Drawing] + ): Unit = + val cw = ctx.canvas.width + val ch = ctx.canvas.height + val s2: LineChartStyle = LineChartStyle( + commonProps = style.commonProps.copy(width = cw, height = ch), + xAxis = style.xAxis, + yAxis = style.yAxis, + seriesStyles = style.seriesStyles, + defaultSeriesStyle = style.defaultSeriesStyle, + showPoints = style.showPoints + ) + val drw = interpreter.interpret((chart, s2)) + CanvasRenderer.render(drw, ctx) diff --git a/sandbox-tyrian/src/florence/sandbox/CustomChart.scala b/sandbox-tyrian/src/florence/sandbox/CustomChart.scala index 8f3adfc..1491489 100644 --- a/sandbox-tyrian/src/florence/sandbox/CustomChart.scala +++ b/sandbox-tyrian/src/florence/sandbox/CustomChart.scala @@ -84,4 +84,4 @@ object CustomChart: def renderChart(canvas: HTMLCanvasElement): Unit = chart .withStyling(style) - .renderToFit(canvas.getContext2D()) + .renderToResize(canvas.getContext2D()) diff --git a/sandbox/src/florence/sandbox/js/Example.scala b/sandbox/src/florence/sandbox/js/Example.scala index 4030a88..3707c27 100644 --- a/sandbox/src/florence/sandbox/js/Example.scala +++ b/sandbox/src/florence/sandbox/js/Example.scala @@ -186,6 +186,6 @@ object Example: .withHeight(400) .withMargins(Margins(40, 80, 50, 110)) val styledChart = chart.withStyling(style) - styledChart.renderToFit(canvas.getContext2D()) + styledChart.renderToResize(canvas.getContext2D()) end renderChart end Example From b475e75a658ef95aa23b1c645f23ac6f95ad524d Mon Sep 17 00:00:00 2001 From: Dario Abdulrehman Date: Fri, 3 Oct 2025 22:15:32 +0100 Subject: [PATCH 3/5] Add pure resize rendering test and apply formatting changes --- .../florence/renderer/CanvasRenderer.scala | 12 ++-- .../renderer/ResizeRenderingTests.scala | 57 +++++++++++++++++++ 2 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 florence/test/src/florence/renderer/ResizeRenderingTests.scala diff --git a/florence/src-js/florence/renderer/CanvasRenderer.scala b/florence/src-js/florence/renderer/CanvasRenderer.scala index 6d43b92..c8d675d 100644 --- a/florence/src-js/florence/renderer/CanvasRenderer.scala +++ b/florence/src-js/florence/renderer/CanvasRenderer.scala @@ -208,12 +208,13 @@ object CanvasRendererExtensions: CanvasRenderer.render(drw.transformed(sx = sx, sy = sy), ctx) extension (styled: StyledChart[LineChart, LineChartStyle]) + def renderToResize(ctx: CanvasRenderingContext2D)(using interpreter: Interpreter[StyledChart[LineChart, LineChartStyle], Drawing] ): Unit = - val cw = ctx.canvas.width - val ch = ctx.canvas.height - val s = styled.style + val cw = ctx.canvas.width + val ch = ctx.canvas.height + val s = styled.style val s2: LineChartStyle = LineChartStyle( commonProps = s.commonProps.copy(width = cw, height = ch), xAxis = s.xAxis, @@ -226,11 +227,12 @@ object CanvasRendererExtensions: CanvasRenderer.render(drw, ctx) extension (chart: LineChart) + def renderWithResize(style: LineChartStyle, ctx: CanvasRenderingContext2D)(using interpreter: Interpreter[(LineChart, LineChartStyle), Drawing] ): Unit = - val cw = ctx.canvas.width - val ch = ctx.canvas.height + val cw = ctx.canvas.width + val ch = ctx.canvas.height val s2: LineChartStyle = LineChartStyle( commonProps = style.commonProps.copy(width = cw, height = ch), xAxis = style.xAxis, diff --git a/florence/test/src/florence/renderer/ResizeRenderingTests.scala b/florence/test/src/florence/renderer/ResizeRenderingTests.scala new file mode 100644 index 0000000..cff6576 --- /dev/null +++ b/florence/test/src/florence/renderer/ResizeRenderingTests.scala @@ -0,0 +1,57 @@ +package florence + +import munit.FunSuite + +import florence.core.dsl.LineChartDsl.* +import florence.core.dsl.styling.LineChartStylingDsl.* +import florence.core.model.* +import florence.core.model.styling.* +import florence.core.model.shared.StyleTypes.* +import florence.core.rendering.* + +class ResizeRenderingTests extends FunSuite: + + private def hasXAxisAt(ops: List[DrawOp], width: Int, height: Int, margins: Margins): Boolean = + ops.exists { + case LineOp(x1, y1, x2, y2, style) => + val expectedY = height - margins.bottom + val expectedX1 = margins.left + val expectedX2 = width - margins.right + y1 == expectedY && y2 == expectedY && x1 == expectedX1 && x2 == expectedX2 && style.width == 2.0 + case _ => false + } + + private def hasYAxisAt(ops: List[DrawOp], height: Int, margins: Margins): Boolean = + ops.exists { + case LineOp(x1, y1, x2, y2, style) => + val expectedX = margins.left + val expectedY1 = margins.top + val expectedY2 = height - margins.bottom + x1 == expectedX && x2 == expectedX && y1 == expectedY1 && y2 == expectedY2 && style.width == 2.0 + case _ => false + } + + private def resizedStyle(base: ChartStyle.LineChartStyle, w: Int, h: Int, margins: Margins): ChartStyle.LineChartStyle = + base + .withWidth(w) + .withHeight(h) + .withMargins(margins) + .withXAxis(base.xAxis.copy(gridLines = false)) + .withYAxis(base.yAxis.copy(gridLines = false)) + + private def simpleChart: Chart.LineChart = + lineChart("t", pointsSeries("s", (1.0, 1.0), (2.0, 2.0))) + + test("axes respect resized width/height at 600x200"): + val margins = Margins(20, 40, 30, 50) + val style = resizedStyle(lineChartStyle(), 600, 200, margins) + val drawing = LineChartInterpreter.interpretLineChart(simpleChart, style) + assert(hasXAxisAt(drawing.ops, 600, 200, margins)) + assert(hasYAxisAt(drawing.ops, 200, margins)) + + test("axes respect resized width/height at 1300x300"): + val margins = Margins(30, 60, 40, 70) + val style = resizedStyle(lineChartStyle(), 1300, 300, margins) + val drawing = LineChartInterpreter.interpretLineChart(simpleChart, style) + assert(hasXAxisAt(drawing.ops, 1300, 300, margins)) + assert(hasYAxisAt(drawing.ops, 300, margins)) From 50aa023fe9f96551be4e9a3cd065d6aec76b36d8 Mon Sep 17 00:00:00 2001 From: Dario Abdulrehman Date: Mon, 6 Oct 2025 20:37:17 +0100 Subject: [PATCH 4/5] Rename canvas helpers for clarity renderFillCanvas and renderFillCanvasWith for non-uniform fill, renderAtCanvasSize and renderAtCanvasSizeWith for redraw --- README.md | 4 +-- .../florence/renderer/CanvasRenderer.scala | 26 +++++++++++++------ .../src/florence/sandbox/CustomChart.scala | 2 +- sandbox/src/florence/sandbox/js/Example.scala | 2 +- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4d52655..7d11176 100644 --- a/README.md +++ b/README.md @@ -78,10 +78,10 @@ val canvas = document.getElementById("my-canvas").asInstanceOf[HTMLCanvasElement styledChart.renderTo(canvas.getContext2D()) // Fit by redrawing at canvas size (no distortion) -styledChart.renderToResize(canvas.getContext2D()) +styledChart.renderAtCanvasSize(canvas.getContext2D()) // Or non-uniform scale to fill (may distort) -styledChart.renderToFit(canvas.getContext2D()) +styledChart.renderFillCanvas(canvas.getContext2D()) ``` ## Customising Charts diff --git a/florence/src-js/florence/renderer/CanvasRenderer.scala b/florence/src-js/florence/renderer/CanvasRenderer.scala index c8d675d..6c4160c 100644 --- a/florence/src-js/florence/renderer/CanvasRenderer.scala +++ b/florence/src-js/florence/renderer/CanvasRenderer.scala @@ -178,8 +178,11 @@ object CanvasRendererExtensions: import florence.core.model.styling.WithCommonProps extension [C <: Chart, S <: ChartStyle](styled: StyledChart[C, S]) - - def renderToFit(ctx: CanvasRenderingContext2D)(using + /** Scale X and Y to fill the canvas + * Aspect ratio not preserved + * Layout not recomputed + */ + def renderFillCanvas(ctx: CanvasRenderingContext2D)(using interpreter: Interpreter[StyledChart[C, S], Drawing] ): Unit = val cw = ctx.canvas.width.toDouble @@ -193,8 +196,10 @@ object CanvasRendererExtensions: CanvasRenderer.render(drw.transformed(sx = sx, sy = sy), ctx) extension [C <: Chart, S <: ChartStyle](chart: C) - - def renderWithFit(style: S, ctx: CanvasRenderingContext2D)(using + /** Scale chart and style to fill the canvas + * Same behavior as renderFillCanvas + */ + def renderFillCanvasWith(style: S, ctx: CanvasRenderingContext2D)(using interpreter: Interpreter[(C, S), Drawing] ): Unit = val cw = ctx.canvas.width.toDouble @@ -208,8 +213,11 @@ object CanvasRendererExtensions: CanvasRenderer.render(drw.transformed(sx = sx, sy = sy), ctx) extension (styled: StyledChart[LineChart, LineChartStyle]) - - def renderToResize(ctx: CanvasRenderingContext2D)(using + /** Redraw at canvas width and height + * No distortion + * Layout recomputed for ticks labels and margins + */ + def renderAtCanvasSize(ctx: CanvasRenderingContext2D)(using interpreter: Interpreter[StyledChart[LineChart, LineChartStyle], Drawing] ): Unit = val cw = ctx.canvas.width @@ -227,8 +235,10 @@ object CanvasRendererExtensions: CanvasRenderer.render(drw, ctx) extension (chart: LineChart) - - def renderWithResize(style: LineChartStyle, ctx: CanvasRenderingContext2D)(using + /** Redraw with style at canvas size + * Same as renderAtCanvasSize but takes chart and style separately + */ + def renderAtCanvasSizeWith(style: LineChartStyle, ctx: CanvasRenderingContext2D)(using interpreter: Interpreter[(LineChart, LineChartStyle), Drawing] ): Unit = val cw = ctx.canvas.width diff --git a/sandbox-tyrian/src/florence/sandbox/CustomChart.scala b/sandbox-tyrian/src/florence/sandbox/CustomChart.scala index 1491489..d74b19d 100644 --- a/sandbox-tyrian/src/florence/sandbox/CustomChart.scala +++ b/sandbox-tyrian/src/florence/sandbox/CustomChart.scala @@ -84,4 +84,4 @@ object CustomChart: def renderChart(canvas: HTMLCanvasElement): Unit = chart .withStyling(style) - .renderToResize(canvas.getContext2D()) + .renderAtCanvasSize(canvas.getContext2D()) diff --git a/sandbox/src/florence/sandbox/js/Example.scala b/sandbox/src/florence/sandbox/js/Example.scala index 3707c27..7c356ce 100644 --- a/sandbox/src/florence/sandbox/js/Example.scala +++ b/sandbox/src/florence/sandbox/js/Example.scala @@ -186,6 +186,6 @@ object Example: .withHeight(400) .withMargins(Margins(40, 80, 50, 110)) val styledChart = chart.withStyling(style) - styledChart.renderToResize(canvas.getContext2D()) + styledChart.renderAtCanvasSize(canvas.getContext2D()) end renderChart end Example From b19c328e52227145562ab3c53fd8ae5338ef71d4 Mon Sep 17 00:00:00 2001 From: Dario Abdulrehman Date: Tue, 7 Oct 2025 10:25:54 +0100 Subject: [PATCH 5/5] Remove non-uniform fill helpers and docs, keep redraw-only API (renderAtCanvasSize*) --- README.md | 2 - .../florence/renderer/CanvasRenderer.scala | 40 ++----------------- 2 files changed, 3 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 7d11176..05768c3 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,6 @@ styledChart.renderTo(canvas.getContext2D()) // Fit by redrawing at canvas size (no distortion) styledChart.renderAtCanvasSize(canvas.getContext2D()) -// Or non-uniform scale to fill (may distort) -styledChart.renderFillCanvas(canvas.getContext2D()) ``` ## Customising Charts diff --git a/florence/src-js/florence/renderer/CanvasRenderer.scala b/florence/src-js/florence/renderer/CanvasRenderer.scala index 6c4160c..01fae0c 100644 --- a/florence/src-js/florence/renderer/CanvasRenderer.scala +++ b/florence/src-js/florence/renderer/CanvasRenderer.scala @@ -171,48 +171,13 @@ object CanvasRendererExtensions: CanvasRenderer.render(drawing, ctx) import florence.core.dsl.styling.StyledChart - import florence.core.model.Chart import florence.core.model.Chart.LineChart - import florence.core.model.styling.ChartStyle import florence.core.model.styling.ChartStyle.LineChartStyle - import florence.core.model.styling.WithCommonProps - extension [C <: Chart, S <: ChartStyle](styled: StyledChart[C, S]) - /** Scale X and Y to fill the canvas - * Aspect ratio not preserved - * Layout not recomputed - */ - def renderFillCanvas(ctx: CanvasRenderingContext2D)(using - interpreter: Interpreter[StyledChart[C, S], Drawing] - ): Unit = - val cw = ctx.canvas.width.toDouble - val ch = ctx.canvas.height.toDouble - val propsS = summon[WithCommonProps[ChartStyle]].getCommonProps(styled.style) - val w = propsS.width - val h = propsS.height - val sx = if w == 0 then 1.0 else cw / w.toDouble - val sy = if h == 0 then 1.0 else ch / h.toDouble - val drw = interpreter.interpret(styled) - CanvasRenderer.render(drw.transformed(sx = sx, sy = sy), ctx) - - extension [C <: Chart, S <: ChartStyle](chart: C) - /** Scale chart and style to fill the canvas - * Same behavior as renderFillCanvas - */ - def renderFillCanvasWith(style: S, ctx: CanvasRenderingContext2D)(using - interpreter: Interpreter[(C, S), Drawing] - ): Unit = - val cw = ctx.canvas.width.toDouble - val ch = ctx.canvas.height.toDouble - val propsS = summon[WithCommonProps[ChartStyle]].getCommonProps(style) - val w = propsS.width - val h = propsS.height - val sx = if w == 0 then 1.0 else cw / w.toDouble - val sy = if h == 0 then 1.0 else ch / h.toDouble - val drw = interpreter.interpret((chart, style)) - CanvasRenderer.render(drw.transformed(sx = sx, sy = sy), ctx) + extension (styled: StyledChart[LineChart, LineChartStyle]) + /** Redraw at canvas width and height * No distortion * Layout recomputed for ticks labels and margins @@ -235,6 +200,7 @@ object CanvasRendererExtensions: CanvasRenderer.render(drw, ctx) extension (chart: LineChart) + /** Redraw with style at canvas size * Same as renderAtCanvasSize but takes chart and style separately */