com.d3x.morpheus.viz.jfree.JFXyPlot Maven / Gradle / Ivy
/*
* Copyright (C) 2014-2018 D3X Systems - All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.d3x.morpheus.viz.jfree;
import java.awt.*;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import org.jfree.chart.axis.Axis;
import org.jfree.chart.axis.AxisLocation;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.DatasetRenderingOrder;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.data.xy.XYDataset;
import com.d3x.morpheus.frame.DataFrame;
import com.d3x.morpheus.viz.chart.ChartException;
import com.d3x.morpheus.viz.chart.xy.XyAxes;
import com.d3x.morpheus.viz.chart.xy.XyDataset;
import com.d3x.morpheus.viz.chart.xy.XyModel;
import com.d3x.morpheus.viz.chart.xy.XyOrient;
import com.d3x.morpheus.viz.chart.xy.XyPlotBase;
import com.d3x.morpheus.viz.chart.xy.XyRender;
import com.d3x.morpheus.viz.chart.xy.XyTrend;
import com.d3x.morpheus.viz.chart.xy.XyTrendBase;
import com.d3x.morpheus.viz.html.HtmlCode;
/**
* The plot implementation for JFreeChart xy plots.
*
* @author Xavier Witdouck
*
* This is open source software released under the Apache 2.0 License
*/
class JFXyPlot extends XyPlotBase {
private XYPlot plot;
private Map trendMap = new HashMap<>();
private Map> datasetMap = new LinkedHashMap<>();
private DecimalFormat decimalFormat = new DecimalFormat("###,##0.####;-###,##0.####");
/**
* Constructor
* @param domainAxis the domain axis
* @param rangeAxis the range axis
*/
JFXyPlot(ValueAxis domainAxis, ValueAxis rangeAxis) {
this.plot = new XYPlot(null, domainAxis, rangeAxis, null);
this.plot.getRangeAxis().setAutoRange(true);
this.plot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_LEFT);
this.plot.setDomainGridlinesVisible(true);
this.plot.setRangeGridlinesVisible(true);
this.plot.setDomainGridlinePaint(Color.DARK_GRAY);
this.plot.setRangeGridlinePaint(Color.DARK_GRAY);
this.plot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD);
if (rangeAxis instanceof NumberAxis) ((NumberAxis)rangeAxis).setAutoRangeIncludesZero(false);
if (domainAxis instanceof NumberAxis) ((NumberAxis)domainAxis).setAutoRangeIncludesZero(false);
}
/**
* Returns the underlying JFreeChart plot object
* @return the underlying plot object
*/
final XYPlot underlying() {
return plot;
}
@Override
public XyAxes axes() {
return new JFXyAxes(plot);
}
@Override
public XyModel data() {
return new ModelAdapter<>();
}
@Override
public XyOrient orient() {
return new OrientAdapter();
}
@Override
@SuppressWarnings("unchecked")
public XyTrend trend(S seriesKey) {
TrendLine trend = trendMap.get(seriesKey);
if (trend == null) {
final ModelAdapter modelAdapter = new ModelAdapter<>();
final Stream> datasets = modelAdapter.getModels().map(m -> (JFXyDataset)m);
final Optional> datasetOpt = datasets.filter(m -> m.contains(seriesKey)).findFirst();
if (!datasetOpt.isPresent()) {
throw new ChartException("No chart data could be located for series: " + seriesKey);
} else {
final JFXyDataset dataset = datasetOpt.get();
trend = new TrendLine(dataset, seriesKey);
trendMap.put(seriesKey, trend);
applyTrend(dataset, trend);
}
}
return trend;
}
@Override
public XyRender render(int index) {
return new JFXyRender(this, index);
}
/**
* Returns the number of none null data sets for this plot
* @return the data set count for plot
*/
private int getDatasetCount() {
int count = 0;
for (int i=0; i dataset, TrendLine trend) {
try {
this.render(trend.datasetIndex).withLines(false, false);
this.style(trend.trendKey).withColor(trend.lineColor);
this.style(trend.trendKey).withLineWidth(trend.lineWidth);
this.plot.getRenderer(trend.datasetIndex).setBaseSeriesVisibleInLegend(false);
this.plot.getRenderer(trend.datasetIndex).setBaseToolTipGenerator(this::getTrendTooltip);
this.plot.setDataset(trend.datasetIndex, JFXyDataset.of(() -> {
final Comparable seriesKey = trend.seriesKey();
return trend.createTrendData(dataset, seriesKey, trend.trendKey);
}));
} catch (Exception ex) {
ex.printStackTrace();
}
}
/**
* Returns a tooltip to display details for an XY point
* @param dataset the dataset reference
* @param series the series index
* @param item the item index
* @return the tooltip, which can be null
*/
String getXyTooltip(XYDataset dataset, int series, int item) {
try {
final Comparable seriesKey = dataset.getSeriesKey(series);
final double x = dataset.getXValue(series, item);
final double y = dataset.getYValue(series, item);
if (plot.getDomainAxis() instanceof DateAxis) {
final DateFormat dateFormat = ((DateAxis)plot.getDomainAxis()).getDateFormatOverride();
final DateFormat formatter = dateFormat != null ? dateFormat : new SimpleDateFormat("dd-MMM-yyyy HH:mm");
final String xLabel = formatter.format(new Date((long)x));
final String yLabel = decimalFormat.format(y);
return HtmlCode.createHtml(writer -> {
writer.newElement("html", html -> {
html.newElement("h2", h2 -> h2.text(seriesKey.toString()));
html.newElement("h3", h3 -> h3.text(String.format("X = %s, Y = %s", xLabel, yLabel)));
});
});
} else {
final String xLabel = decimalFormat.format(x);
final String yLabel = decimalFormat.format(y);
return HtmlCode.createHtml(writer -> {
writer.newElement("html", html -> {
html.newElement("h2", h2 -> h2.text(seriesKey.toString()));
html.newElement("h3", h3 -> h3.text(String.format("X = %s, Y = %s", xLabel, yLabel)));
});
});
}
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
/**
* Returns a tooltip to display a trend line equation
* @param dataset the dataset reference
* @param series the series index
* @param item the item index
* @return the tooltip
*/
private String getTrendTooltip(XYDataset dataset, int series, int item) {
try {
final Comparable seriesKey = dataset.getSeriesKey(series).toString().replace(" (trend)", "");
final TrendLine trend = trendMap.get(seriesKey);
if (trend == null) {
return null;
} else {
final String slope = decimalFormat.format(trend.slope());
final String intercept = decimalFormat.format(trend.intercept());
final String equation = String.format("Y = %s * X + %s", slope, intercept);
return HtmlCode.createHtml(writer -> {
writer.newElement("html", html -> {
html.newElement("h2", h2 -> h2.text(seriesKey.toString()));
html.newElement("h3", h3 -> h3.text(equation));
});
});
}
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
/**
* An adapter implementation for the ChartOrientation interface
*/
private class OrientAdapter implements XyOrient {
@Override
public void vertical() {
plot.setOrientation(PlotOrientation.VERTICAL);
}
@Override
public void horizontal() {
plot.setOrientation(PlotOrientation.HORIZONTAL);
}
}
/**
* A XyTrend adapter for the JFreeChart library
*/
private class TrendLine extends XyTrendBase {
private Color lineColor;
private float lineWidth;
private int datasetIndex;
private Comparable trendKey;
/**
* Constructor
* @param seriesKey the series key
*/
@SuppressWarnings("unchecked")
TrendLine(JFXyDataset source, Comparable seriesKey) {
super(seriesKey);
this.lineWidth = 2f;
this.lineColor = Color.BLACK;
this.datasetIndex = getDatasetCount();
this.trendKey = String.format("%s (trend)", seriesKey);
}
@Override
public XyTrend withColor(Color color) {
this.lineColor = color;
JFXyPlot.this.style(trendKey).withColor(color);
return this;
}
@Override
public XyTrend withLineWidth(float width) {
this.lineWidth = width;
JFXyPlot.this.style(trendKey).withLineWidth(width);
return this;
}
@Override
public XyTrend clear() {
final Comparable seriesKey = seriesKey();
plot.setDataset(datasetIndex, null);
trendMap.remove(seriesKey);
return this;
}
}
/**
* An implementation of the XyModel interface that manages data for this plot
*/
private class ModelAdapter implements XyModel {
/**
* Constructor
*/
private ModelAdapter() {
super();
}
/**
* Returns a stream of the models contained in this adapter
* @return the stream of models
*/
private Stream> getModels() {
return datasetMap.values().stream();
}
@Override
public Class domainType() {
if (datasetMap.isEmpty()) {
return null;
} else {
return datasetMap.entrySet().iterator().next().getValue().domainType();
}
}
@Override
@SuppressWarnings("unchecked")
public XyDataset at(int index) {
final JFXyDataset dataset = datasetMap.get(index);
if (dataset != null) {
return (XyDataset)dataset;
} else {
throw new IllegalArgumentException("No chart data located at index: " + index);
}
}
@Override
public void setRangeAxis(int dataset, int axis) {
final Axis rangeAxis = plot.getRangeAxis(axis);
if (rangeAxis == null) {
plot.setRangeAxis(axis, new NumberAxis());
plot.mapDatasetToRangeAxis(dataset, axis);
} else {
plot.mapDatasetToRangeAxis(dataset, axis);
}
}
@Override
@SuppressWarnings("unchecked")
public int add(DataFrame frame) {
final int index = getDatasetCount();
final JFXyDataset dataset = JFXyDataset.of(() -> frame);
datasetMap.put(index, dataset);
plot.setDataset(index, dataset);
render(index).withLines(false, false);
return index;
}
@Override
@SuppressWarnings("unchecked")
public int add(DataFrame,S> frame, S domainKey) {
final int index = getDatasetCount();
final JFXyDataset dataset = JFXyDataset.of(domainKey, () -> frame);
datasetMap.put(index, dataset);
plot.setDataset(index, dataset);
render(index).withLines(false, false);
return index;
}
@Override
@SuppressWarnings("unchecked")
public XyDataset update(int index, DataFrame frame) {
final JFXyDataset dataset = JFXyDataset.of(() -> frame);
datasetMap.put(index, dataset);
plot.setDataset(index, dataset);
return dataset;
}
@Override
@SuppressWarnings("unchecked")
public XyDataset update(int index, DataFrame,S> frame, S domainKey) {
final JFXyDataset dataset = JFXyDataset.of(domainKey, () -> frame);
datasetMap.put(index, dataset);
plot.setDataset(index, dataset);
return dataset;
}
@Override
public void remove(int index) {
final JFXyDataset dataset = datasetMap.remove(index);
if (dataset == null) {
throw new ChartException("No chart dataset exists for id: " + index);
} else {
plot.setDataset(index, null);
}
}
@Override
public void removeAll() {
final int count = plot.getDatasetCount();
datasetMap.clear();
for (int i=0; i