Mixed typesafe / mutable

Start mutable and add a little safety

The "mutuable" approach combines the previous two ideas.We could use a "typesafe" part of the DSL, to create the part we want, then pickle it to JSON and put it in our spec...

import viz.dsl.vega.*
import viz.vega.plots.{BarChart, given}
import viz.dsl.Conversion.u

val axisOrient : TitleOrientEnum = TitleOrientEnum.top
val newAxis : Axis= Axis(orient = axisOrient, scale = "xscale")

val chart = BarChart(
    List(
        viz.Utils.removeXAxis,
        viz.Utils.fillDiv,
        spec => spec("axes") = spec("axes").arr :+ newAxis.u
    )
)
viz.js.showChartJs(chart, node)

Note that we get some typechecking.

import viz.dsl.vega.*

val axisOrient : TitleOrientEnum = "Not an orientation" // This can one of the TitleOrientEnum values... visit in IDE for help.

// error:
// Found:    ("Not an orientation" : String)
// Required: viz.dsl.vega.TitleOrientEnum
// val parsed :  Either[io.circe.Error, viz.dsl.vega.Axis] = decode[Axis](copyPastedAxisFromExample)
//                                                          ^

We can also take this to greater extremes, and in a sort of magpie style, parse more complex parts of the spec.

Shortcuts

import viz.dsl.vega.*
import io.circe._, io.circe.parser._

// Steal a part of the spec you want from an example.
val copyPastedAxisFromExample = """
{ "orient": "top", "scale": "xscale" }
"""
// copyPastedAxisFromExample: String = """
//     { "orient": "top", "scale": "xscale" }
//     """
val parsed :  Either[io.circe.Error, viz.dsl.vega.Axis] = decode[Axis](copyPastedAxisFromExample)
// parsed: Either[Error, Axis] = Right(
//   value = Axis(
//     aria = None,
//     bandPosition = None,
//     description = None,
//     domain = None,
//     domainCap = None,
//     domainColor = None,
//     domainDash = None,
//     domainDashOffset = None,
//     domainOpacity = None,
//     domainWidth = None,
//     encode = None,
//     format = None,
//     formatType = None,
//     grid = None,
//     gridCap = None,
//     gridColor = None,
//     gridDash = None,
//     gridDashOffset = None,
//     gridOpacity = None,
//     gridScale = None,
//     gridWidth = None,
//     labelAlign = None,
//     labelAngle = None,
//     labelBaseline = None,
//     labelBound = None,
//     labelColor = None,
//     labelFlush = None,
//     labelFlushOffset = None,
//     labelFont = None,
//     labelFontSize = None,
//     labelFontStyle = None,
//     labelFontWeight = None,
//     labelLimit = None,
//     labelLineHeight = None,
//     labelOffset = None,
//     labelOpacity = None,
//     labelOverlap = None,
//     labelPadding = None,
//     labels = None,
//     labelSeparation = None,
//     maxExtent = None,
//     minExtent = None,
//     offset = None,
//     orient = top,
//     position = None,
//     scale = "xscale",
//     tickBand = None,
// ...
val parsedU : ujson.Value = ujson.read(copyPastedAxisFromExample)
// parsedU: Value = Obj(
//   value = Map("orient" -> Str(value = "top"), "scale" -> Str(value = "xscale"))
// )

You could then use either strategy to insert into a spec, depending on whether you're starting from the "mutable" or "typesafe" approach.

import viz.vega.plots.{BarChart, given}

val copyPastedAxisFromExample = """{ "orient": "top", "scale": "xscale" }"""
val parsedU = ujson.read(copyPastedAxisFromExample)

val chart = BarChart(
    List(
        viz.Utils.fillDiv,
        spec => spec("axes") = parsedU // overwrites the entire exes property with our single weird top axis.
    )
)
viz.js.showChartJs(chart, node)

Discussion

This is my clearly favoured strategy. Start with a chart which works, construct "typesafe" modifier for the parts I want to change, and apply them to the chart. I have found this to be effective.