/* * -----------------------------------------------------------------------\ * PerfCake *   * Copyright (C) 2010 - 2016 the original author or authors. *   * 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 org.perfcake.reporting.destination.c3chart; import org.perfcake.PerfCakeException; import org.perfcake.util.Utils; import io.vertx.core.json.JsonArray; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.LinkedList; import java.util.List; /** * Data of a C3 chart stored in the .js file as a script building an array. * Does not work with the data header and does not know anything about the actual data. * It is the role of {@link C3Chart} to carry all the meta-data. * * @author <a href="mailto:marvenec@gmail.com">Martin Večeřa</a> */ public class C3ChartData { /** * The individual lines of data. */ private List<JsonArray> data; /** * Target path where the charts report is stored. The individual data files are located in ${target}/data/${baseName}.js. */ private Path target; /** * Initialize the class with the provided data. No other actions are taken. * * @param target * The target path where the chart report is stored. The individual data files are located in ${target}/data/${baseName}.js. * @param data * An ordered list of individual data lines. */ private C3ChartData(final Path target, final List<JsonArray> data) { this.data = data; this.target = target; } /** * Reads the chart data from the appropriate data file. The data file is located in ${target}/data/${baseName}.js. * * @param baseName * The base of the chart data files name. * @param target * The target path where the chart report is stored. The individual data files are located in ${target}/data/${baseName}.js. * @throws PerfCakeException * When there was an error reading the data. Mainly because of some I/O error. */ public C3ChartData(final String baseName, final Path target) throws PerfCakeException { this(target, new LinkedList<>()); try { List<String> lines = Utils.readLines(Paths.get(target.toString(), "data", baseName + ".js").toUri().toURL()); for (final String line : lines) { if (line.startsWith(baseName)) { String jsonArray = line.substring((baseName + ".push(").length()); jsonArray = jsonArray.substring(0, jsonArray.length() - 2); data.add(new JsonArray(jsonArray)); } } } catch (IOException e) { throw new PerfCakeException("Cannot read chart data: ", e); } } /** * Creates a new chart where with only two columns from the original chart. These columns have indexes 0 and the second is specified in the parameter. * This is used to create a subchart with the values of the X axis and one data line. * * @param keepColumnIndex * The index of the column to be kept. * @return A new chart where with only two columns - the X axis values and the data line specified in the parameter. */ public C3ChartData filter(int keepColumnIndex) { final List<JsonArray> newData = new LinkedList<>(); data.forEach(array -> { List raw = array.getList(); newData.add(new JsonArray(Arrays.asList(raw.get(0), raw.get(keepColumnIndex)))); }); return new C3ChartData(target, newData); } /** * Gets the first line index where there are other values than null (except for the first index column). * * @return The first line index where there are other values than null */ private int getDataStart() { int idx = 0; if (data.size() == 0) { return -1; } boolean dataHit; do { dataHit = false; JsonArray a = data.get(idx); dataHit = dataHit || !isAllNull(a); if (!dataHit) { idx++; } } while (idx < data.size() && !dataHit); // @checkstyle.ignore(RightCurly) - Do-while cycle should have while after the brace. return idx; } /** * Checks whether all values in the field are null (except for the first index column). * * @param a * The array to be checked. * @return True if and only if all values in the field are null. */ private static boolean isAllNull(final JsonArray a) { int i = 1; while (i < a.size()) { if (a.getValue(i) != null) { return false; } i++; } return true; } /** * Gets a list with null values of the given size. * * @param size * The size of the new list. * @return The list with null values of the given size. */ @SuppressWarnings("unchecked") private static List getNullList(final int size) { final List nullList = new LinkedList<>(); for (int i = 0; i < size; i++) { nullList.add(null); } return nullList; } /** * Mixes two charts together sorted according to the first index column. Missing data for any index values in either chart are replaced with null. * Records with only null values are skipped. The existing chart data are not changed, a new instance is created. * * @param otherData * The other chart data to be mixed with this chart data. * @return A new chart data combining both input charts. */ @SuppressWarnings("unchecked") C3ChartData combineWith(final C3ChartData otherData) { final List<JsonArray> newData = new LinkedList<>(); int idx1 = getDataStart(); int idx2 = otherData.getDataStart(); if (idx1 == -1) { if (idx2 == -1) { return new C3ChartData(target, newData); } else { return new C3ChartData(target, new LinkedList<>(otherData.data)); } } else if (idx2 == -2) { return new C3ChartData(target, new LinkedList<>(data)); } int size1 = data.get(0).size(); List nullList1 = getNullList(size1 - 1); int size2 = otherData.data.get(0).size(); List nullList2 = getNullList(size2 - 1); while (idx1 < data.size() || idx2 < otherData.data.size()) { JsonArray a1 = idx1 < data.size() ? data.get(idx1) : null; JsonArray a2 = idx2 < otherData.data.size() ? otherData.data.get(idx2) : null; long p1 = a1 != null ? a1.getLong(0) : Long.MAX_VALUE; long p2 = a2 != null ? a2.getLong(0) : Long.MAX_VALUE; List raw = new LinkedList<>(); if (p1 == p2) { if (!isAllNull(a1) || !isAllNull(a2)) { raw.add(p1); raw.addAll(a1.getList().subList(1, size1)); raw.addAll(a2.getList().subList(1, size2)); } idx1++; idx2++; } else if (p1 < p2) { if (!isAllNull(a1)) { raw.add(p1); raw.addAll(a1.getList().subList(1, size1)); raw.addAll(nullList2); } idx1++; } else { if (!isAllNull(a2)) { raw.add(p2); raw.addAll(nullList1); raw.addAll(a2.getList().subList(1, size2)); } idx2++; } if (raw.size() > 0) { newData.add(new JsonArray(raw)); } } return new C3ChartData(target, newData); } /** * Gets the data lines. * * @return The data lines. */ public List<JsonArray> getData() { return data; } @Override public String toString() { return "C3ChartData{" + "data=" + data + '}'; } }