Skip to content

Instantly share code, notes, and snippets.

@trashgod
Last active March 14, 2022 17:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save trashgod/a3f7581417db19bf5b6dc52d3647a2f9 to your computer and use it in GitHub Desktop.
Save trashgod/a3f7581417db19bf5b6dc52d3647a2f9 to your computer and use it in GitHub Desktop.
PriceVolumeChart—JFreeChart combining XYBarRenderer and XYLineAndShapeRenderer
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