Skip to content

Commit

Permalink
Multiline graph (#180)
Browse files Browse the repository at this point in the history
create single graph with many lines; added method to assign qualitative colors.
renamed LineGraph to SingleLineGraph.  added tests.
Used MultiLineGraph to visualize joint change over time in Robot Arm Control Panel.
  • Loading branch information
i-make-robots authored Nov 8, 2023
2 parents 7808640 + 310d5a6 commit 7689bb9
Show file tree
Hide file tree
Showing 20 changed files with 848 additions and 1,506 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.marginallyclever</groupId>
<artifactId>RobotOverlord</artifactId>
<version>2.10.0</version>
<version>2.11.0</version>
<name>Robot Overlord</name>
<description>A friendly 3D user interface for controlling robots.</description>
<url>http://www.marginallyclever.com/</url>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.marginallyclever.convenience.swing.graph;

import com.marginallyclever.convenience.ColorRGB;

import java.awt.*;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;

/**
* A line in a {@link GraphModel}.
* @author Dan Royer
* @since 2.10.0
*/
public class GraphLine {
private final List<Point2D> points = new ArrayList<>();
private Color color = new Color(0);

public void addPoint(double x,double y) {
points.add(new Point2D.Double(x,y));
}

public List<Point2D> getPoints() {
return points;
}

public void setColor(Color color) {
this.color = color;
}

public Color getColor() {
return color;
}

public boolean isEmpty() {
return points.isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.marginallyclever.convenience.swing.graph;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* Container for graph data. Many named maps, each of which contains numbers.
*/
public class GraphModel {
private final Map<String,GraphLine> lines = new HashMap<>();

public void addLine(String name,GraphLine line) {
lines.put(name,line);
}

public GraphLine getLine(String name) {
return lines.get(name);
}

public int getLineCount() {
return lines.size();
}

public List<String> getLineNames() {
return new ArrayList<>(lines.keySet());
}

public void clear() {
lines.clear();
}

public void removeLine(String name) {
lines.remove(name);
}

public boolean isEmpty() {
if(lines.isEmpty()) return true;
for(GraphLine line : lines.values()) {
if(!line.isEmpty()) return false;
}
return true;
}

public GraphLine[] getLines() {
return lines.values().toArray(new GraphLine[0]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package com.marginallyclever.convenience.swing.graph;

import javax.swing.*;
import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.List;

/**
* A simple line graph. Assumes at most one y value per x value. Interpolates between given values.
*
* @author Dan Royer
* @since 2.5.0
*/
public class MultiLineGraph extends JPanel {
private GraphModel model = new GraphModel();
private double yMin, yMax, xMin, xMax;
private Color majorLineColor = new Color(0.8f,0.8f,0.8f);
private Color minorLineColor = new Color(0.9f,0.9f,0.9f);
private int gridSpacingX = 10;
private int gridSpacingY = 10;

public MultiLineGraph() {
super();
setBackground(Color.WHITE);
}

public void setModel(GraphModel model) {
this.model = model;
}

public GraphModel getModel() {
return model;
}

/**
* Set the visible range of the graph. Values outside this range will not be drawn.
* @param range the minimum values and size of the range.
*/
public void setRange(Rectangle2D.Double range) {
this.xMin = range.getMinX();
this.xMax = range.getMaxX();
this.yMin = range.getMinY();
this.yMax = range.getMaxY();
}

public Rectangle2D.Double getRange() {
return new Rectangle2D.Double(xMin,yMin,xMax-xMin,yMax-yMin);
}

/**
* Set the visible range of the graph to match the data in the model.
*/
public void setRangeToModel() {
if(model.isEmpty()) return;

double [] bounds = getDataBounds();

xMin = bounds[0];
xMax = bounds[1];
yMin = bounds[2];
yMax = bounds[3];
}

/**
*
* @return minx,maxx,miny,maxy
*/
public double [] getDataBounds() {
if(model.isEmpty()) return new double[] {0,0,0,0};

double minX = Double.POSITIVE_INFINITY;
double maxX = Double.NEGATIVE_INFINITY;
double minY = Double.POSITIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY;

for(GraphLine line : model.getLines()) {
List<Point2D> points = line.getPoints();
for(Point2D point : points) {
if(point.getX()<minX) minX = point.getX();
if(point.getX()>maxX) maxX = point.getX();
if(point.getY()<minY) minY = point.getY();
if(point.getY()>maxY) maxY = point.getY();
}
}

return new double[] {minX,maxX,minY,maxY};
}

@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D)g;
super.paintComponent(g2d);
drawGrid(g2d);
drawGraphLines(g2d);
}

private void drawGrid(Graphics g) {
g.setColor(minorLineColor);

int width = getWidth();
int height = getHeight();

double [] bounds = getDataBounds();
double minX = bounds[0];
double maxX = bounds[1];
double minY = bounds[2];
double maxY = bounds[3];

// draw vertical lines
double left = Math.floor(minX / gridSpacingX) * gridSpacingX;
for (double x = left; x <= maxX; x += gridSpacingX) {
int x1 = transformX(x);
g.drawLine(x1, 0, x1, height);
}

// draw horizontal lines
double bottom = Math.floor(minY / gridSpacingY) * gridSpacingY;
for (double y = bottom; y <= maxY; y += gridSpacingY) {
int y1 = transformY(y);
g.drawLine(0, y1, width, y1);
}
}

private void drawGraphLines(Graphics g) {
if(model.isEmpty()) return;

Graphics2D g2d = (Graphics2D)g;
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
int numLines = model.getLineCount();
List<String> names = model.getLineNames();
for(int i=0;i<numLines;++i) {
drawGraphLine(g,model.getLine(names.get(i)));
}
}

private void drawGraphLine(Graphics g,GraphLine line) {
if(model.isEmpty()) return;

g.setColor(line.getColor());
int height = getHeight();

List<Point2D> points = line.getPoints();
int prevX = transformX(points.get(0).getX());
int prevY = transformY(points.get(0).getY());
for(Point2D point : points) {
int currentX = transformX(point.getX());
int currentY = transformY(point.getY());
g.drawLine(prevX, height - prevY, currentX, height - currentY);
prevX = currentX;
prevY = currentY;
}
}

private int transformX(double x) {
return (int) (((x - xMin) / (xMax - xMin)) * getWidth());
}

private int transformY(double y) {
return (int) (((y - yMin) / (yMax - yMin)) * getHeight());
}

public Color getMajorLineColor() {
return majorLineColor;
}

public void setMajorLineColor(Color majorLineColor) {
this.majorLineColor = majorLineColor;
}

public Color getMinorLineColor() {
return minorLineColor;
}

public void setMinorLineColor(Color minorLineColor) {
this.minorLineColor = minorLineColor;
}

public int getGridSpacingX() {
return gridSpacingX;
}

public void setGridSpacingX(int gridSpacingX) {
this.gridSpacingX = gridSpacingX;
}

public int getGridSpacingY() {
return gridSpacingY;
}

public void setGridSpacingY(int gridSpacingY) {
this.gridSpacingY = gridSpacingY;
}

/**
* Set colors for each line in the graph based on the number of lines and the color wheel.
*/
public void assignQualitativeColors() {
int count = model.getLineCount();
GraphLine[] lines = model.getLines();
// use RGB color wheel to generate 'count' colors
for(int i=0;i<count;++i) {
float hue = (float)i/count;
Color color = Color.getHSBColor(hue,1,1);
lines[i].setColor(color);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package com.marginallyclever.convenience.swing;
package com.marginallyclever.convenience.swing.graph;

import com.marginallyclever.convenience.helpers.StringHelper;
import com.marginallyclever.convenience.log.Log;

import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
Expand All @@ -19,7 +17,7 @@
* @author Dan Royer
* @since 2.5.0
*/
public class LineGraph extends JPanel {
public class SingleLineGraph extends JPanel {
private final TreeMap<Double, Double> data = new TreeMap<>();
private double yMin, yMax, xMin, xMax;
private Color majorLineColor = new Color(0.8f,0.8f,0.8f);
Expand All @@ -29,7 +27,7 @@ public class LineGraph extends JPanel {
private boolean mouseIn = false;
private double mouseX, mouseY;

public LineGraph() {
public SingleLineGraph() {
super();
setBackground(Color.WHITE);

Expand Down Expand Up @@ -65,6 +63,8 @@ public void mouseMoved(MouseEvent e) {
}

public void updateToolTip(MouseEvent event) {
if(data.isEmpty()) return;

double mx = event.getX();
double scaledX = mx / getWidth();
mouseX = xMin + (xMax - xMin) * scaledX;
Expand Down Expand Up @@ -100,6 +100,14 @@ public void clear() {
data.clear();
}

/**
* Set the bounds of the graph. This limits the range of values that can be displayed.
* Data outside this range will not be drawn, but will still be stored.
* @param xMin the minimum x value
* @param xMax the maximum x value
* @param yMin the minimum y value
* @param yMax the maximum y value
*/
public void setBounds(double xMin,double xMax,double yMin,double yMax) {
this.xMin = xMin;
this.xMax = xMax;
Expand Down Expand Up @@ -277,31 +285,4 @@ public int getGridSpacingY() {
public void setGridSpacingY(int gridSpacingY) {
this.gridSpacingY = gridSpacingY;
}

// TEST

public static void main(String[] args) {
Log.start();

try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ignored) {}

LineGraph graph = new LineGraph();
double v = Math.random()*500;
for(int i=0;i<250;++i) {
graph.addValue(i,v);
v += Math.random()*10-5;
}
graph.setBoundsToData();
graph.setBorder(new BevelBorder(BevelBorder.LOWERED));

JFrame frame = new JFrame("LineGraph");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setPreferredSize(new Dimension(800,400));
frame.setContentPane(graph);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
Loading

0 comments on commit 7689bb9

Please sign in to comment.