import io.data2viz.axis.Orient import io.data2viz.axis.axis import io.data2viz.color.Colors import io.data2viz.color.Color import io.data2viz.geom.Size import io.data2viz.geom.size import io.data2viz.scale.Scales import io.data2viz.viz.* import kotlin.browser.document import kotlin.browser.window import org.w3c.dom.HTMLCanvasElement import kotlin.math.cos import kotlin.math.max import kotlin.math.sin import kotlin.properties.Delegates.observable import io.data2viz.timer.timer data class Range(val min: Double, val max: Double) fun range(min: Double, max: Double) = Range(min, max) class Plot( val points: MutableList<Double> = mutableListOf(), val stroke: Color = Colors.Web.steelblue, val strokeWidth: Double = 1.5 ) class LinePlotter(val size: Size, val plots: List<Plot>) { val labels: MutableList<Double> = mutableListOf<Double>() // TODO make margins and size mutable // TODO checks that margins aren't bigger than size val margins = Margins(40.5, 30.5, 50.5, 50.5) val chartWidth = size.width - margins.hMargins val chartHeight = size.height - margins.vMargins var xRange: Range by observable( range(labels.firstOrNull() ?: 0.0, labels.lastOrNull() ?: 0.0) ) { _, old, new -> if (old != new) xScale = newXScale() } var yRange: Range by observable( range( plots.mapNotNull { it.points.min() }.min() ?: 0.0 , plots.mapNotNull { it.points.max() }.max() ?: 0.0 ) ) { _, old, new -> if (old != new) yScale = newYScale() } // linear scale for x private var xScale = newXScale() private fun newXScale() = Scales.Continuous.linear { domain = listOf(xRange.min, xRange.max) range = listOf(.0, chartWidth) } // linear scale for y private var yScale = newYScale() private fun newYScale() = Scales.Continuous.linear { domain = listOf(yRange.min, yRange.max) range = listOf(chartHeight, 0.0) // <- y is mapped in the reverse order (in SVG, javafx (0,0) is top left. } private fun GroupNode.xAxis() = group { transform { translate(y = chartHeight + 10) } axis(Orient.BOTTOM, xScale) } private fun GroupNode.yAxis() = group { transform { translate(x = -10.0) } axis(Orient.LEFT, yScale) } private var redraw: Boolean = false private fun Viz.build() { group { transform { translate(x = margins.left, y = margins.top) } group { yAxis() } group { xAxis() } group { plots.forEach { plot -> group { path { fill = null stroke = plot.stroke strokeWidth = plot.strokeWidth if (labels.isNotEmpty()) { moveTo(xScale(labels[0]), yScale(plot.points[0])) for (i in 1 until labels.size) { lineTo(xScale(labels[i]), yScale(plot.points[i])) } } } } } } } } val visual: Viz = viz { size = this@LinePlotter.size build() } init { visual.animation { time -> // update at most 25 frame per second if (time >= 40.0) { if (redraw) { visual.clear() visual.build() redraw = false } } } } fun invalidate() { redraw = true } fun push(x: Double, ys: List<Double>) { labels.add(x) // updates xRange xRange = xRange.copy(max = max(xRange.max, x)) var newMin = yRange.min var newMax = yRange.max for (i in plots.indices) { val y = ys[i] plots[i].points.add(y) if (y < newMin) newMin = y if (y > newMax) newMax = y } // update yRange yRange = range(newMin, newMax) invalidate() } } fun main() { val plots = listOf( Plot(stroke = Colors.Web.yellow, strokeWidth = 4.0), Plot(stroke = Colors.Web.red) ) val plotter = LinePlotter(size(960.0, 500.0), plots) plotter.visual.bindRendererOnNewCanvas() var current = 0.0 val step = 1.0 / 100.0 timer(200.0) { repeat(20) { plotter.push(current, listOf(cos(current+3.14/2.0), sin(current))) current += step } } }