Move Charts to the platform

GitOrigin-RevId: 5cd6ffb9e48459c3a50cf238d49fdf383f2022f5
This commit is contained in:
maksim.pelevin
2021-06-30 15:45:37 +00:00
committed by intellij-monorepo-bot
parent 9e73a1094a
commit 5135570523
10 changed files with 1922 additions and 0 deletions
@@ -0,0 +1,148 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.ui.charts
import java.awt.Graphics2D
import java.awt.Rectangle
import java.awt.geom.Area
import java.lang.Double.min
import javax.swing.SwingConstants
import kotlin.math.abs
import kotlin.math.max
class BarDataset<T: Number>: Dataset<T>() {
var stacked: Boolean = false
var values: Iterable<T>
get() = data
set(value) {
data = value
}
var showValues : ((T) -> String)? = null
init {
fillColor = lineColor
}
companion object {
@JvmStatic
fun <T: Number> of(vararg values: T): BarDataset<T> = BarDataset<T>().apply {
data = values.toList()
}
}
}
abstract class BarChart<T: Number>: GridChartWrapper<Int, T>() {
var datasets: List<BarDataset<T>> = mutableListOf()
override val ranges: Grid<Int, T> = Grid()
var gap: Int = 10
var space: Int = -1;
override fun paintComponent(g: Graphics2D) {
val xy = findMinMax()
if (xy.isInitialized) {
val datasetCount = getDatasetCount()
val grid = g.create(margins.left, margins.top, gridWidth, gridHeight) as Graphics2D
paintGrid(grid, g, xy)
var index = 0
datasets.forEach { dataset ->
dataset.paintDataset(index, datasetCount, grid, xy)
if (!dataset.stacked) {
index++
}
}
grid.dispose()
}
}
fun getDatasetCount() = datasets.map { if (it.stacked) 0 else 1 }.sum()
override fun findMinMax() = if (ranges.isInitialized) ranges else ranges * (ranges + MinMax()).apply {
datasets.forEach { it.data.forEachIndexed { i, v -> process(i, v) } }
}
protected abstract fun BarDataset<T>.paintDataset(datasetIndex: Int, datasetCount: Int, g: Graphics2D, xy: MinMax<Int, T>)
}
class HorizontalBarChart<T: Number> : BarChart<T>() {
override fun BarDataset<T>.paintDataset(datasetIndex: Int, datasetCount: Int, g: Graphics2D, xy: MinMax<Int, T>) {
assert(xy.xMin == 0) { "Int value must start with 0" }
val columns = xy.xMax + 1
val max = max(xy.yMax.toDouble(), 0.0)
val min = min(xy.yMin.toDouble(), 0.0)
if (max == min) {
return
}
val cb = g.clipBounds
val z = cb.height / (max - min)
val axis = (z * abs(min)).toInt()
data.forEachIndexed { column, value ->
var h = (value.toDouble() * z).toInt()
var y = cb.height - h - axis
val groupW = cb.width / columns - gap
val groupX = column * cb.width / columns + gap / 2
val space = if (space < 0) max(1, groupW / 10) else space
val w = max(1, (groupW - space * (datasetCount - 1)) / datasetCount)
val x = groupX + datasetIndex * w + space * datasetIndex
if (h < 0) {
y += h
h = abs(h)
}
lineColor?.let {
g.paint = lineColor
g.drawRect(x, y, w, h)
}
fillColor?.let {
g.paint = it
g.fillRect(x, y, w, h)
}
if (stacked) {
val area = Area(g.clip)
area.subtract(Area(Rectangle(x, y, w, h)))
g.clip = area
}
showValues?.let { toString ->
val str = toString(value)
val bounds = g.fontMetrics.getStringBounds(str, g)
g.drawString(str, x + (w - bounds.width.toInt()) / 2, y - 5)
}
}
}
override fun findGridLineX(gl: GridLine<Int, T, *>, x: Int): Double {
return gridWidth * ((x + 1).toDouble() - gl.xy.xMin.toDouble()) / (gl.xy.xMax.toDouble() - gl.xy.xMin.toDouble() + 1)
}
override fun findY(xy: MinMax<Int, T>, y: T): Double {
val isRanged = xy.yMin.toDouble() <= 0.0 && 0.0 <= xy.yMax.toDouble()
val yMin = if (isRanged || xy.yMin.toDouble() < 0) xy.yMin.toDouble() else 0.0
val yMax = if (isRanged || xy.yMax.toDouble() > 0) xy.yMax.toDouble() else 0.0
val height = height - (margins.top + margins.bottom)
return height - height * (y.toDouble() - yMin) / (yMax - yMin)
}
override fun findGridLabelOffset(line: GridLine<*, *, *>, g: Graphics2D): Coordinates<Double, Double> {
val onLineAlignment = super.findGridLabelOffset(line, g)
if (line.orientation == SwingConstants.VERTICAL) {
val width = width - (margins.left + margins.right)
val columnWidth = width / (line.xy.xMax.toDouble() - line.xy.xMin.toDouble() + 1)
return (onLineAlignment.x + columnWidth / 2) to onLineAlignment.y
} else {
return onLineAlignment
}
}
}
@@ -0,0 +1,196 @@
@file:JvmName("ChartUtils")
package com.intellij.ui.charts
import java.awt.Insets
import java.util.function.Consumer
// Common DSL
fun <X: Number, Y: Number, T: XYChartComponent<X, Y>> T.ranges(init: MinMax<X, Y>.() -> Unit) {
init(this.ranges)
}
fun <X: Number, Y: Number, T: GridChartWrapper<X, Y>> T.grid(init: Grid<X, Y>.() -> Unit) {
init(this.ranges)
}
fun ChartWrapper.margins(init: Insets.() -> Unit) {
init(this.margins)
}
// Generator DSL
fun <T: Number> enumerator(vararg values: T) : ValueIterable<T> = object : ValueIterable<T>() {
override fun iterator(): Iterator<T> = values.iterator()
}
fun generator(step: Int) : ValueIterable<Int> = object: ValueIterable<Int>() {
override fun iterator(): Iterator<Int> = (min..max step step).iterator()
}
fun generator(step: Long) : ValueIterable<Long> = object: ValueIterable<Long>() {
override fun iterator(): Iterator<Long> = (min..max step step).iterator()
}
fun generator(step: Float) : ValueIterable<Float> = object: ValueIterable<Float>() {
override fun iterator(): Iterator<Float> = object: Iterator<Float> {
var position: Int = 0
override fun hasNext() = position * step + min <= max
override fun next(): Float {
val next = position * step + min
position += 1
return next
}
}
}
fun generator(step: Double) : ValueIterable<Double> = object: ValueIterable<Double>() {
override fun iterator(): Iterator<Double> = object: Iterator<Double> {
var position: Int = 0
override fun hasNext() = position * step + min <= max
override fun next(): Double {
val next = position * step + min
position += 1
return next
}
}
}
// Grid
fun <X: Number, Y: Number> Grid<X, Y>.xPainter(converter: GridLine<X, Y, X>.() -> Unit) {
xPainter = Consumer { converter(it) }
}
fun <X: Number, Y: Number> Grid<X, Y>.yPainter(converter: GridLine<X, Y, Y>.() -> Unit) {
yPainter = Consumer { converter(it) }
}
fun <X: Number, Y: Number, T: Number> Grid<X, Y>.format(str: String) = Consumer<GridLine<X, Y, T>> {
it.label = str.format(it.value)
}
/* XY Line DSL */
class XYDataHolder<X: Number, Y: Number>(val data: MutableList<XYLineDataset<X, Y>>)
class XYDataCreator<X: Number, Y: Number> {
lateinit var x: Iterable<X>
lateinit var y: Iterable<Y>
}
class XYDataGenerator<X: Number, Y: Number> {
lateinit var x: Iterable<X>
lateinit var y: (x: X) -> Y
}
fun <X: Number, Y: Number> lineChart(ink: XYLineChart<X, Y>.() -> Unit) = XYLineChart<X, Y>().apply(ink)
fun <X: Number, Y: Number> XYLineChart<X, Y>.datasets(body: XYDataHolder<X, Y>.() -> Unit) {
datasets = XYDataHolder(mutableListOf<XYLineDataset<X, Y>>()).apply(body).data
}
fun <X: Number, Y: Number> XYDataHolder<X, Y>.dataset(body: XYLineDataset<X, Y>.() -> Unit) {
data.add(XYLineDataset<X, Y>().apply(body))
}
fun <X: Number, Y: Number> XYLineChart<X, Y>.dataset(body: XYLineDataset<X, Y>.() -> Unit) {
datasets = mutableListOf(XYLineDataset<X, Y>().apply(body))
}
fun <X: Number, Y: Number> XYLineDataset<X, Y>.values(creator: XYDataCreator<X, Y>.() -> Unit) {
val values = XYDataCreator<X, Y>().apply(creator)
this.data = object: Iterable<Coordinates<X, Y>> {
override fun iterator(): Iterator<Coordinates<X, Y>> = object: Iterator<Coordinates<X, Y>> {
val xIter = values.x.iterator()
val yIter = values.y.iterator()
override fun hasNext() = xIter.hasNext() && yIter.hasNext()
override fun next() = xIter.next() to yIter.next()
}
}
}
fun <X: Number, Y: Number> XYLineDataset<X, Y>.generate(generator: XYDataGenerator<X, Y>.() -> Unit) {
val iterator = XYDataGenerator<X, Y>().apply(generator)
this.data = object: Iterable<Coordinates<X, Y>> {
override fun iterator(): Iterator<Coordinates<X, Y>> = object: Iterator<Coordinates<X, Y>> {
val i = iterator.x.iterator()
override fun hasNext() = i.hasNext()
override fun next(): Coordinates<X, Y> = i.next().let { it to iterator.y(it) }
}
}
}
/* Category Line DSL */
class CategoryDataHolder<X: Number>(val data: MutableList<CategoryLineDataset<X>>)
fun <X: Number> lineChart(ink: CategoryLineChart<X>.() -> Unit) = CategoryLineChart<X>().apply(ink)
fun <X: Number> CategoryLineChart<X>.datasets(body: CategoryDataHolder<X>.() -> Unit) {
datasets = CategoryDataHolder(mutableListOf<CategoryLineDataset<X>>()).apply(body).data
}
fun <X: Number> CategoryDataHolder<X>.dataset(body: CategoryLineDataset<X>.() -> Unit) {
data.add(CategoryLineDataset<X>().apply(body))
}
fun <X: Number> CategoryLineChart<X>.dataset(body: CategoryLineDataset<X>.() -> Unit) {
datasets = mutableListOf(CategoryLineDataset<X>().apply(body))
}
/* Bar Chart DSL */
class BarDataGenerator<R: Number> {
var count: Int = -1
set(value) {
if (value < 0) throw IllegalArgumentException("Value cannot be less than 0")
field = value
}
lateinit var function: (x: Int) -> R
}
class BarDataHolder<T: Number>(val data: MutableList<BarDataset<T>>)
fun <T: Number> barChart(ink: HorizontalBarChart<T>.() -> Unit) = HorizontalBarChart<T>().apply(ink)
fun <T: Number> HorizontalBarChart<T>.datasets(body: BarDataHolder<T>.() -> Unit) {
datasets = BarDataHolder(mutableListOf<BarDataset<T>>()).apply(body).data
}
fun <T: Number> HorizontalBarChart<T>.dataset(body: BarDataset<T>.() -> Unit) {
datasets = listOf(BarDataset<T>().apply(body))
}
fun <T: Number> BarDataHolder<T>.dataset(body: BarDataset<T>.() -> Unit) {
data.add(BarDataset<T>().apply(body))
}
fun <R: Number> generate(generator: BarDataGenerator<R>.() -> Unit): Iterable<R> {
val iterator = BarDataGenerator<R>().apply(generator)
return object : Iterable<R> {
override fun iterator(): Iterator<R> = object: Iterator<R> {
var index = -1
override fun hasNext() = index < iterator.count
override fun next(): R = iterator.function(++index)
}
}
}
// Timeline helpers
val times = arrayOf(
1, 2, 5, 10, 25, 50, 100, 200, 500, // milliseconds
1_000, 2_000, 5_000, 15_000, 30_000, // seconds
60_000, 120_000, 300_000, 600_000, 900_000, 1_800_000, // minutes
3_600_000, 7_200_000, 14_400_000 // hours
)
fun ChartWrapper.findScale(xMin: Long, xMax: Long, spaceWidth: Int = 100): Int {
val duration = xMax - xMin
val width = width.toDouble()
val lineCount = width / spaceWidth
var scale = times[0]
for (i in times.indices) {
scale = times[i]
if (duration / scale <= lineCount) {
break
}
}
return scale
}
@@ -0,0 +1,403 @@
package com.intellij.ui.charts
import com.intellij.ui.ColorUtil
import com.intellij.ui.JBColor
import com.intellij.util.ui.GraphicsUtil
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.components.BorderLayoutPanel
import org.intellij.lang.annotations.MagicConstant
import java.awt.*
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.util.function.Consumer
import javax.swing.JComponent
import javax.swing.SwingConstants
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
import kotlin.math.sign
interface ChartComponent {
fun paintComponent(g: Graphics2D)
}
/**
* Holds instance of ChartWrapper in which has been added.
*/
abstract class Overlay<T: ChartWrapper>: ChartComponent {
var wrapper: ChartWrapper? = null
set(value) {
field = value
afterChartInitialized()
}
var mouseLocation: Point? = null
/**
* Typed value for {@code wrapper}.
*
* Normally wrapper is already initialized when added to ChartWrapper.
*/
val chart: T
@Suppress("UNCHECKED_CAST") get() = wrapper as T
open fun afterChartInitialized() { }
fun Point.toChartSpace(): Point? = wrapper?.let {
if (it.margins.left < x && x < it.width - it.margins.right && it.margins.top < y && y < it.height - it.margins.bottom) {
Point(x - it.margins.left, y - it.margins.top)
} else null
}
}
interface XYChartComponent<X: Number, Y: Number> {
val ranges: MinMax<X, Y>
}
abstract class GridChartWrapper<X: Number, Y: Number>: ChartWrapper(), XYChartComponent<X, Y> {
abstract override val ranges: Grid<X, Y>
val grid: Grid<X, Y>
get() = ranges
val gridWidth: Int
get() = width - (margins.left + margins.right)
val gridHeight: Int
get() = height - (margins.top + margins.bottom)
var gridColor: Color = JBColor(Color(0xF0F0F0), Color(0x313335))
var gridLabelColor: Color = ColorUtil.withAlpha(JBColor.foreground(), 0.6)
protected fun paintGrid(grid: Graphics2D, chart: Graphics2D, xy: MinMax<X, Y>) {
val gc = Color(gridColor.rgb)
val gcd = gc.darker()
val bounds = grid.clipBounds
val xOrigin: Int = if (ranges.xOriginInitialized) findX(xy, ranges.xOrigin).toInt() else 0
val yOrigin: Int = if (ranges.yOriginInitialized) findY(xy, ranges.yOrigin).toInt() else height
var tmp: Int // — helps to filter line drawing, when lines are met too often
// draws vertical grid lines
tmp = -1
ranges.xLines.apply {
min = xy.xMin
max = xy.xMax
}.forEach {
val gl = GridLine(it, xy, SwingConstants.VERTICAL).apply(ranges.xPainter::accept)
val px = findGridLineX(gl, it).roundToInt()
if (gl.paintLine) {
if ((tmp - px).absoluteValue < 1) { return@forEach } else { tmp = px }
grid.color = if (gl.majorLine) gcd else gc
grid.drawLine(px, bounds.y, px, bounds.y + bounds.height)
}
gl.label?.let { label ->
chart.color = gridLabelColor
val (x, y) = findGridLabelOffset(gl, chart)
chart.drawString(label, px + margins.left - x.toInt(), yOrigin - margins.bottom + y.toInt())
}
}
// draws horizontal grid lines
tmp = -1
ranges.yLines.apply {
min = xy.yMin
max = xy.yMax
}.forEach {
val gl = GridLine(it, xy).apply(ranges.yPainter::accept)
val py = findGridLineY(gl, it).roundToInt()
if (gl.paintLine) {
if ((tmp - py).absoluteValue < 1) { return@forEach } else { tmp = py }
grid.color = if (gl.majorLine) gcd else gc
grid.drawLine(bounds.x, py, bounds.x + bounds.width, py)
}
gl.label?.let { label ->
chart.color = gridLabelColor
val (x, y) = findGridLabelOffset(gl, chart)
chart.drawString(label, xOrigin + margins.left - x.toInt(), py + margins.top + y.toInt() )
}
}
}
protected open fun findGridLineX(gl: GridLine<X, Y, *>, x: X) = findX(gl.xy, x)
protected open fun findGridLineY(gl: GridLine<X, Y, *>, y: Y) = findY(gl.xy, y)
abstract fun findMinMax(): MinMax<X, Y>
protected open fun findX(xy: MinMax<X, Y>, x: X): Double {
val width = width - (margins.left + margins.right)
return width * (x.toDouble() - xy.xMin.toDouble()) / (xy.xMax.toDouble() - xy.xMin.toDouble())
}
protected open fun findY(xy: MinMax<X, Y>, y: Y): Double {
val height = height - (margins.top + margins.bottom)
return height - height * (y.toDouble() - xy.yMin.toDouble()) / (xy.yMax.toDouble() - xy.yMin.toDouble())
}
protected open fun findGridLabelOffset(line: GridLine<*, *, *>, g: Graphics2D): Coordinates<Double, Double> {
val s = JBUI.scale(4).toDouble()
val b = g.fontMetrics.getStringBounds(line.label, null)
val x = when (line.horizontalAlignment) {
SwingConstants.RIGHT -> -s
SwingConstants.CENTER -> b.width / 2
SwingConstants.LEFT -> b.width + s
else -> -s
}
val y = b.height - when (line.verticalAlignment) {
SwingConstants.TOP -> b.height + s
SwingConstants.CENTER -> b.height / 2 + s / 2 // compensate
SwingConstants.BOTTOM -> 0.0
else -> 0.0
}
return x to y
}
}
abstract class ChartWrapper : ChartComponent {
var width: Int = 0
private set
var height: Int = 0
private set
var background: Color = JBColor.background()
var overlays: List<ChartComponent> = mutableListOf()
set(value) {
field.forEach { if (it is Overlay<*>) it.wrapper = null }
(field as MutableList<ChartComponent>).addAll(value)
field.forEach { if (it is Overlay<*>) it.wrapper = this }
}
var margins = Insets(0, 0, 0, 0)
open fun paintOverlay(g: Graphics2D) {
overlays.forEach {
it.paintComponent(g)
}
}
open val component: JComponent by lazy {
createCentralPanel().apply {
with(MouseAware()) {
addMouseMotionListener(this)
addMouseListener(this)
}
}
}
fun update() = component.repaint()
protected open fun createCentralPanel(): JComponent = CentralPanel()
protected var mouseLocation: Point? = null
private set(value) {
field = value
overlays.forEach { if (it is Overlay<*>) it.mouseLocation = value }
}
private inner class MouseAware : MouseAdapter() {
override fun mouseMoved(e: MouseEvent) {
mouseLocation = e.point
component.repaint()
}
override fun mouseEntered(e: MouseEvent) {
mouseLocation = e.point
component.repaint()
}
override fun mouseExited(e: MouseEvent) {
mouseLocation = null
component.repaint()
}
override fun mouseDragged(e: MouseEvent) {
mouseLocation = e.point
component.repaint()
}
}
private inner class CentralPanel : JComponent() {
override fun paintComponent(g: Graphics) {
g.clip = Rectangle(0, 0, width, height)
g.color = this@ChartWrapper.background
(g as Graphics2D).fill(g.clip)
this@ChartWrapper.height = height
this@ChartWrapper.width = width
val gridGraphics = (g.create(0, 0, this@ChartWrapper.width, this@ChartWrapper.height) as Graphics2D).apply {
setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED)
setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE)
GraphicsUtil.setupAntialiasing(this)
}
try {
this@ChartWrapper.paintComponent(gridGraphics)
} finally {
gridGraphics.dispose()
}
val overlayGraphics = (g.create() as Graphics2D).apply {
setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED)
setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE)
GraphicsUtil.setupAntialiasing(this)
}
try {
this@ChartWrapper.paintOverlay(overlayGraphics)
} finally {
overlayGraphics.dispose()
}
}
}
}
open class Dataset<T> {
var label: String? = null
var lineColor: Paint? = JBColor.foreground()
var fillColor: Paint? = null
open var data: Iterable<T> = mutableListOf()
fun add(vararg values: T) {
addAll(values.toList())
}
@Suppress("UNCHECKED_CAST")
fun addAll(values: Collection<T>) {
(data as? MutableList<T>)?.addAll(values) ?: throw UnsupportedOperationException()
}
fun Color.transparent(alpha: Double) = ColorUtil.withAlpha(this, alpha)
}
data class Coordinates<X: Number, Y: Number>(val x: X, val y: Y) {
companion object {
@JvmStatic fun <X: Number, Y: Number> of(x: X, y: Y) : Coordinates<X, Y> = Coordinates(x, y)
}
}
infix fun <X: Number, Y: Number> X.to(y: Y): Coordinates<X, Y> = Coordinates(this, y)
open class MinMax<X: Number, Y: Number> {
lateinit var xMin: X
val xMinInitialized
get() = this::xMin.isInitialized
lateinit var xMax: X
val xMaxInitialized
get() = this::xMax.isInitialized
lateinit var yMin: Y
val yMinInitialized
get() = this::yMin.isInitialized
lateinit var yMax: Y
val yMaxInitialized
get() = this::yMax.isInitialized
fun process(point: Coordinates<X, Y>) {
val (x, y) = point
process(x, y)
}
fun process(x: X, y: Y) {
processX(x)
processY(y)
}
fun processX(x: X) {
xMin = if (!xMinInitialized || xMin.toDouble() > x.toDouble()) x else xMin
xMax = if (!xMaxInitialized || xMax.toDouble() < x.toDouble()) x else xMax
}
fun processY(y: Y) {
yMin = if (!yMinInitialized || yMin.toDouble() > y.toDouble()) y else yMin
yMax = if (!yMaxInitialized || yMax.toDouble() < y.toDouble()) y else yMax
}
operator fun times(other: MinMax<X, Y>): MinMax<X, Y> {
val my = MinMax<X, Y>()
if (this.xMinInitialized) my.xMin = this.xMin else if (other.xMinInitialized) my.xMin = other.xMin
if (this.xMaxInitialized) my.xMax = this.xMax else if (other.xMaxInitialized) my.xMax = other.xMax
if (this.yMinInitialized) my.yMin = this.yMin else if (other.yMinInitialized) my.yMin = other.yMin
if (this.yMaxInitialized) my.yMax = this.yMax else if (other.yMaxInitialized) my.yMax = other.yMax
return my
}
operator fun plus(other: MinMax<X, Y>) : MinMax<X, Y> {
val my = MinMax<X, Y>()
help(this.xMinInitialized, this::xMin, other.xMinInitialized, other::xMin, -1, my::xMin.setter)
help(this.xMaxInitialized, this::xMax, other.xMaxInitialized, other::xMax, 1, my::xMax.setter)
help(this.yMinInitialized, this::yMin, other.yMinInitialized, other::yMin, -1, my::yMin.setter)
help(this.yMaxInitialized, this::yMax, other.yMaxInitialized, other::yMax, 1, my::yMax.setter)
return my
}
private fun <T: Number> help(thisInitialized: Boolean, thisGetter: () -> T, otherInitialized: Boolean, otherGetter: () -> T, sign: Int, calc: (T) -> Unit) {
when {
thisInitialized && otherInitialized -> {
val thisValue = thisGetter().toDouble()
val thatValue = otherGetter().toDouble()
calc(if (thisValue.compareTo(thatValue).sign == sign) thisGetter() else otherGetter())
}
thisInitialized && !otherInitialized -> calc((thisGetter()))
!thisInitialized && otherInitialized -> calc(otherGetter())
}
}
operator fun component1(): X = xMin
operator fun component2(): X = xMax
operator fun component3(): Y = yMin
operator fun component4(): Y = yMax
val isInitialized get() = xMinInitialized && xMaxInitialized && yMinInitialized && yMaxInitialized
}
class Grid<X: Number, Y: Number>: MinMax<X, Y>() {
lateinit var xOrigin: X
val xOriginInitialized
get() = this::xOrigin.isInitialized
var xLines: ValueIterable<X> = ValueIterable.createStub()
var xPainter: (Consumer<GridLine<X, Y, X>>) = Consumer { }
lateinit var yOrigin: Y
val yOriginInitialized
get() = this::yOrigin.isInitialized
var yLines: ValueIterable<Y> = ValueIterable.createStub()
var yPainter: (Consumer<GridLine<X, Y, Y>>) = Consumer { }
}
class GridLine<X: Number, Y: Number, T: Number>(val value: T, @get:JvmName("getXY") val xy: MinMax<X, Y>, @MagicConstant val orientation: Int = SwingConstants.HORIZONTAL) {
var paintLine = true
var majorLine = false
var label: String? = null
@MagicConstant var horizontalAlignment = SwingConstants.CENTER
@MagicConstant var verticalAlignment = SwingConstants.BOTTOM
}
abstract class ValueIterable<X: Number> : Iterable<X> {
lateinit var min: X
lateinit var max: X
open fun prepare(min: X, max: X): ValueIterable<X> {
this.min = min
this.max = max
return this
}
companion object {
fun <T: Number> createStub(): ValueIterable<T> {
return object: ValueIterable<T>() {
val iter = object: Iterator<T> {
override fun hasNext() = false
override fun next(): T { error("not implemented") }
}
override fun iterator(): Iterator<T> = iter
}
}
}
}
@@ -0,0 +1,306 @@
package com.intellij.ui.charts
import java.awt.BasicStroke
import java.awt.BasicStroke.CAP_BUTT
import java.awt.BasicStroke.JOIN_ROUND
import java.awt.Graphics2D
import java.awt.Rectangle
import java.awt.geom.Area
import java.awt.geom.Path2D
import java.awt.geom.Point2D
import java.util.*
import kotlin.NoSuchElementException
import kotlin.math.hypot
import kotlin.math.min
/**
* Simple Line Chart.
*
* Has options:
* <ul>
* <li><b>stepped</b> — can be set to LineStepped.(NONE|BEFORE|AFTER)
* <li><b>stacked</b> — if <code>true</code> area under chart's line subtracts from result graphic (every next chart cannot paint on this area anymore)
* <li><b>stroke</b> — set custom stroke for the line
* </ul>
*/
abstract class LineDataset<X: Number, Y: Number>: Dataset<Coordinates<X, Y>>() {
var stepped: LineStepped = LineStepped.NONE
var stacked: Boolean = false
var stroke = BasicStroke(1.5f, CAP_BUTT, JOIN_ROUND)
var smooth: Boolean = false
var modificationFirst: Boolean = false
set(value) {
field = value
data = (if (value) LinkedList() else mutableListOf<Coordinates<X, Y>>()).apply {
data.forEach { this@LineDataset.add(it) }
}
}
fun find(x: X): Y? = data.find { it.x == x }?.y
companion object {
@JvmStatic fun <T: Number> of(vararg values: T) = CategoryLineDataset<T>().apply {
addAll(values.mapIndexed(::Coordinates).toList())
}
@JvmStatic fun <X: Number, Y: Number> of(xs: Array<X>, ys: Array<Y>) = XYLineDataset<X, Y>().apply {
addAll(Array(min(xs.size, ys.size)) { i -> Coordinates(xs[i], ys[i]) }.toList())
}
@JvmStatic fun <X: Number, Y: Number> of(vararg points: Coordinates<X, Y>) = XYLineDataset<X, Y>().apply {
addAll(points.toList())
}
}
}
/**
* Type of stepped line:
*
* * NONE — line continuously connects every line
* * BEFORE — line changes value before connection to another point
* * AFTER — line changes value after connection to another point
*/
enum class LineStepped {
NONE, BEFORE, AFTER
}
/**
* Default 2-dimensional dataset for function like f(x) = y.
*/
open class XYLineDataset<X: Number, Y: Number> : LineDataset<X, Y>() {
}
/**
* Default category dataset, that is specific type of XYLineDataset, when x in range of (0..<value count>).
*/
open class CategoryLineDataset<X: Number> : LineDataset<Int, X>() {
var values: Iterable<X>
get() = data.map { it.y }
set(value) {
data = value.mapIndexed(::Coordinates)
}
}
/**
* Base chart component.
*
* For drawing uses GeneralPath from AWT library.
*
* Has options:
*
* * gridColor
* * borderPainted — if <code>true</code> draws a border around the chart and respects margins
* * ranges — grid based range, that holds all information about grid painting. Has minimal and maximum values for graphic.
*/
abstract class LineChart<X: Number, Y: Number, D: LineDataset<X, Y>>: GridChartWrapper<X, Y>() {
var datasets: List<D> = mutableListOf()
companion object {
@JvmStatic fun <T: Number, D: CategoryLineDataset<T>> of(vararg values: D) = CategoryLineChart<T>().apply {
datasets = mutableListOf(*values)
}
@JvmStatic fun <X: Number, Y: Number, D: XYLineDataset<X, Y>> of(vararg values: D) = XYLineChart<X, Y>().apply {
datasets = mutableListOf(*values)
}
@JvmStatic fun <T: Number> of(vararg values: T) = CategoryLineChart<T>().apply {
datasets = mutableListOf(LineDataset.of(*values))
}
@JvmStatic fun <X: Number, Y: Number> of(vararg points: Coordinates<X, Y>) = XYLineChart<X, Y>().apply {
datasets = mutableListOf(LineDataset.of(*points))
}
}
var borderPainted: Boolean = false
override val ranges = Grid<X, Y>()
override fun paintComponent(g: Graphics2D) {
val gridWidth = width - (margins.left + margins.right)
val gridHeight = height - (margins.top + margins.bottom)
if (borderPainted) {
g.color = gridColor
g.drawRect(margins.left, margins.top, gridWidth, gridHeight)
}
val xy = findMinMax()
if (xy.isInitialized) {
val grid = g.create(margins.left, margins.top, gridWidth, gridHeight) as Graphics2D
paintGrid(grid, g, xy)
datasets.forEach {
it.paintDataset(grid, xy)
}
grid.dispose()
}
}
override fun findMinMax() = if (ranges.isInitialized) ranges else ranges * (ranges + MinMax()).apply {
datasets.forEach { it.data.forEach(::process) }
}
private fun D.paintDataset(g: Graphics2D, xy: MinMax<X, Y>) {
val path = Path2D.Double()
lateinit var first: Point2D
val bounds = g.clipBounds
g.paint = lineColor
g.stroke = stroke
// set small array to store 4 points of values
val useSplines = smooth && stepped == LineStepped.NONE
val neighborhood = DoubleArray(8) { Double.NaN }
data.forEachIndexed { i, (x, y) ->
val px = findX(xy, x)
val py = findY(xy, y)
neighborhood.shiftLeftByTwo(px, py)
if (i == 0) {
first = Point2D.Double(px, py)
path.moveTo(px, py)
} else {
if (!useSplines) {
when (stepped) {
LineStepped.AFTER -> path.lineTo(neighborhood[4], py)
LineStepped.BEFORE -> path.lineTo(px, neighborhood[5])
}
path.lineTo(px, py)
} else if (i > 1) {
path.curveTo(neighborhood)
}
}
}
// last step to draw tail of graphic, when spline is used
if (useSplines) {
neighborhood.shiftLeftByTwo(Double.NaN, Double.NaN)
path.curveTo(neighborhood)
}
g.paint = lineColor
g.stroke = stroke
g.draw(path)
// added some points
path.currentPoint?.let { last ->
path.lineTo(last.x, bounds.height + 1.0)
path.lineTo(first.x, bounds.height + 1.0)
path.closePath()
}
fillColor?.let {
g.paint = it
g.fill(path)
}
if (stacked) {
// fix stroke cutting
val area = Area(g.clip)
area.subtract(Area(path))
g.clip = area
}
}
fun findLocation(xy: MinMax<X, Y>, coordinates: Coordinates<X, Y>) = Point2D.Double(
findX(xy, coordinates.x) + margins.left, findY(xy, coordinates.y) + margins.top
)
open fun add(x: X, y: Y) {
datasets.firstOrNull()?.add(x to y)
}
fun getDataset() = datasets.first()
@JvmName("getDataset")
operator fun get(label: String): LineDataset<X, Y> = datasets.find { it.label == label } ?: throw NoSuchElementException("Cannot find dataset with label $datasets")
fun clear() {
datasets.forEach { (it.data as MutableList).clear() }
}
}
open class CategoryLineChart<X: Number> : LineChart<Int, X, CategoryLineDataset<X>>()
open class XYLineChart<X: Number, Y: Number> : LineChart<X, Y, XYLineDataset<X, Y>>()
// HELPERS
/**
* Calculates control points for bezier function and curves line.
* Requires an array of 4 points in format (x0, y0, x1, y1, x2, y2, x3, y3),
* where (x2, y2) — is target point, x1, y1 — point, that can be acquired by Path2D.currentPoint.
* (x0, y0) — point before current and (x3, y3) —point after.
*
* @param neighbour array keeps 4 points (size 8)
*
* Using Monotone Cubic Splines: algorithm https://en.wikipedia.org/wiki/Monotone_cubic_interpolation
*/
private fun Path2D.Double.curveTo(neighbour: DoubleArray) {
assert(neighbour.size == 8) { "Array must contain 4 points in format (x0, y0, x1, y1, x2, y2, x3, y3)" }
val x0 = neighbour[0]
val y0 = neighbour[1]
val x1 = neighbour[2]
val y1 = neighbour[3]
val x2 = neighbour[4]
val y2 = neighbour[5]
val x3 = neighbour[6]
val y3 = neighbour[7]
val slope0 = ((y1 - y0) / (x1 - x0)).orZero()
val slope1 = ((y2 - y1) / (x2 - x1)).orZero()
val slope2 = ((y3 - y2) / (x3 - x2)).orZero()
var tan1 = if (slope0 * slope1 <= 0) 0.0 else (slope0 + slope1) / 2
var tan2 = if (slope1 * slope2 <= 0) 0.0 else (slope1 + slope2) / 2
if (slope1 == 0.0) {
tan1 = 0.0
tan2 = 0.0
} else {
val a = tan1 / slope1
val b = tan2 / slope1
val h = hypot(a, b)
if (h > 3.0) {
val t = 3.0 / h
tan1 = t * a * slope1
tan2 = t * b * slope1
}
}
val delta2 = (x2 - x1) / 3
var cx0 = x1 + delta2
var cy0 = y1 + delta2 * tan1
if (x0.isNaN() || y0.isNaN()) {
cx0 = x1
cy0 = y1
}
val delta0 = (x2 - x1) / 3
var cx1 = x2 - delta0
var cy1 = y2 - delta0 * tan2
if (x3.isNaN() || y3.isNaN()) {
cx1 = x2
cy1 = y2
}
curveTo(cx0, cy0, cx1, cy1, x2, y2)
}
private fun Double.orZero() = if (this.isNaN()) 0.0 else this
private fun DoubleArray.shiftLeftByTwo(first: Double, second: Double) {
for (j in 2 until size) {
this[j - 2] = this[j]
}
this[size - 2] = first
this[size - 1] = second
}
@@ -0,0 +1,64 @@
package com.intellij.ui.charts
import com.intellij.ui.JBColor
import java.awt.BasicStroke
import java.awt.Graphics2D
import java.awt.Point
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.util.function.BiConsumer
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt
class LabelOverlay: Overlay<LineChart<*, *, *>>() {
override fun paintComponent(g: Graphics2D) {
val bounds = g.fontMetrics.getStringBounds("SAMPLE", null)
g.stroke = BasicStroke(5F)
chart.datasets.forEachIndexed { i, dataset ->
g.paint = dataset.lineColor
val y = chart.height - (bounds.height + i * bounds.height).toInt()
g.drawLine(10, y, 30, y)
if (dataset.label != null) {
val text = dataset.label!!
g.drawString(text, 35, y + (bounds.height / 3).roundToInt())
}
}
}
}
class DragOverlay(val onRelease: BiConsumer<Point, Point>): Overlay<LineChart<*, *, *>>() {
private var startPoint: Point? = null
override fun afterChartInitialized() {
chart.component.addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent) {
startPoint = e.point.toChartSpace()
}
override fun mouseReleased(e: MouseEvent) {
val start = startPoint
val end = e.point.toChartSpace()
startPoint = null
if (start != null && end != null)
onRelease.accept(start, end)
}
})
}
override fun paintComponent(g: Graphics2D) {
val start = startPoint
if (start == null) {
return
}
val end = mouseLocation
if (end == null) {
return
}
g.color = JBColor.BLACK
g.drawRect(min(start.x, end.x), min(start.y, end.y), abs(start.x - end.x), abs(start.y - end.y))
}
}
@@ -0,0 +1,5 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
@ApiStatus.Experimental
package com.intellij.ui.charts;
import org.jetbrains.annotations.ApiStatus;
@@ -0,0 +1,150 @@
package com.intellij.ui.charts
import com.intellij.ide.ui.laf.IntelliJLaf
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.charts.*
import com.intellij.ui.ColorUtil
import com.intellij.ui.components.JBTabbedPane
import com.intellij.util.ui.JBUI
import java.awt.Color
import java.awt.Dimension
import javax.swing.*
import javax.swing.border.Border
import kotlin.math.cos
import kotlin.system.exitProcess
fun simple() = barChart<Double> {
gap = 20
datasets {
dataset {
values = enumerator(1.0, 2.0, 1.0, -1.0, 3.0, 2.0, 2.0)
lineColor = Color(6, 128, 213)
fillColor = Color(6, 128, 213, 122)
}
dataset {
values = enumerator(1.5, 1.0, .5, -2.0, 2.0, 5.0, 1.0)
stacked = true
lineColor = Color(213, 6, 13)
fillColor = Color(213, 6, 13, 122)
}
dataset {
values = enumerator(2.0, 3.0, 1.5, -.20, 2.5, 1.0, 0.0)
lineColor = Color(56, 158, 38)
fillColor = Color(56, 158, 38, 122)
}
dataset {
values = generate {
count = 7
function = { i -> i - count / 2.toDouble()}
}
}
}
grid {
xLines = generator(1)
yLines = generator(1.0)
yOrigin = 0.0
xPainter {
label = "Test"
}
}
}
fun performance() = barChart<Double> {
val format: (Double) -> String = { v -> "%.2f".format(v) }
val labels = listOf("macOS Mojave 10.14.6", "Ubuntu 16.04", "Windows 10")
ranges {
yMax = 15.0
yMin = 0.0
}
space = 1
gap = 80
margins {
top = 30
bottom = 40
left = 40
right = 40
}
datasets {
dataset {
showValues = format
label = "2019.1"
values = enumerator(8.57, 10.8, 12.53)
lineColor = null
fillColor = ColorUtil.fromHex("999999")
}
dataset {
showValues = format
label = "2019.2"
values = enumerator(7.14, 10.0, 10.42)
lineColor = null
fillColor = ColorUtil.fromHex("262525")
}
dataset {
showValues = format
label = "2019.3"
lineColor = null
values = enumerator(5.42, 6.24, 7.18)
fillColor = ColorUtil.fromHex("1255CC")
}
}
grid {
yLines = generator(5.0)
yPainter {
label = "%.0fs".format(value)
majorLine = 0.0 == value
horizontalAlignment = SwingUtilities.LEFT
verticalAlignment = SwingUtilities.CENTER
}
xLines = generator(1)
xPainter {
label = labels[value]
paintLine = false
}
xOrigin = 0
}
}
fun generate() = barChart<Float> {
dataset {
values = generate {
count = 180
function = { i -> cos(Math.toRadians(i.toDouble())).toFloat() }
}
}
}
fun main() {
UIManager.setLookAndFeel(IntelliJLaf())
SwingUtilities.invokeLater {
object: DialogWrapper(false) {
init {
init()
isModal = false
setUndecorated(false)
title = "Bar Chart Kotlin Demo"
}
override fun createCenterPanel(): JComponent = JBTabbedPane().apply {
preferredSize = Dimension(640, 480)
tabPlacement = JTabbedPane.BOTTOM
add("Performance", performance().component).apply { (this as JComponent).border = JBUI.Borders.empty() }
add("Chart", simple().component).apply { (this as JComponent).border = JBUI.Borders.empty() }
add("Generate", generate().component).apply { (this as JComponent).border = JBUI.Borders.empty() }
}
override fun createActions(): Array<Action> = arrayOf()
override fun createSouthPanel(): JComponent? = null
override fun createContentPaneBorder(): Border = JBUI.Borders.empty()
override fun dispose() {
super.disposeIfNeeded()
exitProcess(0)
}
}.show()
}
}
@@ -0,0 +1,135 @@
package com.intellij.ui.charts;
import com.intellij.util.Consumer;
import org.junit.Assert;
import org.junit.Test;
import javax.swing.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class LineChartJavaTest {
@Test
public void createCategoryDatasetTest() {
CategoryLineDataset<Double> dataset = LineDataset.of(-1.0, 0.0, 1.0, 2.0, 3.0);
Iterable<Coordinates<Integer, Double>> data = dataset.getData();
Iterator<Coordinates<Integer, Double>> iterator = data.iterator();
Assert.assertTrue(data instanceof ArrayList);
Assert.assertEquals(Coordinates.of(0, -1.0), iterator.next());
Assert.assertEquals(Coordinates.of(1, 0.0), iterator.next());
Assert.assertEquals(Coordinates.of(2, 1.0), iterator.next());
Assert.assertEquals(Coordinates.of(3, 2.0), iterator.next());
Assert.assertEquals(Coordinates.of(4, 3.0), iterator.next());
Assert.assertFalse(iterator.hasNext());
}
@Test
public void createXYDatasetTest() {
Integer[] xs = {-1, 0, 1};
Double[] ys = {-100.0, 0.0, 100.0};
XYLineDataset<Integer, Double> dataset = LineDataset.of(xs, ys);
Iterable<Coordinates<Integer, Double>> data = dataset.getData();
Iterator<Coordinates<Integer, Double>> iterator = data.iterator();
Assert.assertTrue(data instanceof ArrayList);
Assert.assertEquals(Coordinates.of(-1, -100.0), iterator.next());
Assert.assertEquals(Coordinates.of(0, 0.0), iterator.next());
Assert.assertEquals(Coordinates.of(1, 100.0), iterator.next());
Assert.assertFalse(iterator.hasNext());
}
@Test
public void fastCreateLineChartTest() {
CategoryLineChart<Double> chart = LineChart.of(1.0, 2.0, 3.0, 4.0, 5.0);
CategoryLineDataset<Double> negativeValues = LineDataset.of(-1.0, -2.0, -3.0, -4.0, -5.0);
chart.getDatasets().add(negativeValues);
MinMax<Integer, Double> xy = chart.findMinMax();
Assert.assertEquals(2, chart.getDatasets().size());
Assert.assertTrue(chart.getComponent() instanceof JComponent);
Assert.assertEquals(Integer.valueOf(0), xy.xMin);
Assert.assertEquals(Integer.valueOf(4), xy.xMax);
Assert.assertEquals(Double.valueOf(-5.0), xy.yMin);
Assert.assertEquals(Double.valueOf(5.0), xy.yMax);
}
@Test
public void gridLineChartTest() {
Double[] doubles = {1.0, 2.0, 3.0, 4.0, 5.0};
CategoryLineChart<Double> chart = LineChart.of(doubles);
AtomicInteger counter = new AtomicInteger();
final Consumer<GridLine<Integer, Double, ?>> common = gridline -> {
MinMax<Integer, Double> xy = gridline.getXY();
Assert.assertEquals(Integer.valueOf(0), xy.xMin);
Assert.assertEquals(Integer.valueOf(4), xy.xMax);
Assert.assertEquals(Double.valueOf(1.0), xy.yMin);
Assert.assertEquals(Double.valueOf(5.0), xy.yMax);
// mutations:
xy.setXMin(-1);
xy.setXMax(60000);
xy.setYMin(-1.0);
xy.setYMax(-60000.0);
Assert.assertEquals(doubles[counter.getAndIncrement()], gridline.getValue());
};
chart.getGrid().setXPainter(gridline -> {
Assert.assertEquals(Integer.valueOf(counter.getAndIncrement()), gridline.getValue());
common.consume(gridline);
});
counter.set(0);
chart.getGrid().setYPainter(gridline -> {
Assert.assertEquals(doubles[counter.getAndIncrement()], gridline.getValue());
common.consume(gridline);
});
}
@Test
public void dynamicUpdateTest() {
XYLineDataset<Double, Double> maxValuesDataset = new XYLineDataset<>();
maxValuesDataset.setLabel("MAX VALUES");
XYLineDataset<Double, Double> minValuesDataset = new XYLineDataset<>();
minValuesDataset.setLabel("MIN VALUES");
XYLineChart<Double, Double> chart = LineChart.of(maxValuesDataset, minValuesDataset);
Assert.assertFalse(chart.getDataset("MAX VALUES").getData().iterator().hasNext());
Assert.assertFalse(chart.getDataset("MIN VALUES").getData().iterator().hasNext());
for (int i = 1; i < 1000; i++) {
chart.getDataset("MAX VALUES").add(Coordinates.of( Double.valueOf(i), Double.valueOf(i)));
Assert.assertEquals(i, ((List<?>) chart.getDataset("MAX VALUES").getData()).size());
Assert.assertFalse(chart.getDataset("MIN VALUES").getData().iterator().hasNext());
}
}
@Test
public void customGeneratorDataset() {
XYLineChart<Integer, Double> chart = LineChart.of(new XYLineDataset<>());
ChartUtils.generator(45).prepare(-360, 360).forEach(value -> {
chart.getDataset().add(Coordinates.of(value, Math.sin(Math.toRadians(value))));
});
MinMax<Integer, Double> xy = chart.findMinMax();
Assert.assertEquals(Integer.valueOf(-360), xy.xMin);
Assert.assertEquals(Integer.valueOf(360), xy.xMax);
Iterator<Coordinates<Integer, Double>> iterator = chart.getDataset().getData().iterator();
for (int i = -360; i <= 360; i += 45) {
Coordinates<Integer, Double> next = iterator.next();
Assert.assertEquals(Integer.valueOf(i), next.getX());
Assert.assertEquals(Double.valueOf(Math.sin(Math.toRadians(i))), next.getY());
}
Assert.assertFalse(iterator.hasNext());
}
}
@@ -0,0 +1,412 @@
package com.intellij.ui.charts
import com.intellij.ide.ui.laf.IntelliJLaf
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.charts.*
import com.intellij.ui.ColorUtil
import com.intellij.ui.JBColor
import com.intellij.ui.components.JBTabbedPane
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.components.BorderLayoutPanel
import java.awt.Color
import java.awt.Component
import java.awt.Dimension
import java.awt.Graphics2D
import java.util.function.BiConsumer
import javax.swing.*
import javax.swing.border.Border
import kotlin.math.*
import kotlin.random.Random
import kotlin.system.exitProcess
fun sin() = lineChart<Int, Double> {
datasets {
dataset {
lineColor = JBColor.BLUE
stepped = LineStepped.NONE
label = "Discrete (step = 1)"
borderPainted = true
generate {
x = -360..360 step 1
y = { sin(Math.toRadians(it.toDouble())) }
}
}
dataset {
lineColor = JBColor.BLACK
stepped = LineStepped.NONE
label = "Discrete (step = 45)"
borderPainted = true
generate {
x = -360..360 step 45
y = { sin(Math.toRadians(it.toDouble())) }
}
}
dataset {
lineColor = JBColor.RED
stepped = LineStepped.NONE
label = "Smooth (step = 45)"
borderPainted = true
smooth = true
generate {
x = -360..360 step 45
y = { sin(Math.toRadians(it.toDouble())) }
}
}
overlays = listOf(LabelOverlay())
ranges {
yMin = -1.25
yMax = 1.25
}
grid {
xLines = generator(90)
xOrigin = 0
xPainter {
label = "%d".format(value)
majorLine = value == 0
}
yLines = generator(0.25)
yOrigin = 0.0
yPainter {
label = "%.2f".format(value)
majorLine = value == 0.0
horizontalAlignment = SwingConstants.RIGHT
verticalAlignment = SwingConstants.TOP
}
}
}
}
fun many() = lineChart<Double, Double> {
dataset {
generate {
x = generator(1.0).prepare(0.0, 500_000.0)
y = { x -> x.pow(3) }
}
}
}
fun trivials() = lineChart<Double, Double> {
val values = generator(0.01).prepare(0.0, 8.0)
ranges {
xMin = 0.0
xMax = 8.0
yMin = 0.0
yMax = 8.0
}
datasets {
dataset {
generate {
x = values
y = 2.0::pow
}
lineColor = JBColor.GREEN
}
dataset {
generate {
x = values
y = { it.pow(2.0) }
}
lineColor = JBColor.ORANGE
}
dataset {
generate {
x = values
y = { it }
}
lineColor = JBColor.RED
}
dataset {
generate {
x = values
y = { log2(it) * it }
}
lineColor = JBColor.BLUE
}
dataset {
generate {
x = values
y = ::log2
}
lineColor = JBColor.MAGENTA
}
}
grid {
xLines = generator(1.0)
yLines = generator(1.0)
}
}
fun stepped() = lineChart<Int> {
dataset {
values = enumerator(1, 2, 1, 4, 3, 3, 2, 5, 3, 2, 5, 2, 1, 1, 2)
stepped = LineStepped.AFTER
fillColor = Color.GRAY
}
ranges {
yMin = 0
yMax = 6
}
grid {
xLines = generator(1)
yLines = generator(1)
}
}
enum class YEAR {
JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC
}
private fun simpleCategoryLineChart() = lineChart<Double> {
margins {
bottom = 100
}
datasets {
dataset {
values = listOf(1.0, 2.0, 3.0, 2.0, 3.0, 1.0, 1.0)
lineColor = Color.BLUE
}
dataset {
values = listOf(1.0, 2.0, 3.0, 2.0, 3.0, 1.0, 1.0)
smooth = true
lineColor = Color.RED
}
}
grid {
xLines = generator(1)
xPainter {
label = YEAR.values()[value].name
horizontalAlignment = SwingUtilities.CENTER
}
}
}
private fun simpleXYLineChart() = lineChart<Double, Double> {
datasets {
dataset {
data = listOf(1.0 to 1.0, 2.0 to 2.0, 2.5 to 3.0)
}
dataset {
data = listOf(1.0 to 1.0, 2.0 to 2.0, 3.0 to 1.0)
smooth = true
}
}
}
private fun funcXYLineChart() = lineChart<Double, Double> {
datasets {
// Generated values
dataset {
generate {
x = listOf(0.0, 1.0, 2.0, 3.0)
y = ::sin
}
}
dataset {
values {
x = generator(0.1).prepare(0.0, 0.3)
y = enumerator(0.0, 0.01, 0.002, 0.0003)
}
}
}
}
fun areaChart() = lineChart<Int, Int> {
val years = generator(5).prepare(1700, 1780).toList()
margins {
top = 40
right = 40
left = 40
bottom = 30
}
ranges {
yMin = 0
yMax = 200
}
grid {
xOrigin = 1780
xLines = generator(10)
xPainter {
label = "%d".format(value)
verticalAlignment = SwingConstants.BOTTOM
horizontalAlignment = SwingConstants.CENTER
}
yLines = generator(10)
yPainter {
majorLine = value == 100
label = "%d".format(value)
verticalAlignment = SwingConstants.CENTER
horizontalAlignment = SwingConstants.RIGHT
}
}
overlays = listOf(
object: Overlay<ChartWrapper>() {
override fun paintComponent(g: Graphics2D) {
g.color = JBColor.foreground()
val str = "Exports and Imports to and from DENMARK & NORWAY from 1700 to 1780"
val w = g.fontMetrics.stringWidth(str)
g.drawString(str, (chart.width - w) / 2, 30)
}
}
)
datasets {
dataset {
stacked = true
lineColor = Color(0, 0, 0, 0)
smooth = true
generate {
x = years
y = { min(get("Imports").find(it)!!, get("Exports").find(it)!!) }
}
}
dataset {
label = "Imports"
lineColor = ColorUtil.fromHex("E7D8B6")
fillColor = ColorUtil.fromHex("F8D9D8").transparent(0.5)
smooth = true
values {
x = years
y = listOf(70, 75, 80, 90, 100, 102, 98, 92, 92, 91, 90, 80, 77, 80, 85, 90, 90)
}
}
dataset {
label = "Exports"
lineColor = ColorUtil.fromHex("F8D9D8")
fillColor = ColorUtil.fromHex("E7D8B6").transparent(0.5)
smooth = true
values {
x = years
y = listOf(35, 43, 60, 80, 75, 70, 60, 60, 63, 72, 79, 80, 110, 150, 160, 180, 185, 190)
}
}
}
}
fun zoom() = lineChart<Int, Double> {
val values = 1..200
val random = values.map { Random.nextInt(it).toDouble() }
dataset {
values {
x = values
y = random
}
grid {
xMin = 1
xMax = 200
yMin = 0.0
yMax = 200.0
}
}
overlays = listOf(DragOverlay(BiConsumer { start, end ->
val chart = this@lineChart
val chartWidth = chart.width - chart.margins.left - chart.margins.right
val chartHeight = chart.height - chart.margins.top - chart.margins.bottom
val upperLeftX = min(start.x, end.x)
val upperLeftY = min(start.y, end.y)
val bottomRightX = max(start.x, end.x)
val bottomRightY = max(start.y, end.y)
val startXRatio = upperLeftX / chartWidth.toDouble()
val startYRatio = upperLeftY / chartHeight.toDouble()
val endXRatio = bottomRightX / chartWidth.toDouble()
val endYRatio = bottomRightY / chartHeight.toDouble()
chart.grid {
val (xm, xx, ym, yx) = findMinMax()
xMin = floor((xx - xm) * startXRatio + xm).toInt()
xMax = ceil((xx - xm) * endXRatio + xm).toInt()
yMax = (yx - ym) * (1 - startYRatio) + ym
yMin = (yx - ym) * (1 - endYRatio) + ym
}
chart.update()
}))
}
fun main() {
UIManager.setLookAndFeel(IntelliJLaf())
SwingUtilities.invokeLater {
object: DialogWrapper(false) {
init {
init()
isModal = false
setUndecorated(false)
title = "Line Chart Kotlin Demo"
}
override fun createCenterPanel(): JComponent = JBTabbedPane().apply {
preferredSize = Dimension(640, 480)
tabPlacement = JTabbedPane.BOTTOM
fun Component.setEmptyBorders() = this.apply {
(this as JComponent).border = JBUI.Borders.empty()
}
add("Sin", sin().component).setEmptyBorders()
add("Trivials", trivials().component).setEmptyBorders()
add("Many", many().component).setEmptyBorders()
add("Stepped", stepped().component).setEmptyBorders()
add("Category", simpleCategoryLineChart().component).setEmptyBorders()
add("XY", simpleXYLineChart().component).setEmptyBorders()
add("Func", funcXYLineChart().component).setEmptyBorders()
add("Import/Export", areaChart().component).setEmptyBorders()
val zoomPane = BorderLayoutPanel()
val zoom = zoom()
zoomPane.addToCenter(zoom.component)
val box = Box(BoxLayout.X_AXIS)
box .add(JButton("Zoom in (+)").apply {
addActionListener {
val xChange = (zoom.grid.xMax - zoom.grid.xMin) / 4
val yChange = (zoom.grid.yMax - zoom.grid.yMin) / 4
zoom.grid.xMin += xChange
zoom.grid.xMax -= xChange
zoom.grid.yMin += yChange
zoom.grid.yMax -= yChange
zoom.update()
}
})
box .add(JButton("Zoom out (-)").apply {
addActionListener {
val xChange = (zoom.grid.xMax - zoom.grid.xMin) / 2
val yChange = (zoom.grid.yMax - zoom.grid.yMin) / 2
zoom.grid.xMin -= xChange
zoom.grid.xMax += xChange
zoom.grid.yMin -= yChange
zoom.grid.yMax += yChange
zoom.update()
}
})
box.add(JButton("Reset").apply {
val (myXMin, myXMax, myYMin, myYMax) = zoom.grid
addActionListener {
zoom.grid {
xMin = myXMin
xMax = myXMax
yMin = myYMin
yMax = myYMax
}
zoom.update()
}
})
box.alignmentX = 0.0f
zoomPane.addToBottom(box)
add("Zoomed", zoomPane).setEmptyBorders()
}
override fun createActions(): Array<Action> = arrayOf()
override fun createSouthPanel(): JComponent? = null
override fun createContentPaneBorder(): Border = JBUI.Borders.empty()
override fun dispose() {
super.disposeIfNeeded()
exitProcess(0)
}
}.show()
}
}
@@ -0,0 +1,103 @@
package com.intellij.ui.charts
import com.intellij.ui.charts.*
import com.intellij.util.ui.ImageUtil
import org.junit.Assert
import org.junit.Test
import java.awt.Dimension
import java.awt.image.BufferedImage
import kotlin.math.roundToInt
class LineChartKotlinTest {
@Test
fun simpleCreation() {
val chart = lineChart<Int, Int> {
datasets {
dataset {
values {
x = listOf(2, 3, 4)
y = listOf(1, 2, 3)
}
}
}
}
val xy = chart.findMinMax()
Assert.assertEquals(2, xy.xMin)
Assert.assertEquals(4, xy.xMax)
Assert.assertEquals(1, xy.yMin)
Assert.assertEquals(3, xy.yMax)
}
@Test
fun checkDoubleCreation() {
val chart = lineChart<Double, Double> {
datasets {
dataset {
generate {
x = generator(0.01).prepare(0.0, 100.0)
y = { it }
}
}
}
}
val xy = chart.findMinMax()
Assert.assertEquals(0.0, xy.xMin, 1e-6)
Assert.assertEquals(100.0, xy.xMax, 1e-6)
Assert.assertEquals(0.0, xy.yMin, 1e-6)
Assert.assertEquals(100.0, xy.yMax, 1e-6)
}
@Test
fun testLinearChart() {
val size = 1000
val chart = lineChart<Double, Double> {
dataset {
generate {
x = generator(10.0 / size).prepare(0.0, size / 10.0)
y = { it }
}
}
}
val img = ImageUtil.createImage(size, size, BufferedImage.TYPE_INT_RGB)
chart.component.apply {
this.size = Dimension(size, size)
invalidate()
paint(img.createGraphics())
}
val xy = chart.findMinMax()
for (i in 0..size) {
val loc = chart.findLocation(xy, i / 10.0 to i / 10.0)
Assert.assertEquals("x fails, i = $i", i.toDouble(), loc.x, 1e-6)
Assert.assertEquals("y fails, i = $i", (size - i).toDouble(), loc.y, 1e-6)
}
}
@Test
fun testLinearChartAndScaled() {
val size = 1000
val chart = lineChart<Double, Double> {
dataset {
generate {
x = generator(10.0 / size).prepare(0.0, size / 10.0)
y = { it }
}
}
}
val img = ImageUtil.createImage(size, size, BufferedImage.TYPE_INT_RGB)
chart.component.apply {
this.size = Dimension(size, (size * 1.5).roundToInt())
invalidate()
paint(img.createGraphics())
}
val xy = chart.findMinMax()
for (i in 0..size) {
val loc = chart.findLocation(xy, i / 10.0 to i / 10.0)
Assert.assertEquals("x fails, i = $i", i.toDouble(), loc.x, 1e-6)
Assert.assertEquals("y fails, i = $i", (size - i) * 1.5, loc.y, 1e-6)
}
}
}