Last active
February 8, 2019 04:29
-
-
Save born2snipe/72c9c425622755017241a240b4205122 to your computer and use it in GitHub Desktop.
Example usage of LineChart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.badlogic.gdx.graphics.Color; | |
import com.badlogic.gdx.graphics.g2d.Batch; | |
import com.badlogic.gdx.graphics.g2d.SpriteBatch; | |
import com.badlogic.gdx.graphics.glutils.ShapeRenderer; | |
import com.badlogic.gdx.math.Vector2; | |
import com.badlogic.gdx.scenes.scene2d.Touchable; | |
import com.badlogic.gdx.scenes.scene2d.ui.Widget; | |
import com.badlogic.gdx.utils.Array; | |
import com.badlogic.gdx.utils.ObjectMap; | |
import static com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType.Filled; | |
import static com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType.Line; | |
/** | |
* This widget draws lines of on a chart over time. The original intent for chart was for | |
* drawing the FPS as timed passed to make it easier to visualize spikes. | |
*/ | |
public class LineChart extends Widget { | |
private static final float MINIMUM_STEP_SIZE = 1.0f; | |
private final float durationBetweenQueryingProvider; | |
private int maxNumberOfValues; | |
private float elapsedTime; | |
private ShapeRenderer shapeRenderer; | |
private float maxValue = 100.0f; | |
private float minValue = 0f; | |
private Color backgroundColor; | |
private float graphStep; | |
private Color graphStepColor; | |
private ObjectMap<ValueProvider, Array<GraphPoint>> valueProviderToValues = new ObjectMap<>(); | |
private ObjectMap<ValueProvider, GraphPoint> valueProviderToInitialPoint = new ObjectMap<>(); | |
/** | |
* By default it will query the value providers every 10th of a second and store | |
* data for a 10 second window. | |
* | |
* @param shapeRenderer - used to draw the lines | |
* @param valueProviders - the values you would like to be plotted on the graph | |
*/ | |
public LineChart(ShapeRenderer shapeRenderer, ValueProvider... valueProviders) { | |
this(shapeRenderer, 0.1f, 10f, valueProviders); | |
} | |
/** | |
* @param shapeRenderer - used to draw the lines | |
* @param secondsBetweenQueryingProvider - the number seconds in between the querying of the ValueProviders collection | |
* @param maxTimeWindowSizeInSeconds - how big of a time window of data to hold in memory | |
* @param valueProviders - the values you would like to be plotted on the graph | |
*/ | |
public LineChart(ShapeRenderer shapeRenderer, float secondsBetweenQueryingProvider, float maxTimeWindowSizeInSeconds, ValueProvider... valueProviders) { | |
this.durationBetweenQueryingProvider = secondsBetweenQueryingProvider; | |
this.maxNumberOfValues = Math.round(maxTimeWindowSizeInSeconds / secondsBetweenQueryingProvider); | |
this.shapeRenderer = shapeRenderer; | |
for (ValueProvider valueProvider : valueProviders) { | |
valueProviderToValues.put(valueProvider, new Array<GraphPoint>(maxNumberOfValues)); | |
} | |
setTouchable(Touchable.enabled); | |
} | |
@Override | |
public void act(float delta) { | |
super.act(delta); | |
elapsedTime += delta; | |
if (elapsedTime >= durationBetweenQueryingProvider) { | |
captureValue(); | |
elapsedTime = 0; | |
} | |
} | |
@Override | |
public void draw(Batch batch, float parentAlpha) { | |
SpriteBatch mainWidgetBatch = (SpriteBatch) batch; | |
super.draw(mainWidgetBatch, parentAlpha); | |
mainWidgetBatch.end(); | |
shapeRenderer.setTransformMatrix(mainWidgetBatch.getTransformMatrix()); | |
shapeRenderer.setProjectionMatrix(mainWidgetBatch.getProjectionMatrix()); | |
if (backgroundColor != null) { | |
drawBackgroundColor(parentAlpha); | |
} | |
if (graphStepColor != null) { | |
drawGraphMeasurementLines(parentAlpha); | |
} | |
for (ObjectMap.Entry<ValueProvider, Array<GraphPoint>> entry : valueProviderToValues) { | |
drawLineOnGraph(parentAlpha, entry); | |
} | |
mainWidgetBatch.begin(); | |
} | |
@Override | |
public float getPrefWidth() { | |
return 200f; | |
} | |
@Override | |
public float getPrefHeight() { | |
return 100f; | |
} | |
/** | |
* Set the min/max values that will displayed on the chart | |
* <p> | |
* By default range is 0..100 | |
*/ | |
public void setValueRange(float minValue, float maxValue) { | |
this.minValue = minValue; | |
this.maxValue = maxValue; | |
} | |
public void setBackgroundColor(Color backgroundColor) { | |
this.backgroundColor = backgroundColor; | |
} | |
/** | |
* Horizontal lines drawn on the graph to allow visual measuring | |
* | |
* @param graphStep - how often to draw a line | |
* @param graphStepColor - the color of the line | |
*/ | |
public void setGraphStep(float graphStep, Color graphStepColor) { | |
this.graphStep = graphStep; | |
this.graphStepColor = graphStepColor; | |
} | |
private void drawLineOnGraph(float parentAlpha, ObjectMap.Entry<ValueProvider, Array<GraphPoint>> entry) { | |
GraphPoint initialPoint = valueProviderToInitialPoint.get(entry.key); | |
if (entry.value.size < 1 || initialPoint == null) { | |
return; | |
} else if (initialPoint.position == null) { | |
initialPoint.position = new Vector2(getX(), convertToY(initialPoint.value)); | |
} | |
final float xStep = Math.max(getWidth() / maxNumberOfValues, MINIMUM_STEP_SIZE); | |
final float maxY = convertToY(maxValue); | |
final float minY = convertToY(minValue); | |
shapeRenderer.begin(Line); | |
shapeRenderer.setColor(applyAlphaTo(entry.key.getColor(), parentAlpha)); | |
Vector2 previousPoint = initialPoint.position; | |
Array<GraphPoint> values = entry.value; | |
for (int i = 0; i < values.size; i++) { | |
GraphPoint graphPoint = values.get(i); | |
float value = graphPoint.value; | |
Vector2 point = graphPoint.position; | |
if (point == null) { | |
float x = previousPoint.x + xStep; | |
point = new Vector2(x, convertToY(value)); | |
graphPoint.position = point; | |
if (isOutsideHorizontalView(x)) { | |
maxNumberOfValues = entry.value.size; | |
} | |
} | |
if (previousPoint == null) { | |
continue; | |
} else if (previousPoint.equals(point)) { | |
continue; | |
} else if (isOutsideVerticalGraphingArea(minY, maxY, previousPoint, point)) { | |
continue; | |
} | |
float slope = (point.y - previousPoint.y) / (point.x - previousPoint.x); | |
float yIntercept = point.y - (slope * point.x); | |
float firstX = previousPoint.x; | |
float firstY = previousPoint.y; | |
float secondX = point.x; | |
float secondY = point.y; | |
if (previousPoint.y < maxY && point.y > maxY) { | |
// draw partial line going up (middle to max) | |
secondY = maxY; | |
secondX = (maxY - yIntercept) / slope; | |
} else if (previousPoint.y > maxY && point.y < maxY) { | |
// draw partial line going down (max to middle) | |
firstY = maxY; | |
firstX = (maxY - yIntercept) / slope; | |
} else if (previousPoint.y < minY && point.y > minY) { | |
// draw partial line going up (min to middle) | |
firstY = minY; | |
firstX = (minY - yIntercept) / slope; | |
} else if (previousPoint.y > minY && point.y < minY) { | |
// draw partial line going down (middle to min) | |
secondY = minY; | |
secondX = (minY - yIntercept) / slope; | |
} | |
shapeRenderer.line(firstX, firstY, secondX, secondY); | |
previousPoint = point; | |
} | |
shapeRenderer.end(); | |
} | |
private boolean isOutsideVerticalGraphingArea(float minY, float maxY, Vector2 previousPoint, Vector2 point) { | |
return (previousPoint.y >= maxY && point.y >= maxY) | |
|| (previousPoint.y <= minY && point.y <= minY); | |
} | |
private boolean isOutsideHorizontalView(float x) { | |
return x >= (getX() + getWidth()); | |
} | |
private void drawGraphMeasurementLines(float parentAlpha) { | |
shapeRenderer.begin(Line); | |
shapeRenderer.setColor(applyAlphaTo(graphStepColor, parentAlpha)); | |
for (float value = minValue + graphStep; value < maxValue; value += graphStep) { | |
float y = convertToY(value); | |
shapeRenderer.line(getX(), y, getX() + getWidth(), y); | |
} | |
// draw border around chart | |
shapeRenderer.rect(getX(), getY(), getWidth(), getHeight()); | |
shapeRenderer.end(); | |
} | |
private void drawBackgroundColor(float parentAlpha) { | |
shapeRenderer.begin(Filled); | |
shapeRenderer.setColor(applyAlphaTo(backgroundColor, parentAlpha)); | |
shapeRenderer.rect(getX(), getY(), getWidth(), getHeight()); | |
shapeRenderer.end(); | |
} | |
private void captureValue() { | |
final float xStep = Math.max(getWidth() / maxNumberOfValues, MINIMUM_STEP_SIZE); | |
for (ObjectMap.Entry<ValueProvider, Array<GraphPoint>> entry : valueProviderToValues) { | |
float value = entry.key.get(); | |
GraphPoint point = new GraphPoint(value); | |
entry.value.add(point); | |
if (!valueProviderToInitialPoint.containsKey(entry.key)) { | |
valueProviderToInitialPoint.put(entry.key, point); | |
} else if (entry.value.size > maxNumberOfValues) { | |
removeOldData(entry.value); | |
moveToLeft(entry.value, xStep); | |
valueProviderToInitialPoint.put(entry.key, entry.value.first()); | |
} | |
} | |
} | |
private void moveToLeft(Array<GraphPoint> points, float xAmountToMove) { | |
for (GraphPoint point : points) { | |
if (point.position != null) { | |
point.position.x -= xAmountToMove; | |
// ensure we are not outside of the widget's bounds | |
if (point.position.x < getX()) { | |
point.position.x = getX(); | |
} | |
} | |
} | |
} | |
private float convertToY(float value) { | |
float yValueRange = maxValue - minValue; | |
if (yValueRange > 0.0f) { | |
float percentOfRange = value / yValueRange; | |
float chartVerticalArea = getHeight(); | |
return (chartVerticalArea * percentOfRange) + getY(); | |
} | |
return 0; | |
} | |
private void removeOldData(Array values) { | |
int diff = values.size - maxNumberOfValues; | |
for (int i = 0; i < diff; i++) { | |
values.removeIndex(0); | |
} | |
} | |
private Color applyAlphaTo(Color originalColor, float alpha) { | |
Color color = new Color(originalColor); | |
color.a *= alpha; | |
return color; | |
} | |
/** | |
* The class the queries for the data you would like charted | |
*/ | |
public interface ValueProvider { | |
/** | |
* The value to be added to chart a this given point in time | |
*/ | |
float get(); | |
/** | |
* The color used to draw the line | |
*/ | |
Color getColor(); | |
} | |
private class GraphPoint { | |
final float value; | |
Vector2 position; | |
public GraphPoint(float value) { | |
this.value = value; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.badlogic.gdx.ApplicationAdapter; | |
import com.badlogic.gdx.ApplicationListener; | |
import com.badlogic.gdx.Gdx; | |
import com.badlogic.gdx.backends.lwjgl.LwjglApplication; | |
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration; | |
import com.badlogic.gdx.graphics.Color; | |
import com.badlogic.gdx.graphics.g2d.SpriteBatch; | |
import com.badlogic.gdx.graphics.glutils.ShapeRenderer; | |
import com.badlogic.gdx.scenes.scene2d.Stage; | |
import com.badlogic.gdx.scenes.scene2d.ui.Table; | |
import com.badlogic.gdx.utils.viewport.ScreenViewport; | |
import com.sun.management.OperatingSystemMXBean; | |
import libgdx.boot.widget.LineChart; | |
import org.lwjgl.opengl.Display; | |
import java.lang.management.ManagementFactory; | |
import java.util.Random; | |
import static com.badlogic.gdx.graphics.Color.DARK_GRAY; | |
import static com.badlogic.gdx.graphics.Color.LIGHT_GRAY; | |
public class LineChartTest { | |
private static final Color FPS_COLOR = Color.GREEN; | |
private static final Color CPU_COLOR = Color.RED; | |
private static final String title = "Line Chart"; | |
public static void main(String[] args) { | |
LwjglApplicationConfiguration config = new LwjglApplicationConfiguration(); | |
config.title = title; | |
config.useGL30 = false; | |
config.width = 320; | |
config.height = 240; | |
config.foregroundFPS = 120; | |
config.backgroundFPS = 2; | |
new LwjglApplication(newAppListener(), config); | |
} | |
private static ApplicationListener newAppListener() { | |
return new ApplicationAdapter() { | |
private Stage stage; | |
private ShapeRenderer shapeRenderer; | |
private SpriteBatch batch; | |
@Override | |
public void create() { | |
batch = new SpriteBatch(); | |
shapeRenderer = new ShapeRenderer(); | |
stage = new Stage(new ScreenViewport(), batch); | |
final com.sun.management.OperatingSystemMXBean operatingSystemMXBean = (com.sun.management.OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean(); | |
LineChart chart = new LineChart(shapeRenderer, 0.1f, 60.0f, | |
fpsProvider(), | |
cpuProvider(operatingSystemMXBean) | |
); | |
chart.setBackgroundColor(DARK_GRAY); | |
chart.setValueRange(0, 65); | |
chart.setGraphStep(10, LIGHT_GRAY); | |
Table table = new Table(); | |
table.setFillParent(true); | |
table.pad(2); | |
table.add(chart).expand(true, true).row(); | |
stage.addActor(table); | |
} | |
@Override | |
public void render() { | |
Gdx.gl.glClearColor(0, 0, 0, 0); | |
Gdx.gl.glClear(Gdx.gl.GL_COLOR_BUFFER_BIT); | |
stage.act(Gdx.graphics.getDeltaTime()); | |
stage.draw(); | |
Display.setTitle(title + " - FPS: " + Gdx.graphics.getFramesPerSecond()); | |
} | |
@Override | |
public void dispose() { | |
stage.dispose(); | |
batch.dispose(); | |
shapeRenderer.dispose(); | |
} | |
}; | |
} | |
private static LineChart.ValueProvider cpuProvider(final OperatingSystemMXBean operatingSystemMXBean) { | |
return new LineChart.ValueProvider() { | |
public float get() { | |
return (float) operatingSystemMXBean.getProcessCpuLoad(); | |
} | |
public Color getColor() { | |
return FPS_COLOR; | |
} | |
}; | |
} | |
private static LineChart.ValueProvider fpsProvider() { | |
return new LineChart.ValueProvider() { | |
public float get() { | |
return Gdx.graphics.getFramesPerSecond(); | |
} | |
public Color getColor() { | |
return CPU_COLOR; | |
} | |
}; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment