Imaging Vector Rendering
KorIM support vector rendering by the `Context2d` class. This class is meant to simulate the JS HTML Canvas API, but hyper-vitaminated.
Context2d
Context2d is a class that mimics the HTML Canvas API, it implements the VectorBuilder interface.
You can get a Context2d from a NativeImage
or a Bitmap32
.
NOTE: Context2d with Bitmap32 has a performance and quality that might be subpar with native implementations, but it is consistent among targets
Constructing
The typical usage is by calling the Bitmap.context2d
method.
bitmap.context2d { // this: Context2d ->
// ...
}
You can get an instance with getContext2d
, but then you are on your own to dispose the instance after finishing.
val context2d = bitmap.getContext2d(antialiasing = true)
run {
// ...
}
context2d.dispose() // We must dispose it once finished to release resources and write back the data
API
class Context2d : Disposable, VectorBuilder {
val renderer: Renderer
val defaultFontRegistry: FontRegistry? = null
val defaultFont: Font? = null
var debug: Boolean
val width: Int
val height: Int
// Last moveTo registered position
var moveX: Double
var moveY: Double
fun dispose()
fun withScaledRenderer(scaleX: Double, scaleY: Double = scaleX): Context2d = if (scaleX == 1.0 && scaleY == 1.0) this else Context2d(ScaledRenderer(renderer, scaleX, scaleY))
inline fun <T> buffering(callback: () -> T): T
}
class Context2d.ScaledRenderer(val parent: Renderer, val scaleX: Double, val scaleY: Double) : Renderer()
Bounds
You can get the bounds of your vector graphics:
fun Context2d.getBounds(out: Rectangle = Rectangle()): Rectangle
Vector building
Context2d implements the basic VectorBuilder interface, so you can call all the extension methods for building different shapes.
// From VectorBuilder
override var lastX: Double
override var lastY: Double
override val totalPoints: Int
fun beginPath()
override fun close()
override fun moveTo(x: Double, y: Double)
override fun lineTo(x: Double, y: Double)
override fun quadTo(cx: Double, cy: Double, ax: Double, ay: Double)
override fun cubicTo(cx1: Double, cy1: Double, cx2: Double, cy2: Double, ax: Double, ay: Double)
Drawing
You can also draw (put vectors) a GraphicsPath and a Drawable
fun Context2d.path(path: GraphicsPath)
fun Context2d.draw(d: Drawable)
Direct filling / stroking
For some basic stuff, there are methods that directly draw things without having to call the stroke
or fill
methods.
fun Context2d.drawShape(
shape: Drawable,
rasterizerMethod: ShapeRasterizerMethod = ShapeRasterizerMethod.X4
)
fun Context2d.drawImage(image: Bitmap, x: Number, y: Number, width: Number = image.width, height: Number = image.height)
fun Context2d.strokeRect(x: Number, y: Number, width: Number, height: Number)
fun Context2d.fillRect(x: Number, y: Number, width: Number, height: Number)
fun Context2d.fillRoundRect(x: Number, y: Number, width: Number, height: Number, rx: Number, ry: Number = rx)
Filling and stroking
Once you have your vectors added, you can stroke those paths or fill them.
Filling and stroking with the paints provided in the state
fun Context2d.stroke()
fun Context2d.fill()
fun Context2d.fillStroke()
Filling the current vector with a specific paint
fun Context2d.fill(paint: Paint)
fun Context2d.stroke(paint: Paint)
Block for filling, stroking or both
You can also call a block, that will render the vectors you draw inside the block. This is usually the preferred way, since it is clearer.
fun Context2d.fill(paint: Paint, begin: Boolean = true, block: () -> Unit)
fun Context2d.stroke(paint: Paint, lineWidth: Double = this.lineWidth, lineCap: LineCap = this.lineCap, lineJoin: LineJoin = this.lineJoin, begin: Boolean = true, callback: () -> Unit)
fun Context2d.stroke(paint: Paint, info: StrokeInfo, begin: Boolean = true, callback: () -> Unit)
fun Context2d.fillStroke(fill: Paint, stroke: Paint, callback: () -> Unit)
Clipping
fun Context2d.unclip()
fun Context2d.clip(path: VectorPath? = state.path, winding: Winding = Winding.NON_ZERO)
fun Context2d.clip(path: VectorPath?, winding: Winding = Winding.NON_ZERO, block: () -> Unit)
fun Context2d.clip(path: VectorPath.() -> Unit, winding: Winding = Winding.NON_ZERO, block: () -> Unit)
Fills
fun Context2d.createLinearGradient(x0: Number, y0: Number, x1: Number, y1: Number, cycle: CycleMethod = CycleMethod.NO_CYCLE, transform: Matrix = Matrix(), block: GradientPaint.() -> Unit = {}): LinearGradientPaint
fun Context2d.createRadialGradient(x0: Number, y0: Number, r0: Number, x1: Number, y1: Number, r1: Number, cycle: CycleMethod = CycleMethod.NO_CYCLE, transform: Matrix = Matrix(), block: GradientPaint.() -> Unit = {}): RadialGradientPaint
fun Context2d.createSweepGradient(x0: Number, y0: Number, transform: Matrix = Matrix(), block: GradientPaint.() -> Unit = {}): SweepGradientPaint
fun Context2d.createColor(color: RGBA): RGBA
fun Context2d.createPattern(
bitmap: Bitmap,
cycleX: CycleMethod = CycleMethod.NO_CYCLE,
cycleY: CycleMethod = cycleX,
smooth: Boolean = true,
transform: Matrix = Matrix()
): BitmapPaint
Rendering State
var Context2d.lineScaleMode: LineScaleMode
var Context2d.lineWidth: Double
var Context2d.lineCap: LineCap
var Context2d.startLineCap: LineCap
var Context2d.endLineCap: LineCap
var Context2d.lineJoin: LineJoin
var Context2d.strokeStyle: Paint
var Context2d.fillStyle: Paint
var Context2d.fontRegistry: FontRegistry?
var Context2d.font: Font?
var Context2d.fontName: String?
var Context2d.fontSize: Double
var Context2d.verticalAlign: VerticalAlign
var Context2d.horizontalAlign: HorizontalAlign
var Context2d.alignment: TextAlignment
var Context2d.globalAlpha: Double
var Context2d.globalCompositeOperation: CompositeOperation
Temporarily set some states
Equivalent to calling keep
and setting state properties manually.
fun Context2d.fillStyle(paint: Paint, callback: () -> Unit)
fun Context2d.strokeStyle(paint: Paint, callback: () -> Unit)
fun Context2d.font(
font: Font? = this.font,
halign: HorizontalAlign = this.horizontalAlign,
valign: VerticalAlign = this.verticalAlign,
fontSize: Double = this.fontSize,
callback: () -> Unit
)
Text
fun Context2d.fillText(text: String, x: Double, y: Double) = rendererRenderSystemText(state, font, fontSize, text, x, y, fill = true)
fun Context2d.strokeText(text: String, x: Double, y: Double) = rendererRenderSystemText(state, font, fontSize, text, x, y, fill = false)
fun Context2d.getTextBounds(text: String, out: TextMetrics = TextMetrics()): TextMetrics
fun Context2d.strokeText(text: String, x: Number, y: Number): Unit
fun Context2d.fillText(text: String, x: Number, y: Number): Unit
fun Context2d.fillText(
text: String,
x: Number,
y: Number,
font: Font? = this.font,
fontSize: Double = this.fontSize,
halign: HorizontalAlign = this.horizontalAlign,
valign: VerticalAlign = this.verticalAlign,
color: Paint? = null
): Unit
fun <T> Context2d.drawText(text: T, x: Double = 0.0, y: Double = 0.0, fill: Boolean = true, paint: Paint? = null, font: Font? = this.font, size: Double = this.fontSize, renderer: TextRenderer<T> = DefaultStringTextRenderer as TextRenderer<T>)
Transformations
Context2d has an internal affine transformation, so all the paintings happening at some point have that transformation applied.
Transforming
fun Context2d.scale(sx: Number, sy: Number = sx, block: () -> Unit = {})
fun Context2d.translate(tx: Number, ty: Number, block: () -> Unit = {})
fun Context2d.rotate(angle: Angle, block: () -> Unit = {})
fun Context2d.rotateDeg(degs: Number)
fun Context2d.shear(sx: Double, sy: Double)
fun Context2d.transform(m: Matrix)
fun Context2d.setTransform(m: Matrix)
fun Context2d.transform(a: Double, b: Double, c: Double, d: Double, tx: Double, ty: Double)
fun Context2d.setTransform(a: Double, b: Double, c: Double, d: Double, tx: Double, ty: Double)
Keeping the transformations
fun Context2d.keepApply(callback: Context2d.() -> Unit)
fun Context2d.keep(callback: () -> Unit)
fun Context2d.keepTransform(callback: () -> Unit)
fun Context2d.save()
fun Context2d.restore()
You would typically call the keep
block when transforming,
so code outside that block is not affected by the new transformations:
...context2d {
keep {
translate(10, 20)
scale(2)
}
// Returns to the original transformation
}
VectorBuilder (KorMA)
Since Context2d implements VectorBuilder, you can use these extension methods for simplifying drawing other shapes:
fun VectorBuilder.isEmpty(): Boolean
fun VectorBuilder.isNotEmpty(): Boolean
fun VectorBuilder.arcTo(ax: Double, ay: Double, cx: Double, cy: Double, r: Double)
fun VectorBuilder.rect(x: Double, y: Double, width: Double, height: Double)
fun VectorBuilder.rectHole(x: Double, y: Double, width: Double, height: Double)
fun VectorBuilder.roundRect(x: Double, y: Double, w: Double, h: Double, rx: Double, ry: Double = rx)
fun VectorBuilder.arc(x: Double, y: Double, r: Double, start: Double, end: Double)
fun VectorBuilder.circle(x: Double, y: Double, radius: Double)
fun VectorBuilder.ellipse(x: Double, y: Double, rw: Double, rh: Double)
fun VectorBuilder.moveTo(p: Point)
fun VectorBuilder.lineTo(p: Point)
fun VectorBuilder.quadTo(c: Point, a: Point)
fun VectorBuilder.cubicTo(c1: Point, c2: Point, a: Point)
fun VectorBuilder.moveTo(x: Number, y: Number)
fun VectorBuilder.lineTo(x: Number, y: Number)
fun VectorBuilder.quadTo(controlX: Number, controlY: Number, anchorX: Number, anchorY: Number)
fun VectorBuilder.cubicTo(cx1: Number, cy1: Number, cx2: Number, cy2: Number, ax: Number, ay: Number)
fun VectorBuilder.moveToH(x: Number)
fun VectorBuilder.rMoveToH(x: Number)
fun VectorBuilder.moveToV(y: Number)
fun VectorBuilder.rMoveToV(y: Number)
fun VectorBuilder.lineToH(x: Number)
fun VectorBuilder.rLineToH(x: Number)
fun VectorBuilder.lineToV(y: Number)
fun VectorBuilder.rLineToV(y: Number)
fun VectorBuilder.rMoveTo(x: Number, y: Number)
fun VectorBuilder.rLineTo(x: Number, y: Number)
fun VectorBuilder.rQuadTo(cx: Number, cy: Number, ax: Number, ay: Number)
fun VectorBuilder.rCubicTo(cx1: Number, cy1: Number, cx2: Number, cy2: Number, ax: Number, ay: Number)
fun VectorBuilder.arcTo(ax: Number, ay: Number, cx: Number, cy: Number, r: Number)
fun VectorBuilder.rect(x: Number, y: Number, width: Number, height: Number)
fun VectorBuilder.rectHole(x: Number, y: Number, width: Number, height: Number)
fun VectorBuilder.roundRect(x: Number, y: Number, w: Number, h: Number, rx: Number, ry: Number = rx)
fun VectorBuilder.arc(x: Number, y: Number, r: Number, start: Number, end: Number)
fun VectorBuilder.circle(x: Number, y: Number, radius: Number)
fun VectorBuilder.ellipse(x: Number, y: Number, rw: Number, rh: Number)
fun VectorBuilder.star(points: Int, radiusSmall: Double, radiusBig: Double, rotated: Angle = 0.degrees, x: Double = 0.0, y: Double = 0.0)
fun VectorBuilder.regularPolygon(points: Int, radius: Double, rotated: Angle = 0.degrees, x: Double = 0.0, y: Double = 0.0)
fun VectorBuilder.starHole(points: Int, radiusSmall: Double, radiusBig: Double, rotated: Angle = 0.degrees, x: Double = 0.0, y: Double = 0.0)
fun VectorBuilder.regularPolygonHole(points: Int, radius: Double, rotated: Angle = 0.degrees, x: Double = 0.0, y: Double = 0.0)
fun VectorBuilder.polygon(path: IPointArrayList, close: Boolean = true)
fun VectorBuilder.polygon(path: Array<IPoint>, close: Boolean = true)
fun VectorBuilder.polygon(path: List<IPoint>, close: Boolean = true
GraphicsPath
GraphicsPath is a normal VectorPath that is also a SizedDrawable
, so it can be renderer in a Context2d with the draw(drawable: Drawable)
method.
Paint
interface Paint {
fun transformed(m: Matrix): Paint
}
val DefaultPaint: Paint get() = Colors.BLACK
NonePaint
object NonePaint : Paint
ColorPaint
ColorPaint is an alias of the RGBA class, that represents a single solid color with opacity.
TransformedPaint
A Transformed Paint is a paint that can have an affine transformation Matrix. That’s used for bitmap and gradient paints.
interface TransformedPaint : Paint { val transform: Matrix }
GradientPaint
Gradient paints can be linear, radial and sweep (sweep only supported in some targets and Bitmap32).
enum class GradientKind { LINEAR, RADIAL, SWEEP }
enum class GradientUnits { USER_SPACE_ON_USE, OBJECT_BOUNDING_BOX }
enum class GradientInterpolationMethod { LINEAR, NORMAL }
data class GradientPaint(
val kind: GradientKind,
val x0: Double,
val y0: Double,
val r0: Double,
val x1: Double,
val y1: Double,
val r1: Double,
val stops: DoubleArrayList = DoubleArrayList(),
val colors: IntArrayList = IntArrayList(),
val cycle: CycleMethod = CycleMethod.NO_CYCLE,
override val transform: Matrix = Matrix(),
val interpolationMethod: GradientInterpolationMethod = GradientInterpolationMethod.NORMAL,
val units: GradientUnits = GradientUnits.OBJECT_BOUNDING_BOX
) : TransformedPaint {
fun x0(m: Matrix) = m.transformX(x0, y0)
fun y0(m: Matrix) = m.transformY(x0, y0)
fun r0(m: Matrix) = m.transformX(r0, r0)
fun x1(m: Matrix) = m.transformX(x1, y1)
fun y1(m: Matrix) = m.transformY(x1, y1)
fun r1(m: Matrix) = m.transformX(r1, r1)
val numberOfStops: Int
companion object {
fun identity(kind: GradientKind): GradientPaint
fun gradientBoxMatrix(width: Double, height: Double, rotation: Angle, tx: Double, ty: Double, out: Matrix = Matrix()): Matrix
fun fromGradientBox(kind: GradientKind, width: Double, height: Double, rotation: Angle, tx: Double, ty: Double): GradientPaint
}
fun addColorStop(stop: Number, color: RGBA): GradientPaint = add(stop.toDouble(), color)
fun add(stop: Double, color: RGBA): GradientPaint
val untransformedGradientMatrix: Matrix
val gradientMatrix: Matrix
val gradientMatrixInv = gradientMatrix.inverted()
fun getRatioAt(x: Double, y: Double): Double
fun getRatioAt(x: Double, y: Double, m: Matrix): Double
fun applyMatrix(m: Matrix): GradientPaint
}
Constructing GradientPaint
fun LinearGradientPaint(x0: Number, y0: Number, x1: Number, y1: Number, cycle: CycleMethod = CycleMethod.NO_CYCLE, transform: Matrix = Matrix(), block: GradientPaint.() -> Unit = {}): GradientPaint
fun RadialGradientPaint(x0: Number, y0: Number, r0: Number, x1: Number, y1: Number, r1: Number, cycle: CycleMethod = CycleMethod.NO_CYCLE, transform: Matrix = Matrix(), block: GradientPaint.() -> Unit = {}): GradientPaint
fun SweepGradientPaint(x0: Number, y0: Number, transform: Matrix = Matrix(), block: GradientPaint.() -> Unit = {}): GradientPaint
BitmapPaint
class BitmapPaint(
val bitmap: Bitmap,
override val transform: Matrix,
val cycleX: CycleMethod = CycleMethod.NO_CYCLE,
val cycleY: CycleMethod = CycleMethod.NO_CYCLE,
val smooth: Boolean = true
) : TransformedPaint {
val repeatX: Boolean get() = cycleX.repeating
val repeatY: Boolean get() = cycleY.repeating
val repeat: Boolean get() = repeatX || repeatY
val bmp32 = bitmap.toBMP32()
}
Drawable, SizedDrawable and BoundsDrawable
All these interfaces allows to draw to a Context2d, and each one provides more information about metrics and expected size. That extra information is used by some code to generate bitmaps of the right size or determine the bounds of an object.
interface Drawable {
fun draw(c: Context2d)
}
interface SizedDrawable : Drawable {
val width: Int
val height: Int
}
interface BoundsDrawable : SizedDrawable {
val bounds: Rectangle
val left: Int
val top: Int
}
Constructing a Drawable from a lambda
class FuncDrawable(val action: Context2d.() -> Unit) : Drawable {
override fun draw(c: Context2d) {
c.keep {
action(c)
}
}
}
Extra methods
fun <T : Bitmap> Drawable.draw(out: T): T {
out.context2d {
this@draw.draw(this)
}
return out
}
ShapeBuilder
Shape
A shape is a set of vector and styling information that can be rendered in a Context2d.
Shape
The basic interface:
interface Shape : BoundsDrawable {
fun addBounds(bb: BoundsBuilder, includeStrokes: Boolean = false): Unit
fun buildSvg(svg: SvgBuilder): Unit
fun getPath(path: GraphicsPath = GraphicsPath()): GraphicsPath
fun containsPoint(x: Double, y: Double): Boolean
}
fun Shape.getBounds(out: Rectangle = Rectangle()): Rectangle
EmptyShape
When you want to represent a shape that renders nothing, this is your object:
object EmptyShape : Shape
StyledShape
The base interface for FillShape
, PolylineShape
and TextShape
,
with information about the path, the paint, clipping, and transformation.
interface StyledShape : Shape {
val path: GraphicsPath? get() = null
val clip: GraphicsPath?
val paint: Paint
val transform: Matrix
}
FillShape
A shape that renders fillings a solid vector.
data class FillShape(
override val path: GraphicsPath,
override val clip: GraphicsPath?,
override val paint: Paint,
override val transform: Matrix = Matrix()
) : StyledShape
PolylineShape
A shape that renders stroking the contour of a path.
data class PolylineShape(
override val path: GraphicsPath,
override val clip: GraphicsPath?,
override val paint: Paint,
override val transform: Matrix,
val thickness: Double,
val pixelHinting: Boolean,
val scaleMode: LineScaleMode,
val startCaps: LineCap,
val endCaps: LineCap,
val lineJoin: LineJoin,
val miterLimit: Double
) : StyledShape
TextShape
A shape that renders text.
class TextShape(
val text: String,
val x: Double,
val y: Double,
val font: Font?,
val fontSize: Double,
override val clip: GraphicsPath?,
val fill: Paint?,
val stroke: Paint?,
val halign: HorizontalAlign = HorizontalAlign.LEFT,
val valign: VerticalAlign = VerticalAlign.TOP,
override val transform: Matrix = Matrix()
) : StyledShape
CompoundShape
A shape that can hold/group several shapes at once.
class CompoundShape(
val components: List<Shape>
) : Shape
Converting a Drawable into a Shape
You can convert a Drawable and a SizedDrawable into a Shape:
fun Drawable.toShape(width: Int, height: Int): Shape
fun SizedDrawable.toShape(): Shape
SVG
SVG
is interrelated with Shape
. You can convert a Shape into an SVG string.
fun Shape.buildSvg(svg: SvgBuilder): Unit = Unit
fun Shape.toSvg(scale: Double = 1.0): Xml
fun Drawable.toSvg(width: Int, height: Int, scale: Double = 1.0): Xml
fun SizedDrawable.toSvg(scale: Double = 1.0): Xml = toSvg(width, height, scale)
class SvgBuilder(val bounds: Rectangle, val scale: Double) {
val defs = arrayListOf<Xml>()
val nodes = arrayListOf<Xml>()
fun toXml(): Xml
}
BitmapVector
A BitmapVector is a special Bitmap that is lazily created from a BoundsDrawable
instance:
class BitmapVector : Bitmap {
val shape: BoundsDrawable
val bounds: Rectangle
val scale: Double
val rasterizerMethod: ShapeRasterizerMethod
val antialiasing: Boolean
val width: Int
val height: Int
val premultiplied: Boolean
val left: Int
val top: Int
val nativeImage: Bitmap
}
CycleMethod, CompositeOperation, CompositeMode, BlendMode, LineScaleMode, StrokeInfo
CycleMethod
enum class CycleMethod {
NO_CYCLE, REFLECT, REPEAT;
val repeating: Boolean
fun apply(ratio: Double, clamp: Boolean = false): Double
fun apply(value: Double, size: Double): Double
fun apply(value: Double, min: Double, max: Double): Double
companion object {
fun fromRepeat(repeat: Boolean): CycleMethod
}
}
CompositeOperation, CompositeMode, BlendMode
// https://drafts.fxtf.org/compositing-1/
interface CompositeOperation {
companion object {
val UNIMPLEMENTED: CompositeOperation
// CompositeMode
val DEFAULT: CompositeOperation
val CLEAR: CompositeOperation
val COPY: CompositeOperation
val SOURCE_OVER: CompositeOperation
val DESTINATION_OVER: CompositeOperation
val SOURCE_IN: CompositeOperation
val DESTINATION_IN: CompositeOperation
val SOURCE_OUT: CompositeOperation
val DESTINATION_OUT: CompositeOperation
val SOURCE_ATOP: CompositeOperation
val DESTINATION_ATOP: CompositeOperation
val XOR: CompositeOperation
val LIGHTER: CompositeOperation
// BlendMode
val NORMAL: CompositeOperation
val MULTIPLY: CompositeOperation
val SCREEN: CompositeOperation
val OVERLAY: CompositeOperation
val DARKEN: CompositeOperation
val LIGHTEN: CompositeOperation
val COLOR_DODGE: CompositeOperation
val COLOR_BURN: CompositeOperation
val HARD_LIGHT: CompositeOperation
val SOFT_LIGHT: CompositeOperation
val DIFFERENCE: CompositeOperation
val EXCLUSION: CompositeOperation
val HUE: CompositeOperation
val SATURATION: CompositeOperation
val COLOR: CompositeOperation
val LUMINOSITY: CompositeOperation
operator fun invoke(func: (dst: RgbaPremultipliedArray, dstN: Int, src: RgbaPremultipliedArray, srcN: Int, count: Int) -> Unit): CompositeOperation
}
fun blend(dst: RgbaPremultipliedArray, dstN: Int, src: RgbaPremultipliedArray, srcN: Int, count: Int)
}
// https://drafts.fxtf.org/compositing-1/
enum class CompositeMode(val op: CompositeOperation) : CompositeOperation by op {
CLEAR,
COPY,
SOURCE_OVER,
DESTINATION_OVER,
SOURCE_IN,
DESTINATION_IN,
SOURCE_OUT,
DESTINATION_OUT,
SOURCE_ATOP,
DESTINATION_ATOP,
XOR,
LIGHTER;
companion object {
val DEFAULT get() = SOURCE_OVER
}
}
// https://drafts.fxtf.org/compositing-1/
enum class BlendMode(val op: CompositeOperation) : CompositeOperation by op {
NORMAL,
MULTIPLY,
SCREEN,
OVERLAY,
DARKEN,
LIGHTEN,
COLOR_DODGE,
COLOR_BURN,
HARD_LIGHT,
SOFT_LIGHT,
DIFFERENCE,
EXCLUSION,
HUE,
SATURATION,
COLOR,
LUMINOSITY,
ADDITION,
SUBTRACT,
DIVIDE,
}
LineScaleMode, StrokeInfo
enum class LineScaleMode {
NONE, HORIZONTAL, VERTICAL, NORMAL;
val hScale: Boolean
val vScale: Boolean
}
class StrokeInfo {
val thickness: Double
val pixelHinting: Boolean
val scaleMode: LineScaleMode
val startCap: LineCap
val endCap: LineCap
val lineJoin: LineJoin
val miterLimit: Double
}
Chart
abstract class Chart() : Drawable {
abstract fun Context2d.renderChart()
}
ChartBars
open class ChartBars(val list: List<DataPoint>) : Chart() {
companion object {
operator fun invoke(vararg items: Pair<String, Number>): ChartBars
fun fromPoints(vararg items: Pair<String, List<Number>>): ChartBars
}
data class DataPoint(val name: String, val values: List<Double>) {
val localMaxValue = values.maxOrNull() ?: 0.0
}
val maxValue = list.map { it.localMaxValue }.maxOrNull() ?: 0.0
val chartStep = 10.0.pow(floor(log10(maxValue))) / 2.0
val rMaxValue = ceil(maxValue / chartStep) * chartStep
val colors = listOf(Colors["#5485ec"], Colors.GREEN, Colors.BLUE, Colors.AZURE, Colors.CHARTREUSE, Colors.CADETBLUE)
enum class Fit(val angle: Double) { FULL(0.0), DEG45(-45.0), DEG90(-90.0) }
fun Context2d.renderBars(rect: Rectangle)
}