This question is related to my previous question [ How to show cross lines and red color for price drop in JFreeChart? ]. Thanks to @trashgod I have solved a lot of issues from that question, I'll ask what's left in this question. My sample code looks like this :
import java.awt.*;
import java.awt.event.*;
import java.text.*;
import java.util.*;
import javax.swing.*;
import org.jfree.chart.*;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.entity.*;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYBarRenderer;
import org.jfree.data.time.*;
import org.jfree.data.xy.XYDataset;
import org.jfree.chart.labels.*;
import org.jfree.chart.panel.*;
import org.jfree.chart.plot.*;
public class PriceVolume_Chart extends JPanel implements ChartMouseListener // A demo application for price-volume chart.
{
ChartPanel panel;
TimeSeries Price_series=new TimeSeries("Price");
TimeSeries Volume_Series=new TimeSeries("Volume");
Crosshair xCrosshair,yCrosshair;
static Vector<String> Volume_Color_Vector=new Vector();
public PriceVolume_Chart(String Symbol)
{
JFreeChart chart=createChart(Symbol);
panel=new ChartPanel(chart,true,true,true,false,true);
panel.setPreferredSize(new Dimension(1000,500));
panel.addChartMouseListener(this);
CrosshairOverlay crosshairOverlay=new CrosshairOverlay();
float[] dash={2f,0f,2f};
BasicStroke bs=new BasicStroke(1,BasicStroke.CAP_BUTT,BasicStroke.JOIN_ROUND,1.0f,dash,2f);
xCrosshair=new Crosshair(Double.NaN,Color.GRAY,bs);
xCrosshair.setLabelBackgroundPaint(new Color(0f,0f,0f,1f));
xCrosshair.setLabelFont(xCrosshair.getLabelFont().deriveFont(14f));
xCrosshair.setLabelPaint(new Color(1f,1f,1f,1f));
xCrosshair.setLabelVisible(true);
yCrosshair=new Crosshair(Double.NaN,Color.GRAY,bs);
yCrosshair.setLabelBackgroundPaint(new Color(0f,0f,0f,1f));
yCrosshair.setLabelFont(xCrosshair.getLabelFont().deriveFont(14f));
yCrosshair.setLabelPaint(new Color(1f,1f,1f,1f));
yCrosshair.setLabelVisible(true);
crosshairOverlay.addDomainCrosshair(xCrosshair);
crosshairOverlay.addRangeCrosshair(yCrosshair);
panel.addOverlay(crosshairOverlay);
add(panel);
xCrosshair.setValue(1.5700752E12);
xCrosshair.setVisible(true);
yCrosshair.setValue(119.959007);
yCrosshair.setVisible(true);
}
private JFreeChart createChart(String Symbol)
{
createPriceDataset(Symbol);
XYDataset priceData=new TimeSeriesCollection(Price_series);
JFreeChart chart=ChartFactory.createTimeSeriesChart(Symbol,"Date",getYLabel("Price ( $ )"),priceData,true,true,true);
XYPlot plot=chart.getXYPlot();
plot.setBackgroundPaint(new Color(192,196,196));
NumberAxis rangeAxis1=(NumberAxis)plot.getRangeAxis();
rangeAxis1.setLowerMargin(0.40); // Leave room for volume bars
// plot.getRenderer().setDefaultToolTipGenerator(new StandardXYToolTipGenerator(StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT,DateFormat.getDateInstance(), NumberFormat.getCurrencyInstance()));
plot.getRenderer().setDefaultToolTipGenerator(new StandardXYToolTipGenerator(StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT,new SimpleDateFormat("yyyy-MM-d"),NumberFormat.getCurrencyInstance()));
// DecimalFormat format=new DecimalFormat("00.00");
// rangeAxis1.setNumberFormatOverride(format);
// rangeAxis1.setNumberFormatOverride(NumberFormat.getCurrencyInstance());
NumberAxis rangeAxis2=new NumberAxis("Volume");
rangeAxis2.setUpperMargin(1.00); // Leave room for price line
rangeAxis2.setNumberFormatOverride(NumberFormat.getNumberInstance());
plot.setRangeAxis(1,rangeAxis2);
plot.setDataset(1,new TimeSeriesCollection(Volume_Series));
plot.setRangeAxis(1,rangeAxis2);
plot.mapDatasetToRangeAxis(1,1);
plot.setRenderer(1,new MyRender());
return chart;
}
private void createPriceDataset(String Symbol)
{
String Lines[]=new String[21], Items[], Date;
int Year, Month, Day;
long Volume,Last_Volume=0;
double Price;
Lines[0]="Date,Open,High,Low,Close,Adj Close,Volume";
Lines[1]="2019-09-23,129.589996,130.710007,128.240005,129.300003,126.555969,553700";
Lines[2]="2019-09-24,129.309998,129.529999,125.500000,126.750000,124.060089,732900";
Lines[3]="2019-09-25,126.570000,128.500000,126.190002,127.879997,125.166100,422000";
Lines[4]="2019-09-26,127.849998,128.589996,127.169998,127.779999,125.068230,376100";
Lines[5]="2019-09-27,128.669998,129.289993,126.389999,126.419998,123.737083,332900";
Lines[6]="2019-09-30,126.589996,128.789993,125.849998,128.130005,125.410797,456700";
Lines[7]="2019-10-01,129.039993,130.899994,125.480003,126.040001,123.365158,322700";
Lines[8]="2019-10-02,125.059998,125.180000,121.620003,123.120003,120.507126,577100";
Lines[9]="2019-10-03,122.650002,123.320000,119.089996,122.559998,119.959007,581300";
Lines[10]="2019-10-04,122.970001,123.949997,121.320000,123.879997,121.250992,315700";
Lines[11]="2019-10-07,123.139999,124.610001,122.669998,122.879997,120.272217,510300";
Lines[12]="2019-10-08,121.720001,121.879997,118.089996,118.660004,116.141777,616600";
Lines[13]="2019-10-09,119.410004,119.610001,116.680000,118.419998,115.906868,603300";
Lines[14]="2019-10-10,119.089996,121.209999,117.080002,118.209999,115.701324,483300";
Lines[15]="2019-10-11,120.330002,123.040001,119.720001,122.550003,119.949226,700500";
Lines[16]="2019-10-14,122.550003,123.720001,120.940002,122.540001,119.939430,492900";
Lines[17]="2019-10-15,122.849998,124.220001,121.230003,123.699997,121.074814,598200";
Lines[18]="2019-10-16,123.889999,124.849998,122.800003,123.209999,120.595207,663600";
Lines[19]="2019-10-17,123.449997,124.889999,122.790001,123.360001,120.742035,563200";
Lines[20]="2019-10-18,123.050003,124.620003,122.459999,123.540001,120.918213,650300";
for (int i=1;i<Lines.length;i++)
{
Items=Lines[i].split(",");
Date=Items[0].replace("-0","-");
Price=Double.parseDouble(Items[5]);
Volume=Long.parseLong(Items[6]);
Items=Date.split("-");
Year=Integer.parseInt(Items[0]);
Month=Integer.parseInt(Items[1]);
Day=Integer.parseInt(Items[2]);
Price_series.add(new Day(Day,Month,Year),Price);
Volume_Series.add(new Day(Day,Month,Year),Volume);
Volume_Color_Vector.add(Volume>=Last_Volume?"+":"-");
Last_Volume=Volume;
}
}
@Override
public void chartMouseClicked(ChartMouseEvent event)
{
// ignore
}
public void chartMouseMoved(ChartMouseEvent cmevent)
{
ChartEntity chartentity=cmevent.getEntity();
if (chartentity instanceof XYItemEntity)
{
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);
// Out("x = "+x+" y = "+y);
xCrosshair.setValue(x);
yCrosshair.setValue(y);
}
}
String getYLabel(String Text)
{
String Result="";
for (int i=0;i<Text.length();i++) Result+=Text.charAt(i)+(i<Text.length()-1?"\u2009":"");
// Out(Result);
return Result;
}
private static void out(String message) { System.out.print(message); }
private static void Out(String message) { System.out.println(message); }
// Create the GUI and show it. For thread safety, this method should be invoked from the event-dispatching thread.
static void Create_And_Show_GUI()
{
final PriceVolume_Chart demo=new PriceVolume_Chart("ADS");
JFrame frame=new JFrame("PriceVolume_Chart Frame");
frame.add(demo);
frame.addWindowListener(new WindowAdapter()
{
public void windowActivated(WindowEvent e) { }
public void windowClosed(WindowEvent e) { }
public void windowClosing(WindowEvent e) { System.exit(0); }
public void windowDeactivated(WindowEvent e) { }
public void windowDeiconified(WindowEvent e) { demo.repaint(); }
public void windowGainedFocus(WindowEvent e) { demo.repaint(); }
public void windowIconified(WindowEvent e) { }
public void windowLostFocus(WindowEvent e) { }
public void windowOpening(WindowEvent e) { demo.repaint(); }
public void windowOpened(WindowEvent e) { }
public void windowResized(WindowEvent e) { demo.repaint(); }
public void windowStateChanged(WindowEvent e) { demo.repaint(); }
});
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args)
{
// Schedule a job for the event-dispatching thread : creating and showing this application's GUI.
SwingUtilities.invokeLater(new Runnable() { public void run() { Create_And_Show_GUI(); } });
}
}
class MyRender extends XYBarRenderer
{
@Override
public Paint getItemPaint(int row,int col)
{
System.out.println(row+" "+col+" "+super.getItemPaint(row,col));
return PriceVolume_Chart.Volume_Color_Vector.elementAt(col).equals("+")?super.getItemPaint(row,col):new Color(0.56f,0.2f,0.5f,1f);
}
}
My questions are :
[1] How to correctly show Volume data in cross-hair [ it's now showing the incorrect value, yet I don't know how to fix it ] ?
[2] How to point the mouse on the N-th [ e.g. 12th ] day when the app starts ?
More details to the 2nd question, when I move the mouse, I can output the x,y value of the mouse on the screen, and I can do the following to set the mouse at the desired location at start up :
...
panel.addOverlay(crosshairOverlay);
add(panel);
xCrosshair.setValue(1.5700752E12);
yCrosshair.setValue(119.959007);
But since I don't know the [x,y] location of the N-th day, therefore I don't know how to set the mouse on that day.
The reason I want to do this is : in my app, I want to look at some data on a certain day in another panel, and user can click on a certain day and I want the JFreeChart to instantly point to that day on the chart.
So my 2nd question is asking : "How can I get the [x,y] location of the N-th day on the chart ?
How to correctly show volume data in cross-hair; it's now showing the incorrect value.
As suggested here, "The x value is the date…absent formatting, the crosshair correctly displays the number of milliseconds from the epoch." To alter the crosshair's display, add a custom CrosshairLabelGenerator
. The implementation below searches your volume series for the matching timestamp and formats the corresponding value as a number.
xCrosshair.setLabelGenerator(new CrosshairLabelGenerator() {
@Override
public String generateLabel(Crosshair crosshair) {
long ms = (long) crosshair.getValue();
TimeSeriesDataItem item = null;
for (int i = 0; i < Volume_Series.getItemCount(); i++) {
item = Volume_Series.getDataItem(i);
if (ms == item.getPeriod().getFirstMillisecond()) {
break;
}
}
long volume = item.getValue().longValue();
return NumberFormat.getInstance().format(volume);
}
});
Consider adjusting the lower margin of the domain axis for symmetry.
DateAxis domainAxis = (DateAxis) plot.getDomainAxis();
domainAxis.setLowerMargin(0.05);
Also consider setting the alignment factor of MyRenderer
to center the bar under the price point.
this.setBarAlignmentFactor(0.5);
As the user controls the mouse, I would avoid moving the crosshair programmatically. Instead, look at a suitable Annotation
or Marker
. Having said that, compare your implementation of chartMouseMoved()
to the one shown here. Your's updates for each new XYItemEntity
encountered, so the crosshairs "snap" to points or bars. In contrast, the latter one updates continuously, scaling via java2DToValue()
, as described here.