はじめに
いまさら JFreeChart を使い始める人はほとんどいないと思う。
ただ、java 使い としては、ちょっと気軽にグラフを描きたかった。
JSPで書く
今時は、WEBアプリケーションを使うのが普通だろう。
ここでは、Servletは一切使わずJSPだけでお手軽に使う。
仕組みは簡単、JSPの最後に、
// バイナリ出力ストリームにJPEG形式で画像を出力
// 例)800×600ピクセル
response.setContentType("image/jpeg");
ServletOutputStream sos = response.getOutputStream();
ChartUtils.writeChartAsJPEG(sos,chart,800,600); // 1.5.x
// ChartUtilities.writeChartAsJPEG(sos,chart,800,600); // 1.0.x
これで、ファイルを作成することなく表示される。
ページ中に挿入したければ、html に
<img src="chart_sample.jsp">
のように画像と同様に扱える。
パラメータを渡して動的に変える事も容易。
Chrome ではChunkLoadErrorになる場合があった。(Firefoxでは起こらない)
描画処理が遅いとなるようだ。また、Apache を介すと起こらなかった。
環境
WEBコンテナとして、Tomcat9
JFreeChart は maven repository からダウンロードして、lib フォルダに置く。
- jcommon-1.5.5.jar
バージョンの 1.0.x と 1.5.x には完全な上位互換性は無い
https://github.com/jfree/jfreechart
jfreechart-1.0.19 を使った記事も多いが、特別な理由が無い限り新バージョンで良い。
古いバージョンを使う場合、jcommon-1.0.24.jar も必要
実用としては、データベースを使う必要がある。
PostgreSQL を使用している。
jsp は「メモ帳」で書いている。
株の資産推移を見たい
サンプルというより実用。
証券会社の提供する資産推移は簡略過ぎて役に立たない。
グラフ化は Excel だと簡単なのだが、データをいちいち吐き出して操作するのが面倒。
完成版はこんな感じ。
株式の資産推移(青)と日経平均株価(赤)を表示している。
さらに、過去の所有資産を時価(今日の株価)で評価したもの(緑)と比較している。
つまりその時から何も売買しなければその価額になっていただろう、というものだ。
資産(青)は順調に増えているように見える。
でも今の株価で評価(緑)すると、例えば2年前の2023年1月以降に何も売買していなければ、今より評価額は上だった。自分ではうまく運用したつもりだったので、結構ショックだ。
(運用の才能はなさそう)
過去に行った売買が本当にその後のプラスになっているかどうかを客観的に振り返って見る為に作成した。
勿論、これ以外に売買損益や含み益等も分析できる。
JFreeChart の学習方法
まず最初に、基本となるチャートを選択する。
枯れているので、チャートのサンプルは探せば豊富にある。
自分が望むものに近いものを探し、後は肉付けしていくのが良いと思う。
残念な事に、基本的な考え方を解説したものが少なく、その後は試行錯誤を繰り返したのが実情である。
そういうこともあり、まとめてみた。
一番大切な事は、グラフの横軸が大きく2種類に分類される事。
- カテゴリー(Category)型
- XY型(時系列も含む)
最初の選択を誤ると、途中で行き詰る。
基本チャートには以下がある。
株価チャートの例
練習台として日経平均を描画した例があまり無いようなので書いてみた。
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ page import="java.io.*"%>
<%@ page import="java.util.ArrayList"%>
<%@ page import="org.jfree.chart.*"%>
<%@ page import="org.jfree.data.xy.*"%>
<%@ page import="org.jfree.chart.plot.*"%>
<%@ page import="org.jfree.chart.axis.*"%>
<%@ include file="include_db.jsp" %>
<%
// 株価グラフ
// 1.5.x では設定しないとラベルが文字化けする
ChartFactory.setChartTheme(StandardChartTheme.createLegacyTheme());
// ローソク足グラフのデータを用意
Connection conn = ds.getConnection();
String where = " where 日付 between '2023-11-01' and '2024-05-31' and 銘柄コード = '0000'";
String sql = "select count(*) from kabuka2 " + where;
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery( sql );
rs.next();
int size = rs.getInt(1);
rs.close();
Date[] aryDate = new Date[ size ];
double[][] aryValues = new double[5][ size ];
sql = "select 日付 ,高値 ,安値, 始値, 終値, 出来高 from kabuka2" + where + " order by 日付";
rs = stmt.executeQuery( sql );
int i = 0;
while( rs.next() ) {
aryDate[i] = rs.getDate(1);
aryValues[0][i] = rs.getDouble(2);
aryValues[1][i] = rs.getDouble(3);
aryValues[2][i] = rs.getDouble(4);
aryValues[3][i] = rs.getDouble(5);
aryValues[4][i] = rs.getDouble(6);
i++;
}
conn.close();
DefaultHighLowDataset data = new DefaultHighLowDataset(
"日経平均",
aryDate,
aryValues[0],
aryValues[1],
aryValues[2],
aryValues[3],
aryValues[4]
);
// ローソク足チャート
JFreeChart chart = ChartFactory.createCandlestickChart(
"株価チャート",
"日付",
"金額",
data,
true);
// バイナリ出力ストリームにJPEG形式で画像を出力
response.setContentType("image/jpeg");
ServletOutputStream sos = response.getOutputStream();
ChartUtils.writeChartAsJPEG(sos,chart,1280,600);
%>
ローソク足(CandlestickChart)を使う。
お世辞にも見やすいとは言えない。
引数が double[] というのは余りにセンスが無い。
出来高 まで引数にしているので、もともと株価を表示する為のものだろう。
株価のデータは証券会社で容易に入手できるし、項目もそのまま使えると思う。
データベース接続部分は、include_db.jsp で設定している。
自分の環境に合わせて用意する。
表示の調整
修正したい箇所としては、
- 日付の表示形式を変えたい
- 株価の下限はもっと上で良い
- 土日祝日を省きたい
- 出来高に目盛りが無い(また高さを低くしたい)
図を構成するオブジェクトを修正すれば良い。
ただ図の要素とオブジェクトの関係が解りにくい。
1.の日付の表示形式は、SimpleDateFormat が使える。
XYPlot plot = chart.getXYPlot();
DateAxis axis = (DateAxis)plot.getDomainAxis();
axis.setDateFormatOverride(new SimpleDateFormat("YYYY/MM/dd"));
2.は、目盛の下限を設定すれば良い。
NumberAxis axisY = (NumberAxis)plot.getRangeAxis();
axisY.setLowerBound(20000);
3.は出来ない。
以前は、SegmentedTimeline があったが、実装が難しく不正確で削除されたようだ。
4.はそもそも、折れ線(ローソク線)と棒グラフを同じ筆(Renderer)で描いているのが悪い。
別々に描けば済む。
ついでに、移動平均も情報提供されていたので、これも加えておこう。
なんか、それらしくなったのではないか。
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ page import="java.io.*"%>
<%@ page import="java.util.ArrayList"%>
<%@ page import="java.util.Calendar"%>
<%@ page import="java.text.SimpleDateFormat"%>
<%@ page import="org.jfree.chart.*"%>
<%@ page import="org.jfree.data.xy.*"%>
<%@ page import="org.jfree.chart.plot.*"%>
<%@ page import="org.jfree.chart.axis.*"%>
<%@ page import="org.jfree.chart.renderer.xy.*"%>
<%@ page import="org.jfree.data.time.*"%>
<%@ include file="include_db.jsp" %>
<%
// 株価グラフ
// 1.5.x では無いとラベルが文字化けする
ChartFactory.setChartTheme(StandardChartTheme.createLegacyTheme());
// ローソク足のデータを用意
// データベース接続
Connection conn = ds.getConnection();
String where = " where 日付 between '2023-11-01' and '2024-05-31' and 銘柄コード = '0000'";
String sql = "select count(*) from kabuka2 " + where;
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery( sql );
rs.next();
int size = rs.getInt(1);
rs.close();
Date[] aryDate = new Date[ size ];
double[][] aryValues = new double[5][ size ];
// 出来高用のデータの作成
// TimeSeriesCollection の作成
TimeSeriesCollection data2 = new TimeSeriesCollection();
TimeSeries series21 = new TimeSeries("出来高");
TimeSeriesCollection data3 = new TimeSeriesCollection();
TimeSeries series31 = new TimeSeries("5日平均");
TimeSeries series32 = new TimeSeries("25日平均");
TimeSeries series33 = new TimeSeries("75日平均");
sql = "select 日付 ,高値 ,安値, 始値, 終値, 出来高, 終値5日平均, 終値25日平均, 終値75日平均 "
+ " from kabuka2" + where + " order by 日付";
rs = stmt.executeQuery( sql );
int i = 0;
while( rs.next() ) {
Date date = rs.getDate(1);
aryDate[i] = date;
aryValues[0][i] = rs.getDouble(2);
aryValues[1][i] = rs.getDouble(3);
aryValues[2][i] = rs.getDouble(4);
aryValues[3][i] = rs.getDouble(5);
aryValues[4][i] = rs.getDouble(6);
Calendar cal = Calendar.getInstance();
cal.setTime(date);
series21.add( new Day(cal.get(Calendar.DATE),cal.get(Calendar.MONTH)+1,cal.get(Calendar.YEAR)), rs.getDouble(6) ); // 出来高
series31.add( new Day(cal.get(Calendar.DATE),cal.get(Calendar.MONTH)+1,cal.get(Calendar.YEAR)), rs.getDouble(7) ); // 5日平均
series32.add( new Day(cal.get(Calendar.DATE),cal.get(Calendar.MONTH)+1,cal.get(Calendar.YEAR)), rs.getDouble(8) ); // 25日平均
series33.add( new Day(cal.get(Calendar.DATE),cal.get(Calendar.MONTH)+1,cal.get(Calendar.YEAR)), rs.getDouble(9) ); // 75日平均
i++;
}
data2.addSeries(series21);
data3.addSeries(series31);
data3.addSeries(series32);
data3.addSeries(series33);
conn.close();
// ローソク足用データセットの作成
DefaultHighLowDataset data = new DefaultHighLowDataset(
"日経平均",
aryDate,
aryValues[0], // 高値(High)
aryValues[1], // 安値(Low)
aryValues[2], // 始値(Open)
aryValues[3], // 終値(Close)
aryValues[4] // 出来高(Volume)
);
// ローソク足チャート
JFreeChart chart = ChartFactory.createCandlestickChart(
"株価チャート",
"日付",
"株価(円)",
data,
true);
// 見栄えの調整
// 日付の表示形式
XYPlot plot = chart.getXYPlot();
DateAxis axisX = (DateAxis)plot.getDomainAxis();
axisX.setDateFormatOverride(new SimpleDateFormat("YYYY/MM/dd"));
// 縦目盛りの下限を設定
NumberAxis axisY = (NumberAxis)plot.getRangeAxis();
axisY.setLowerBound(20000);
// 土・日を表示しない 正しく機能しない(1.5.x で削除された)
// ((DateAxis) plot.getDomainAxis()).setTimeline(SegmentedTimeline.newMondayThroughFridayTimeline());
// ローソク足の Volume を表示しない
((CandlestickRenderer) plot.getRenderer()).setDrawVolume(false);
// 棒グラフを追加
XYBarRenderer renderer2 = new XYBarRenderer();
plot.setRenderer(1, renderer2); // 1=dataset index(0~)
plot.setDataset(1, data2 );
ValueAxis axisY2 = new NumberAxis("出来高(万株)");
plot.setRangeAxis(1,axisY2); // 1=axis index(0~)
plot.mapDatasetToRangeAxis(1, 1); // (dataset index, axis index)
// 上限変更
axisY2.setUpperBound(1000000);
// 移動平均線
StandardXYItemRenderer renderer3 = new StandardXYItemRenderer(); // 折れ線
plot.setDataset(2, data3 );
plot.setRenderer(2, renderer3);
// plot.mapDatasetToRangeAxis(2, 0); // default
// バイナリ出力ストリームにJPEG形式で画像を出力
response.setContentType("image/jpeg");
ServletOutputStream objSos = response.getOutputStream();
// ChartUtilities.writeChartAsJPEG(objSos,chart,1280,600); // for ver 1.0.x
ChartUtils.writeChartAsJPEG(objSos,chart,1280,600);
%>
グラフの要素とオブジェクトの関係
図で示さないと解りにくい
各要素を整理すると次のような関係になっている。
基本チャートを作成した時点で index 0 の要素が整う。
矢印のデータセットと軸の紐づけは、
plot.mapDatasetToRangeAxis(1, 1); // (dataset index, axis index)
で行っている。データセットはデフォルトで、axis index 0 に紐づく。
前に、カテゴリ型 と XY型 があると書いたが、異なる型のデータセット・Renderer は組み合わせる事が出来ないので注意して欲しい。
失敗例
資産推移は折れ線ではなく塗りつぶしたかったので、当初カテゴリ型に属するエリアグラフを使った。
株式市場は開いていない日もあるので、この選択は自然に思える。
// エリアグラフを生成
JFreeChart chart = ChartFactory.createAreaChart(
"株式資産推移", // グラフのタイトル
"日付", // x軸タイトル
"評価額", // y軸タイトル
data,
PlotOrientation.VERTICAL,
true,
false,
false);
横軸として日付を使うが、期間が短ければ良いが、この例のように年単位になると日付が読めない。
そう、目盛りの表示項目の間引きが出来ない のだ。
(Excelだと何の問題もないんですがね)
角度を付けて縦書きにする事も可能だが、数か月が限界になる。
カテゴリ型=横軸は連続性の無い項目 という事で変更できる項目に違いがある。
資産推移グラフ
実は、株価チャートの前に完成していたので、おまけと思って欲しい。
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ page import="java.net.*"%>
<%@ page import="java.io.*"%>
<%@ page import="java.util.Calendar"%>
<%@ page import="java.awt.Color"%>
<%@ page import="java.text.SimpleDateFormat"%>
<%@ page import="org.jfree.chart.*"%>
<%@ page import="org.jfree.data.general.DefaultPieDataset"%>
<%@ page import="org.jfree.data.category.*"%>
<%@ page import="org.jfree.chart.plot.*"%>
<%@ page import="org.jfree.chart.axis.*"%>
<%@ page import="org.jfree.chart.renderer.category.*"%>
<%@ page import="org.jfree.data.time.*"%>
<%@ page import="org.jfree.chart.renderer.xy.*"%>
<%@ page import="org.jfree.chart.ChartColor"%>
<%@ include file="include_db.jsp" %>
<%@ include file="utils.jsp" %>
<%
// 資産推移グラフ(時系列)
String fromDate = "2023-01-01";
String toDate = "2024-11-22";
Connection conn = ds.getConnection();
// 1.5.x だと設定しないとラベルが文字化けする
ChartFactory.setChartTheme(StandardChartTheme.createLegacyTheme());
// 時系列グラフのデータセットを用意する
TimeSeriesCollection data = new TimeSeriesCollection();
// TimeSeriesCollection は複数の線要素(series)から成る
TimeSeries series1 = new TimeSeries("資産評価額");
TimeSeries series2 = new TimeSeries("現在換算評価");
// (1) 資産評価額
String where = " where 日付 between '" + fromDate + "' and '" + toDate + "'";
String sql = "select 日付,sum(株数*終値) from sisan_p " + where + " group by 日付 order by 日付";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery( sql );
double upperLeft = 0l;
double upperRight = 0l;
double lowerLeft = Long.MAX_VALUE;
double lowerRight = Long.MAX_VALUE;
while( rs.next() ) {
Date date = rs.getDate(1);
double gaku = rs.getDouble(2);
Calendar cal = Calendar.getInstance();
cal.setTime(date);
series1.add( new Day(cal.get(Calendar.DATE) ,cal.get(Calendar.MONTH)+1 ,cal.get(Calendar.YEAR)), gaku);
lowerLeft = Math.min(lowerLeft,gaku);
upperLeft = Math.max(upperLeft,gaku);
}
rs.close();
data.addSeries(series1);
// (2) 現在換算評価
sql = "select 日付,sum(株数*"
+ "(select 終値 from sisan_p x where x.日付 = '" + toDate + "' and x.銘柄コード = a.銘柄コード)"
+ "*(select coalesce(分割前倍率,1) from kabuka2 y where 日付 = a.日付 and y.銘柄コード = a.銘柄コード)"
+ ") from sisan_p a " + where + " group by 日付 order by 日付";
stmt = conn.createStatement();
rs = stmt.executeQuery( sql );
while( rs.next() ) {
Date date = rs.getDate(1);
double gaku = rs.getDouble(2);
Calendar cal = Calendar.getInstance();
cal.setTime(date);
series2.add( new Day(cal.get(Calendar.DATE) ,cal.get(Calendar.MONTH)+1 ,cal.get(Calendar.YEAR)), gaku);
lowerLeft = Math.min(lowerLeft,gaku);
upperLeft = Math.max(upperLeft,gaku);
}
rs.close();
data.addSeries(series2);
// 日経平均用の折れ線グラフのデータセットを用意する
TimeSeriesCollection data2 = new TimeSeriesCollection();
TimeSeries series3 = new TimeSeries("日経平均" );
// (3) 日経平均
sql = "select 日付, 終値 from kabuka2 "
+ where + " and 銘柄コード = '0000' order by 日付";
rs = stmt.executeQuery( sql );
while( rs.next() ) {
Date date = rs.getDate(1);
double gaku = rs.getDouble(2);
Calendar cal = Calendar.getInstance();
cal.setTime(date);
series3.add( new Day(cal.get(Calendar.DATE) ,cal.get(Calendar.MONTH)+1 ,cal.get(Calendar.YEAR)) ,gaku);
lowerRight = Math.min(lowerRight,gaku);
upperRight = Math.max(upperRight,gaku);
}
rs.close();
data2.addSeries(series3);
conn.close();
// 基本となるチャートを生成(時系列)
// 注)createXYAreaChart は時系列ではない
JFreeChart chart = ChartFactory.createTimeSeriesChart(
"株式資産推移", // グラフのタイトル
"日付", // x軸タイトル
"評価額", // y軸タイトル
data, // TimeSeriesCollection
true, // 凡例(Legend)
false, // tooltips
false); // urls
// Plot を取得(時系列は XYPlot)
XYPlot plot = chart.getXYPlot();
// 折れ線用の2つ目のデータセットを設定(追加)
plot.setDataset(1, data2); // (dataset index 0~, Dataset)
// 折れ線用の軸を設定(追加)
plot.setRangeAxis(1, new NumberAxis("日経平均株価")); // (axis index 0~, Axis)
// データセットと紐づけ
plot.mapDatasetToRangeAxis(1, 1); // (dataset index, axis index)
// Renderer の設定
// デフォルトが XYLineAndShapeRenderer(点付き折れ線)なので入れ替える
XYItemRenderer renderer1 = new XYAreaRenderer(); // 塗りつぶし線
// さらに折れ線用 Renderer
StandardXYItemRenderer renderer2 = new StandardXYItemRenderer();
// 線の色
// シリーズ"0"に色(RGBA)を指定
renderer1.setSeriesPaint(0, new Color( 0, 0, 255, 128)); // BLUE/半透明
renderer1.setSeriesPaint(1, new Color( 0, 255, 0, 128)); // GREEN/半透明
renderer2.setSeriesPaint(0, ChartColor.RED);
plot.setRenderer(0, renderer1);
plot.setRenderer(1, renderer2);
// 【目盛】Axis
// 時系列では目盛りの表示フォーマットが変更できる
// x軸(Domain)
DateAxis axis = (DateAxis)plot.getDomainAxis();
axis.setDateFormatOverride(new SimpleDateFormat("YYYY/MM/dd"));
// XY型だとy軸の目盛の表示範囲が自動で設定されるので必要なら修正する
// y軸(Range)
NumberAxis axisyLeft = (NumberAxis)plot.getRangeAxis();
axisyLeft.setLowerBound(getLowerBound(upperLeft,lowerLeft ));
NumberAxis axisyRight = (NumberAxis)plot.getRangeAxis(1);
axisyRight.setLowerBound(getLowerBound(upperRight, lowerRight));
// バイナリ出力ストリームにJPEG形式で画像を出力
response.setContentType("image/jpeg");
ServletOutputStream sos = response.getOutputStream();
// ChartUtilities.writeChartAsJPEG(sos,chart,1280,600);
ChartUtils.writeChartAsJPEG(sos,chart,1280,600);
%>
少し補足しておくと、ChartFactory に該当する基本チャートは無い。
XYAreaRenderer というのが目的に叶うもので、基本の Renderer を置き換えている。
これは、色に透明度を設定できるので、重なってもそれなりに見えるところが良い。
getLowerBound() は utils.jsp 内にメソッドとして定義している。
目盛りの下限を自動で設定したかったもので、好みで修正して欲しい。
グラフは、このように、結構簡単に書く事ができる。
むしろ、データを揃えるのが一番大変なところでもある。
データベースの説明
念のため、ここで使ったデータベースの構造を記載しておく。
sisan_p は複数のテーブルから成るVIEWの実体(VIEWでは遅い)。
ここではその諸元については詳しく記載しないが、
契約している証券会社から、取引履歴、損益履歴、株価情報 を取得して加工している。
難しいのは、取得単価 の計算である。
「特定口座」で、特に同日に売買があると計算方法が証券会社によって多少違う場合があるので注意が必要。
期首の株数やその取得単価、株式分割 や他証券会社間の 移管 が記載されていない場合があるので自分で補正する必要もある。
kabuka で 分割倍率 という項目を独自に追加している。これが無いと過去の株数で現在の評価値を計算することが出来ない。
おわりに
カテゴリ型のラベル表示の間隔が調整できればもっと良いと思う。
1回限りならExceが一番良い。
継続して行いたい場合はこういった方法もあるという紹介でした。