All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.deephaven.plot.datasets.multiseries.AbstractMultiSeries Maven / Gradle / Ivy

There is a newer version: 0.36.1
Show newest version
//
// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
//
package io.deephaven.plot.datasets.multiseries;

import io.deephaven.base.verify.Assert;
import io.deephaven.engine.table.PartitionedTable;
import io.deephaven.io.logger.Logger;
import io.deephaven.plot.*;
import io.deephaven.plot.datasets.ColumnNameConstants;
import io.deephaven.plot.datasets.DataSeriesInternal;
import io.deephaven.plot.datasets.DynamicSeriesNamer;
import io.deephaven.plot.errors.PlotIllegalStateException;
import io.deephaven.plot.errors.PlotRuntimeException;
import io.deephaven.plot.errors.PlotUnsupportedOperationException;
import io.deephaven.plot.util.ArgumentValidations;
import io.deephaven.plot.util.functions.ClosureFunction;
import io.deephaven.engine.table.impl.*;
import groovy.lang.Closure;

import io.deephaven.internal.log.LoggerFactory;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Function;

import static io.deephaven.engine.util.TableTools.emptyTable;

/**
 * Creates and holds a {@link DataSeriesInternal} for every key in a {@link PartitionedTable}.
 */
@SuppressWarnings("SynchronizeOnNonFinalField")
public abstract class AbstractMultiSeries extends AbstractSeriesInternal
        implements MultiSeriesInternal, TableSnapshotSeries {
    private static final long serialVersionUID = 3548896765688007362L;
    private static final Logger log = LoggerFactory.getLogger(AbstractMultiSeries.class);

    protected static final PartitionedTable EMPTY_PARTITIONED_TABLE = emptyTable(0).partitionBy();
    protected final String[] byColumns;

    protected transient Object partitionedTableLock;
    protected transient PartitionedTable partitionedTable;

    private transient Object seriesLock;
    private transient List series;
    private transient Set seriesKeys;
    private transient Map seriesNames;

    private final transient java.util.function.Function DEFAULT_NAMING_FUNCTION = key -> {
        final String keyString;
        if (key instanceof Object[]) {
            final Object[] keyArray = (Object[]) key;
            if (keyArray.length == 1) {
                keyString = Objects.toString(keyArray[0]);
            } else {
                keyString = Arrays.toString(keyArray);
            }
        } else {
            keyString = Objects.toString(key);
        }
        return name() + ": " + keyString;
    };

    transient java.util.function.Function namingFunction;

    private DynamicSeriesNamer seriesNamer;
    private transient Object seriesNamerLock;

    private boolean allowInitialization = false;
    protected boolean initialized;

    private String seriesNameColumnName = null;

    /**
     * Creates a MultiSeries instance.
     *
     * @param axes axes on which this {@link MultiSeries} will be plotted
     * @param id data series id
     * @param name series name
     * @param byColumns columns forming the keys of the partitioned table
     */
    AbstractMultiSeries(final AxesImpl axes, final int id, final Comparable name, final String[] byColumns) {
        super(axes, id, name);
        this.byColumns = byColumns;
        this.namingFunction = DEFAULT_NAMING_FUNCTION;

        initializeTransient();
    }

    /**
     * Creates a copy of a series using a different Axes.
     *
     * @param series series to copy.
     * @param axes new axes to use.
     */
    AbstractMultiSeries(final AbstractMultiSeries series, final AxesImpl axes) {
        super(series, axes);
        this.byColumns = series.byColumns;
        this.namingFunction = series.namingFunction;
        this.seriesNamer = series.seriesNamer;
        this.allowInitialization = series.allowInitialization;
        this.seriesNameColumnName = series.seriesNameColumnName;

        initializeTransient();
    }

    @Override
    public ChartImpl chart() {
        return axes().chart();
    }

    @Override
    public DynamicSeriesNamer getDynamicSeriesNamer() {
        return seriesNamer;
    }

    @Override
    public void setDynamicSeriesNamer(DynamicSeriesNamer seriesNamer) {
        this.seriesNamer = seriesNamer;
    }

    @Override
    public String[] getByColumns() {
        return byColumns;
    }

    String makeSeriesName(final String seriesName, final DynamicSeriesNamer seriesNamer) {
        // this would occur when server side datasets were being updated via OneClick
        // this should no longer happen
        if (seriesNamer == null) {
            throw new PlotIllegalStateException("seriesNamer null " + this, this);
        }

        synchronized (seriesNamerLock) {
            return seriesNamer.makeUnusedName(seriesName, getPlotInfo());
        }
    }

    @Override
    public AbstractMultiSeries seriesNamingFunction(
            final Function namingFunction) {
        if (namingFunction == null) {
            this.namingFunction = DEFAULT_NAMING_FUNCTION;
        } else {
            this.namingFunction = namingFunction;
        }

        applyNamingFunction(namingFunction);
        return this;
    }

    @Override
    public AbstractMultiSeries seriesNamingFunction(final Closure namingFunction) {
        return seriesNamingFunction(namingFunction == null ? null : new ClosureFunction<>(namingFunction));
    }

    /**
     * This is used by super classes so we can call applyNamingFunction during construction without NPEs
     */
    protected void applyNamingFunction() {
        applyNamingFunction(namingFunction);
    }

    private void applyNamingFunction(final java.util.function.Function namingFunction) {
        ArgumentValidations.assertNotNull(namingFunction, "namingFunction", getPlotInfo());
        seriesNameColumnName = seriesNameColumnName == null
                ? ColumnNameConstants.SERIES_NAME + this.hashCode()
                : seriesNameColumnName;
        final String functionInput = byColumns.length > 1
                ? "new Object[] {" + String.join(", ", byColumns) + "}"
                : byColumns[0];
        applyFunction(namingFunction, seriesNameColumnName, functionInput, String.class);
    }

    /**
     * Applies the {@code function} to the {@code byColumns} of the underlying table to create a new column named
     * {@code columnName}.
     *
     * @param function The function to apply
     * @param columnName The column name to create
     * @param functionInput The formula string to use for gathering input to {@code function}
     * @param resultClass The expected result type of {@code function}
     */
    protected  void applyFunction(final java.util.function.Function function,
            final String columnName, final String functionInput, final Class resultClass) {
        ArgumentValidations.assertNotNull(function, "function", getPlotInfo());
        final String queryFunction = columnName + "Function";
        final Map params = new HashMap<>();
        params.put(queryFunction, function);

        final String update = columnName + " = (" + resultClass.getSimpleName() + ") "
                + queryFunction + ".apply(" + functionInput + ")";

        applyTransform(columnName, update, new Class[] {resultClass}, params, true);
    }

    @Override
    public void initializeSeries(SERIES series) {

    }

    private List getSeries() {
        if (series == null) {
            synchronized (seriesLock) {
                if (series != null) {
                    return series;
                }

                series = new CopyOnWriteArrayList<>();
            }
        }

        return series;
    }

    @Override
    // this should be run under a seriesLock
    public void addSeries(SERIES series, Object key) {
        Assert.holdsLock(seriesLock, "seriesLock");

        if (seriesKeys == null) {
            seriesKeys = new HashSet<>();
        }

        if (seriesNames == null) {
            seriesNames = new HashMap<>();
        }

        if (seriesKeys.contains(key)) {
            log.warn("MultiSeries: attempting to add a series with the same key as an existing series.  key=" + key);
            return;
        }

        if (seriesNames.containsKey(series.name().toString())) {
            throw new PlotRuntimeException(
                    "MultiSeries: attempting to add a series with the same name as an existing series.   name="
                            + series.name() + "oldKey=" + seriesNames.containsKey(series.name()) + " newKey=" + key,
                    this);
        }

        initializeSeries(series);

        seriesKeys.add(key);
        seriesNames.put(series.name().toString(), key);
        getSeries().add(series);
    }

    @Override
    public SERIES get(final int series) {
        final int size = getSeries().size();

        if (series < 0 || series >= size) {
            throw new PlotRuntimeException("Series index is out of range: key=" + series + " range=[0," + size + "]",
                    this);
        }

        return getSeries().get(series);
    }

    @Override
    public int getSeriesCount() {
        return getSeries().size();
    }

    @Override
    public SERIES createSeries(String seriesName, final BaseTable t) {
        return createSeries(seriesName, t, seriesNamer);
    }

    private void initializeTransient() {
        this.partitionedTableLock = new Object();
        this.seriesLock = new Object();
        this.seriesNamerLock = new Object();
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        initializeTransient();
    }

    ////////////////////////////// CODE BELOW HERE IS GENERATED -- DO NOT EDIT BY HAND //////////////////////////////
    ////////////////////////////// TO REGENERATE RUN GenerateMultiSeries //////////////////////////////
    ////////////////////////////// AND THEN RUN GenerateFigureImmutable //////////////////////////////
// @formatter:off

    @Override public  AbstractMultiSeries pointColor(final groovy.lang.Closure pointColor, final Object... multiSeriesKey) {
        throw new PlotUnsupportedOperationException("DataSeries " + this.getClass() + " does not support method pointColor for arguments [groovy.lang.Closure]. If you think this method should work, try placing your multiSeriesKey into an Object array", this);
    }



    @Override public  AbstractMultiSeries pointColor(final java.util.function.Function pointColor, final Object... multiSeriesKey) {
        throw new PlotUnsupportedOperationException("DataSeries " + this.getClass() + " does not support method pointColor for arguments [java.util.function.Function]. If you think this method should work, try placing your multiSeriesKey into an Object array", this);
    }



    @Override public  AbstractMultiSeries pointColorInteger(final groovy.lang.Closure colors, final Object... multiSeriesKey) {
        throw new PlotUnsupportedOperationException("DataSeries " + this.getClass() + " does not support method pointColorInteger for arguments [groovy.lang.Closure]. If you think this method should work, try placing your multiSeriesKey into an Object array", this);
    }



    @Override public  AbstractMultiSeries pointColorInteger(final java.util.function.Function colors, final Object... multiSeriesKey) {
        throw new PlotUnsupportedOperationException("DataSeries " + this.getClass() + " does not support method pointColorInteger for arguments [java.util.function.Function]. If you think this method should work, try placing your multiSeriesKey into an Object array", this);
    }



    @Override public