mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Move Charts to the platform
GitOrigin-RevId: 5cd6ffb9e48459c3a50cf238d49fdf383f2022f5
This commit is contained in:
committed by
intellij-monorepo-bot
parent
9e73a1094a
commit
5135570523
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user