com.d3x.morpheus.viz.chart.ChartFactory Maven / Gradle / Ivy
The newest version!
/*
* 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.chart;
import java.awt.*;
import java.util.Iterator;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.apache.commons.math3.special.Erf;
import com.d3x.morpheus.array.Array;
import com.d3x.morpheus.frame.DataFrameLeastSquares;
import com.d3x.morpheus.util.Bounds;
import com.d3x.morpheus.viz.chart.pie.PiePlot;
import com.d3x.morpheus.viz.chart.xy.XyPlot;
import com.d3x.morpheus.frame.DataFrame;
/**
* The factory interface for creating various types of Charts using the Morpheus Charting API
*
* @author Xavier Witdouck
*
* This is open source software released under the Apache 2.0 License
*/
public interface ChartFactory {
/**
* Returns true if this factory supports the chart type specified
* @param chart the chart instance
* @return true if chart is supported by this factory
*/
boolean isSupported(Chart> chart);
/**
* Displays the collection of charts in a grid with the number of columns specified
* @param columns the number of columns for chart grid
* @param charts the sequence of charts to plot
*/
void show(int columns, Iterable> charts);
/**
* Displays the collection of charts in a grid with the number of columns specified
* @param columns the number of columns for chart grid
* @param charts the sequence of charts to plot
*/
void show(int columns, Stream> charts);
/**
* Returns Javascript to embed in an HTML page which will plot the charts specified.
* By convention, the user will need to create an html page with div
elements which
* have attributes labelled "chart_N" where N is 0, 1, 2, N. The first chart
* in the arguments will be plotted in div with id=chart_0, the second will be plotted in the
* div with id=chart_1 and so on.
* @param charts the sequence of charts to generate Javascript from
* @return the resulting Javascript to embed in an HTML page.
*/
String javascript(Chart>... charts);
/**
* Returns Javascript to embed in an HTML page which will plot the charts specified.
* By convention, the user will need to create an html page with div
elements which
* have attributes labelled "chart_N" where N is 0, 1, 2, N. The first chart
* in the arguments will be plotted in div with id=chart_0, the second will be plotted in the
* div with id=chart_1 and so on.
* @param charts the sequence of charts to generate Javascript from
* @return the resulting Javascript to embed in an HTML page.
*/
String javascript(Iterable> charts);
/**
* Returns a newly created XY chart and applies it to the configurator provided
* @param configurator the chart configurator
* @param domainType the data type for the domain axis
* @param the domain key type
* @return the newly created chart
*/
Chart> ofXY(Class domainType, Consumer>> configurator);
/**
* Returns a newly created Pie chart and applies it to the configurator provided
* @param is3d true for a 3d plot
* @param configurator the chart configurator
* @param the item key type
* @return the newly created chart
*/
Chart> ofPiePlot(boolean is3d, Consumer>> configurator);
/**
* Returns a newly created Line Chart using the row keys for the domain axis
* @param frame the DataFrame for the chart
* @param configurator the configurator to accept config to the chart
* @return the newly created chart
*/
default Chart> withLinePlot(DataFrame frame, Consumer>> configurator) {
return ofXY(frame.rows().keyClass(), chart -> {
chart.plot().data().add(frame);
chart.plot().render(0).withLines(false, false);
if (configurator != null) {
configurator.accept(chart);
}
});
}
/**
* Returns a newly created Line Chart using a column for the domain axis
* @param frame the DataFrame for the chart
* @param domainKey the column key in the frame that defines the domain
* @param configurator the configurator to accept config to the chart
* @return the newly created chart
*/
@SuppressWarnings("unchecked")
default Chart> withLinePlot(DataFrame,S> frame, S domainKey, Consumer>> configurator) {
return ofXY((Class)frame.cols().type(domainKey), chart -> {
chart.plot().data().add(frame, domainKey);
chart.plot().render(0).withLines(false, false);
if (configurator != null) {
configurator.accept(chart);
}
});
}
/**
* Returns a newly created area chart using the row keys for the domain axis
* @param frame the DataFrame for the chart
* @param stacked true to generate a stacked area plot, false for overlapping
* @param configurator the configurator to accept config to the chart
* @return the newly created chart
*/
default Chart> withAreaPlot(DataFrame frame, boolean stacked, Consumer>> configurator) {
return ofXY(frame.rows().keyClass(), chart -> {
chart.plot().data().add(frame);
chart.plot().render(0).withArea(stacked);
if (configurator != null) {
configurator.accept(chart);
}
});
}
/**
* Returns a newly created area chart using a column for the domain axis
* @param frame the DataFrame for the chart
* @param stacked true to generate a stacked area plot, false for overlapping
* @param domainKey the column key in the frame that defines the domain
* @param configurator the configurator to accept config to the chart
* @return the newly created chart
*/
@SuppressWarnings("unchecked")
default Chart> withAreaPlot(DataFrame, S> frame, boolean stacked, S domainKey, Consumer>> configurator) {
return ofXY((Class)frame.cols().type(domainKey), chart -> {
chart.plot().data().add(frame, domainKey);
chart.plot().render(0).withArea(stacked);
if (configurator != null) {
configurator.accept(chart);
}
});
}
/**
* Returns a newly created scatter chart with shapes using the row keys for the domain axis
* @param frame the DataFrame for the chart
* @param shapes true for series specific shapes
* @param configurator the configurator to accept config to the chart
* @return the newly created chart
*/
default Chart> withScatterPlot(DataFrame frame, boolean shapes, Consumer>> configurator) {
return ofXY(frame.rows().keyClass(), chart -> {
chart.plot().data().add(frame);
if (shapes) {
chart.plot().render(0).withShapes();
} else {
chart.plot().render(0).withDots();
}
if (configurator != null) {
configurator.accept(chart);
}
});
}
/**
* Returns a newly created scatter chart with shapes using a column for the domain axis
* @param frame the DataFrame for the chart
* @param shapes true for series specific shapes
* @param domainKey the column key in the frame that defines the domain
* @param configurator the configurator to accept config to the chart
* @return the newly created chart
*/
@SuppressWarnings("unchecked")
default Chart> withScatterPlot(DataFrame, S> frame, boolean shapes, S domainKey, Consumer>> configurator) {
return ofXY((Class)frame.cols().type(domainKey), chart -> {
chart.plot().data().add(frame, domainKey);
if (shapes) {
chart.plot().render(0).withShapes();
} else {
chart.plot().render(0).withDots();
}
if (configurator != null) {
configurator.accept(chart);
}
});
}
/**
* Returns a newly created Bar Chart using the row keys to build the domain axis
* @param frame the DataFrame for the chart
* @param stacked true to generate a stacked bar plot, false for non-statcked
* @param configurator the configurator to accept config to the chart
* @return the newly created chart
*/
default Chart> withBarPlot(DataFrame frame, boolean stacked, Consumer>> configurator) {
return ofXY(frame.rows().keyClass(), chart -> {
chart.plot().data().add(frame);
chart.plot().render(0).withBars(stacked, 0d);
if (configurator != null) {
configurator.accept(chart);
}
});
}
/**
* Returns a newly created Bar Chart using a column to build the domain axis
* @param frame the DataFrame for the chart
* @param stacked true to generate a stacked bar plot, false for non-statcked
* @param domainKey the column key in the frame that defines the domain
* @param configurator the configurator to accept config to the chart
* @return the newly created chart
*/
@SuppressWarnings("unchecked")
default Chart> withBarPlot(DataFrame, S> frame, boolean stacked, S domainKey, Consumer>> configurator) {
return ofXY((Class)frame.cols().type(domainKey), chart -> {
chart.plot().data().add(frame, domainKey);
chart.plot().render(0).withBars(stacked, 0d);
if (configurator != null) {
configurator.accept(chart);
}
});
}
/**
* Returns a newly created Pie Chart using the row keys for labels and the first numeric column for values
* @param frame the DataFrame for the chart
* @param is3d true for 3D PiePlot
* @param configurator the configurator to accept config to the chart
* @param the frame row axis type
* @param the frame column axis type
* @return the newly created chart
*/
default Chart> withPiePlot(DataFrame frame, boolean is3d, Consumer>> configurator) {
if (frame == null) {
throw new IllegalArgumentException("The DataFrame cannot be null");
} else {
return ofPiePlot(is3d, chart -> {
chart.plot().data().apply(frame);
if (configurator != null) {
configurator.accept(chart);
}
});
}
}
/**
* Returns a newly created Pie Chart using the row keys for labels and the values from the column labelled dataKey
* @param frame the DataFrame containing data
* @param is3d true for 3D PiePlot
* @param dataKey the column key to use for data values
* @param configurator the configurator to accept config to the chart
* @param the frame row axis type
* @param the frame column axis type
* @return the newly created chart
*/
default Chart> withPiePlot(DataFrame frame, boolean is3d, S dataKey, Consumer>> configurator) {
if (frame == null) {
throw new IllegalArgumentException("The DataFrame cannot be null");
} else {
return ofPiePlot(is3d, chart -> {
chart.plot().data().apply(frame, dataKey);
if (configurator != null) {
configurator.accept(chart);
}
});
}
}
/**
* Returns a newly created Pie Chart using labels from the column identified by labelKey and the values from the column labelled dataKey
* @param frame the DataFrame containing data
* @param is3d true for 3D PiePlot
* @param dataKey the column key to use for data values
* @param labelKey the column key to use for labels
* @param configurator the configurator to accept config to the chart
* @param the frame row axis type
* @param the frame column axis type
* @return the newly created chart
*/
default Chart> withPiePlot(DataFrame,S> frame, boolean is3d, S dataKey, S labelKey, Consumer>> configurator) {
if (frame == null) {
throw new IllegalArgumentException("The DataFrame cannot be null");
} else {
return ofPiePlot(is3d, chart -> {
chart.plot().data().apply(frame, dataKey, labelKey);
if (configurator != null) {
configurator.accept(chart);
}
});
}
}
/**
* Returns a Histogram Bar Chart of the frequency distribution for a all columns in a DataFrame
* @param frame the data from which to generate a histogram for each column
* @param binCount the number of bins to include in the histogram
* @param configurator the optional consumer to configure the chart
* @param the series key type for frame
* @return the newly created chart
*/
@SuppressWarnings("unchecked")
default Chart> withHistPlot(DataFrame frame, int binCount, Consumer>> configurator) {
if (frame == null) {
throw new IllegalArgumentException("The DataFrame cannot be null");
} else if (frame.colCount() < 1) {
throw new ChartException("The histogram data frame should contain at least one 1 column with frequency values");
} else if (frame.rowCount() < 2) {
throw new ChartException("The histogram data frame should have at least 2 rows");
} else {
final Iterator colKeyIterator = frame.cols().keys().iterator();
final DataFrame hist0 = frame.cols().hist(binCount, colKeyIterator.next());
final double stepSize0 = hist0.rows().key(1) - hist0.rows().key(0);
return withBarPlot(hist0, false, chart -> {
chart.plot().data().at(0).withLowerDomainInterval(v -> v + stepSize0);
chart.plot().axes().range(0).label().withText("Frequency");
chart.plot().axes().domain().label().withText("Values");
while (colKeyIterator.hasNext()) {
final DataFrame histN = frame.cols().hist(binCount, colKeyIterator.next());
final double stepSizeN = histN.rows().key(1) - histN.rows().key(0);
final int index = chart.plot().data().add(histN);
chart.plot().data().at(index).withLowerDomainInterval(v -> v + stepSizeN);
chart.plot().render(index).withBars(false, 0d);
}
if (configurator != null) {
configurator.accept(chart);
}
});
}
}
/**
* Returns a Chart to plot a histogram based on the column data in the frame provided
* @param frame the data from which to generate a histogram for each column
* @param binCount the number of bins to include in the histogram
* @param sharedBins if true and the frame has multiple columns, each series will share the same width bin rather than being computed independently
* @param configurator the optional consumer to configure the chart
* @param the column key type for frame
* @return the newly created chart
*/
@SuppressWarnings("unchecked")
default Chart> withHistPlot(DataFrame frame, int binCount, boolean sharedBins, Consumer>> configurator) {
if (frame.colCount() < 1) {
throw new ChartException("The histogram data frame should contain at least one 1 column with frequency values");
} else if (frame.rowCount() < 2) {
throw new ChartException("The histogram data frame should have at least 2 rows");
} else if (!sharedBins) {
return withHistPlot(frame, binCount, configurator);
} else {
final DataFrame hist = frame.cols().hist(binCount);
final double first = hist.rows().key(0);
final double second = hist.rows().key(1);
final double stepSize = second - first;
return withBarPlot(hist, false, chart -> {
chart.title().withText("Histogram");
chart.title().withFont(new Font("Arial", Font.PLAIN, 16));
chart.plot().data().at(0).withLowerDomainInterval(v -> v + stepSize);
chart.plot().axes().range(0).label().withText("Frequency");
chart.plot().axes().domain().label().withText("Values");
if (configurator != null) {
configurator.accept(chart);
}
});
}
}
/**
* Returns a Histogram Bar Chart of the frequency distribution for a specific column in a DataFrame
* @param frame the DataFrame of data to generate a histogram for
* @param binCount the number of bins to include in the histogram
* @param columnKey the key of the column to generate the histogram for
* @param configurator the optional consumer to configure the chart
* @param the column key type for frame
* @return the newly created chart
*/
@SuppressWarnings("unchecked")
default Chart> withHistPlot(DataFrame frame, int binCount, C columnKey, Consumer>> configurator) {
if (frame == null) {
throw new IllegalArgumentException("The DataFrame cannot be null");
} else if (frame.colCount() < 1) {
throw new ChartException("The histogram data frame should contain at least one 1 column with frequency values");
} else if (frame.rowCount() < 2) {
throw new ChartException("The histogram data frame should have at least 2 rows");
} else {
final DataFrame series = frame.cols().select(columnKey);
return withHistPlot(series, binCount, configurator);
}
}
/**
* Generates a plot of the Autocorrelation function (ACF) given the Least Squares model
* @param model the least squares model
* @param maxLags the max lags for ACF plot
* @param alpha the significance level for confidence intervals (e.g. 0.05 implies 5% level, or 95% confidence interval)
* @param consumer the consumer to configure additional options on the chart
* @param the row key type
* @param the column key type
* @return the resulting chart object
*/
default Chart> withAcf(DataFrameLeastSquares model, int maxLags, double alpha, Consumer>> consumer) {
final DataFrame acf = model.getResidualsAcf(maxLags);
final Array bounds = Array.ofObjects("Upper", "Lower");
final Array lags = acf.rows().keyArray();
final double erfInv = Math.sqrt(2d) * Erf.erfInv(1d - alpha);
final double upper = 1d * erfInv / Math.sqrt(maxLags);
final double lower = -1d * erfInv / Math.sqrt(maxLags);
final int maxLag = acf.rows().lastKey().orElseThrow(() -> new RuntimeException("No data in autocorrelation matrix"));
final DataFrame boundsFrame = DataFrame.ofDoubles(lags, bounds, v -> v.colOrdinal() == 0 ? upper : lower);
return Chart.create().withBarPlot(acf, false, chart -> {
chart.title().withText("Autocorrelation Function (ACF)");
chart.title().withFont(new Font("Arial", Font.BOLD, 16));
chart.plot().data().add(boundsFrame);
chart.plot().render(1).withLines(false, true);
chart.plot().data().at(0).withLowerDomainInterval(v -> v + 1);
chart.plot().axes().domain().label().withText("Lag");
chart.plot().axes().range(0).label().withText("Autocorrelation");
chart.plot().axes().domain().withRange(Bounds.of(-1, (double)maxLag));
chart.plot().style("Upper").withColor(Color.BLUE).withDashes(true).withLineWidth(1f);
chart.plot().style("Lower").withColor(Color.BLUE).withDashes(true).withLineWidth(1f);
if (consumer != null) {
consumer.accept(chart);
}
});
}
/**
* Returns a chart that plots regression residuals against the fitted values in a regression
* @param model the least squares model
* @param consumer the user provide chart configurator
* @param the row key type
* @param the column key type
* @return the resulting chart
*/
default Chart> withResidualsVsFitted(DataFrameLeastSquares model, Consumer>> consumer) {
var residuals = model.getResiduals();
var fittedValues = model.getFittedValues();
var zeroLine = fittedValues.copy();
zeroLine.cols().add("Zero", Double.class, v -> 0d);
final DataFrame combined = DataFrame.concatColumns(residuals, fittedValues);
return Chart.create().withLinePlot(combined, "Fitted", chart -> {
chart.title().withText("Least Squares Residuals vs Fitted Values");
chart.title().withFont(new Font("Arial", Font.BOLD, 15));
chart.plot().data().add(zeroLine, "Fitted");
chart.plot().render(0).withDots();
chart.plot().render(1).withLines(false, false);
chart.plot().style("Zero").withColor(Color.BLACK).withLineWidth(2f);
chart.plot().style("Residuals").withColor(Color.RED).withPointsVisible(true);
chart.plot().axes().domain().label().withText("Fitted Values");
chart.plot().axes().domain().format().withPattern("0.00;-0.00");
chart.plot().axes().range(0).label().withText("Residuals");
chart.plot().axes().range(0).format().withPattern("0.00;-0.00");
chart.legend().on().bottom();
if (consumer != null) {
consumer.accept(chart);
}
});
}
}