Last active
March 14, 2022 17:49
-
-
Save trashgod/a3f7581417db19bf5b6dc52d3647a2f9 to your computer and use it in GitHub Desktop.
PriceVolumeChart—JFreeChart combining XYBarRenderer and XYLineAndShapeRenderer
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 java.awt.BasicStroke; | |
import static java.awt.BasicStroke.*; | |
import java.awt.Color; | |
import java.awt.Dimension; | |
import java.awt.Paint; | |
import java.text.NumberFormat; | |
import java.text.SimpleDateFormat; | |
import java.time.Duration; | |
import java.time.LocalDate; | |
import java.util.ArrayList; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import javax.swing.JFrame; | |
import javax.swing.SwingUtilities; | |
import javax.swing.UIManager; | |
import org.jfree.chart.ChartFactory; | |
import org.jfree.chart.ChartMouseEvent; | |
import org.jfree.chart.ChartMouseListener; | |
import org.jfree.chart.ChartPanel; | |
import org.jfree.chart.JFreeChart; | |
import org.jfree.chart.annotations.XYBoxAnnotation; | |
import org.jfree.chart.annotations.XYTextAnnotation; | |
import org.jfree.chart.axis.DateAxis; | |
import org.jfree.chart.axis.NumberAxis; | |
import org.jfree.chart.entity.ChartEntity; | |
import org.jfree.chart.entity.XYItemEntity; | |
import org.jfree.chart.labels.StandardCrosshairLabelGenerator; | |
import org.jfree.chart.labels.StandardXYToolTipGenerator; | |
import org.jfree.chart.panel.CrosshairOverlay; | |
import org.jfree.chart.plot.Crosshair; | |
import org.jfree.chart.plot.XYPlot; | |
import org.jfree.chart.renderer.xy.XYBarRenderer; | |
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; | |
import org.jfree.chart.ui.Layer; | |
import org.jfree.chart.ui.TextAnchor; | |
import org.jfree.data.time.Day; | |
import org.jfree.data.time.TimeSeries; | |
import org.jfree.data.time.TimeSeriesCollection; | |
import org.jfree.data.time.TimeSeriesDataItem; | |
import org.jfree.data.xy.XYDataset; | |
/** | |
* @see https://stackoverflow.com/q/64266993/230513 | |
*/ | |
public final class PriceVolumeChart { | |
private static final double BAR_FACTOR = 0.5; | |
private static final double BAR_SHIFT = Duration.ofDays(1).toMillis() * BAR_FACTOR; | |
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); | |
private static final StandardXYToolTipGenerator TT_GENERATOR = new StandardXYToolTipGenerator( | |
StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT, | |
DATE_FORMAT, NumberFormat.getCurrencyInstance()); | |
private TimeSeries priceSeries = new TimeSeries("Price"); | |
private TimeSeries volumeSeries = new TimeSeries("Volume"); | |
private final Crosshair xCrosshair; | |
private final Crosshair yCrosshair; | |
private List<String> volumeDelta = new ArrayList<>(); | |
private ChartPanel chartPanel; | |
private XYTextAnnotation note; | |
private XYPlot xyPlot; | |
private XYLineAndShapeRenderer xyRenderer; | |
private XYDataset priceData; | |
public PriceVolumeChart(String symbol, int index, Map.Entry<Integer, Integer> hightlight) { | |
JFreeChart chart = createChart(symbol); | |
chartPanel = new ChartPanel(chart) { | |
@Override | |
public Dimension getPreferredSize() { | |
return new Dimension(800, 500); | |
} | |
}; | |
chartPanel.addChartMouseListener(new MouseListener()); | |
// highlights | |
double t0 = priceSeries.getTimePeriod(hightlight.getKey()).getFirstMillisecond() - BAR_SHIFT; | |
double t1 = priceSeries.getTimePeriod(hightlight.getValue()).getLastMillisecond() - BAR_SHIFT; | |
double minY = 0; | |
double maxY = priceSeries.getMaxY() * 1.1; | |
Color color = new Color(0, 0, 255, 50); | |
xyRenderer.addAnnotation(new XYBoxAnnotation( | |
t0, minY, t1, maxY, null, null, color), Layer.BACKGROUND); | |
// crosshairs | |
float[] dash = {4f}; | |
BasicStroke bs = new BasicStroke(1, CAP_BUTT, JOIN_ROUND, 10f, dash, 0f); | |
xCrosshair = new Crosshair(Double.NaN, Color.black, bs); | |
xCrosshair.setLabelBackgroundPaint(Color.black); | |
xCrosshair.setLabelFont(xCrosshair.getLabelFont().deriveFont(14f)); | |
xCrosshair.setLabelPaint(Color.white); | |
xCrosshair.setLabelGenerator(new StandardCrosshairLabelGenerator( | |
" Volume: {0} ", NumberFormat.getInstance()) { | |
@Override | |
public String generateLabel(Crosshair crosshair) { | |
long ms = (long) crosshair.getValue(); | |
TimeSeriesDataItem item = new TimeSeriesDataItem(new Day(), Double.NaN); | |
for (int i = 0; i < volumeSeries.getItemCount(); i++) { | |
item = volumeSeries.getDataItem(i); | |
if (ms == item.getPeriod().getFirstMillisecond()) { | |
break; | |
} | |
} | |
return super.generateLabel(new Crosshair(item.getValue().doubleValue())); | |
} | |
}); | |
xCrosshair.setLabelVisible(true); | |
yCrosshair = new Crosshair(Double.NaN, Color.black, bs); | |
yCrosshair.setLabelBackgroundPaint(Color.black); | |
yCrosshair.setLabelFont(yCrosshair.getLabelFont().deriveFont(14f)); | |
yCrosshair.setLabelPaint(Color.white); | |
yCrosshair.setLabelGenerator(new StandardCrosshairLabelGenerator( | |
" Price: {0} ", NumberFormat.getCurrencyInstance())); | |
yCrosshair.setLabelVisible(true); | |
CrosshairOverlay crosshairOverlay = new CrosshairOverlay(); | |
crosshairOverlay.addDomainCrosshair(xCrosshair); | |
crosshairOverlay.addRangeCrosshair(yCrosshair); | |
chartPanel.addOverlay(crosshairOverlay); | |
if (index >= 0 && index < volumeSeries.getItemCount()) { | |
TimeSeriesDataItem itemX = volumeSeries.getDataItem(index); | |
double time = itemX.getPeriod().getFirstMillisecond(); | |
xCrosshair.setValue(time); | |
TimeSeriesDataItem itemY = priceSeries.getDataItem(index); | |
double price = itemY.getValue().doubleValue(); | |
yCrosshair.setValue(price); | |
String text = TT_GENERATOR.generateLabelString(priceData, 0, index); | |
note = new XYTextAnnotation(text, time, price - 1); | |
note.setFont(UIManager.getFont("ToolTip.font")); | |
note.setBackgroundPaint(UIManager.getColor("ToolTip.background")); | |
note.setTextAnchor(index < volumeDelta.size() / 2 | |
? TextAnchor.TOP_LEFT : TextAnchor.TOP_RIGHT); | |
note.setOutlinePaint(Color.blue); | |
note.setOutlineVisible(true); | |
xyRenderer.addAnnotation(note); | |
} | |
} | |
private JFreeChart createChart(String symbol) { | |
createSeries(); | |
priceData = new TimeSeriesCollection(priceSeries); | |
JFreeChart chart = ChartFactory.createTimeSeriesChart( | |
symbol, "Date", "Price", priceData, true, true, true); | |
xyPlot = (XYPlot) chart.getPlot(); | |
xyPlot.setBackgroundPaint(Color.lightGray); | |
NumberAxis rangeAxis1 = (NumberAxis) xyPlot.getRangeAxis(); | |
rangeAxis1.setLowerMargin(0.40); // Leave room for volume bars | |
rangeAxis1.setNumberFormatOverride(NumberFormat.getCurrencyInstance()); | |
xyRenderer = (XYLineAndShapeRenderer) xyPlot.getRenderer(); | |
xyRenderer.setDefaultToolTipGenerator(TT_GENERATOR); | |
NumberAxis rangeAxis2 = new NumberAxis("Volume"); | |
rangeAxis2.setUpperMargin(1.00); // Leave room for price line | |
xyPlot.setRangeAxis(1, rangeAxis2); | |
xyPlot.setDataset(1, new TimeSeriesCollection(volumeSeries)); | |
xyPlot.setRangeAxis(1, rangeAxis2); | |
xyPlot.mapDatasetToRangeAxis(1, 1); | |
VolumeRender volumeRenderer = new VolumeRender(); | |
volumeRenderer.setShadowVisible(false); | |
volumeRenderer.setBarAlignmentFactor(BAR_FACTOR); | |
volumeRenderer.setDefaultToolTipGenerator(TT_GENERATOR); | |
xyPlot.setRenderer(1, volumeRenderer); | |
DateAxis domainAxis = (DateAxis) xyPlot.getDomainAxis(); | |
domainAxis.setLowerMargin(0.05); | |
return chart; | |
} | |
private void createSeries() { | |
String[] data = {"Date, Open, High, Low, Close, Adj.Close, Volume", | |
"2020-07-17,44.110001,44.369999,41.919998,42.509998,42.323395,849700", | |
"2020-07-20,41.630001,41.680000,39.669998,40.119999,39.943886,1319300", | |
"2020-07-21,40.880001,42.860001,40.860001,42.270000,42.084450,2070300", | |
"2020-07-22,41.919998,42.700001,41.090000,42.570000,42.383133,1317600", | |
"2020-07-23,43.919998,46.389999,43.279999,44.759998,44.563519,1917700", | |
"2020-07-24,46.500000,46.500000,43.950001,44.410000,44.215057,1384600", | |
"2020-07-27,44.000000,44.240002,42.610001,43.860001,43.667469,799800", | |
"2020-07-28,43.389999,44.590000,42.930000,43.020000,42.831158,699700", | |
"2020-07-29,42.759998,45.590000,42.740002,45.430000,45.230579,826200", | |
"2020-07-30,44.160000,44.639999,42.959999,44.500000,44.304661,798100", | |
"2020-07-31,44.330002,44.419998,42.580002,44.360001,44.165276,1037800", | |
"2020-08-03,44.560001,45.599998,43.419998,44.939999,44.742729,797000", | |
"2020-08-04,44.900002,45.500000,43.450001,43.540001,43.348877,971100", | |
"2020-08-05,44.860001,45.389999,43.650002,45.330002,45.131020,902000", | |
"2020-08-06,45.049999,46.279999,44.330002,45.299999,45.101147,645200", | |
"2020-08-07,44.849998,46.189999,44.189999,46.150002,45.947418,604900", | |
"2020-08-10,46.669998,48.410000,46.549999,47.290001,47.082417,960200", | |
"2020-08-11,49.110001,50.849998,48.799999,48.910000,48.695301,1187700", | |
"2020-08-12,49.759998,50.009998,47.060001,47.840000,47.630001,752800", | |
"2020-08-13,46.950001,48.369999,46.459999,47.110001,47.110001,535700"}; | |
priceSeries = new TimeSeries("Price"); | |
volumeSeries = new TimeSeries("Volume"); | |
long lastVolume = 0; | |
String items[]; | |
for (int i = 1; i < data.length; i++) { | |
items = data[i].split(","); | |
LocalDate d = LocalDate.parse(items[0]); | |
Day day = new Day(d.getDayOfMonth(), d.getMonthValue(), d.getYear()); | |
priceSeries.add(day, Double.parseDouble(items[5])); | |
long volume = Long.parseLong(items[6]); | |
volumeSeries.add(day, volume); | |
volumeDelta.add(volume >= lastVolume ? "+" : "-"); | |
lastVolume = volume; | |
} | |
} | |
private class MouseListener implements ChartMouseListener { | |
@Override | |
public void chartMouseClicked(ChartMouseEvent event) { | |
// unused | |
} | |
@Override | |
public void chartMouseMoved(ChartMouseEvent event) { | |
ChartEntity chartentity = event.getEntity(); | |
if (chartentity instanceof XYItemEntity) { | |
if (!xyRenderer.getAnnotations().isEmpty()) { | |
xyRenderer.removeAnnotation(note); | |
} | |
XYItemEntity e = (XYItemEntity) chartentity; | |
XYDataset d = e.getDataset(); | |
int s = e.getSeriesIndex(); | |
int i = e.getItem(); | |
double x = d.getXValue(s, i); | |
double y = d.getYValue(s, i); | |
xCrosshair.setValue(x); | |
yCrosshair.setValue(y); | |
} | |
} | |
} | |
private class VolumeRender extends XYBarRenderer { | |
@Override | |
public Paint getItemPaint(int row, int col) { | |
return volumeDelta.get(col).equals("+") | |
? super.getItemPaint(row, col) : new Color(0.5f, 0.2f, 0.5f); | |
} | |
} | |
private static void createAndShowGUI() { | |
Map.Entry<Integer, Integer> highlight = new HashMap.SimpleEntry<>(6, 8); | |
PriceVolumeChart demo = new PriceVolumeChart("ADS", 3, highlight); | |
JFrame frame = new JFrame("PriceVolumeChart"); | |
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); | |
frame.setContentPane(demo.chartPanel); | |
frame.pack(); | |
frame.setLocationRelativeTo(null); | |
frame.setVisible(true); | |
} | |
public static void main(String[] args) { | |
SwingUtilities.invokeLater(PriceVolumeChart::createAndShowGUI); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment