package org.commcare.graph.view.c3; import org.commcare.graph.model.GraphData; import org.commcare.graph.model.SeriesData; import org.commcare.graph.util.GraphException; import org.commcare.graph.util.GraphUtil; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.Iterator; /** * Axis-related configuration for C3. * * Created by jschweers on 11/16/2015. */ public class AxisConfiguration extends Configuration { public AxisConfiguration(GraphData data) throws GraphException, JSONException { super(data); JSONObject x = getAxis("x"); JSONObject y = getAxis("y"); JSONObject y2 = getAxis("secondary-y"); if (mData.getType().equals(GraphUtil.TYPE_TIME)) { x.put("type", "timeseries"); } mConfiguration.put("x", x); mConfiguration.put("y", y); mConfiguration.put("y2", y2); // Bar graphs may be rotated. C3 defaults to vertical bars. if (mData.getType().equals(GraphUtil.TYPE_BAR) && !mData.getConfiguration("bar-orientation", "horizontal").equalsIgnoreCase("vertical")) { mConfiguration.put("rotated", true); } } /** * Add min and max bounds to given axis. * * @param axis Current axis configuration. Will be modified. * @param prefix Prefix for commcare model's configuration: "x", "y", or "secondary-y" */ private void addBounds(JSONObject axis, String prefix) throws GraphException, JSONException { addBound(axis, prefix, "min"); addBound(axis, prefix, "max"); } /** * Add min or max bound to given axis. * * @param axis Current axis configuratoin. Will be modified. * @param prefix Prefix for commcare model's configuration: "x", "y", or "secondary-y" * @param suffix "min" or "max" */ private void addBound(JSONObject axis, String prefix, String suffix) throws GraphException, JSONException { String key = prefix + "-" + suffix; String value = mData.getConfiguration(key); if (value != null) { if (prefix.equals("x") && mData.getType().equals(GraphUtil.TYPE_TIME)) { axis.put(suffix, parseTime(value, key)); } else { axis.put(suffix, parseDouble(value, key)); } } } /** * Configure tick count, placement, and labels. * * @param axis Current axis configuration. Will be modified. * @param key One of "x-labels", "y-labels", "secondary-y-labels" * @param varName If the axis uses a hash of labels (position => label), a variable * will be created with this name to store those labels. */ private void addTickConfig(JSONObject axis, String key, String varName) throws GraphException, JSONException { // The labels configuration might be a JSON array of numbers, // a JSON object of number => string, or a single number String labelString = mData.getConfiguration(key); JSONObject tick = new JSONObject(); boolean usingCustomText = false; boolean isX = key.startsWith("x"); mVariables.put(varName, "{}"); if (labelString != null) { try { // Array: label each given value JSONArray labels = new JSONArray(labelString); JSONArray values = new JSONArray(); for (int i = 0; i < labels.length(); i++) { String value = labels.getString(i); if (isX && mData.getType().equals(GraphUtil.TYPE_TIME)) { values.put(parseTime(value, key)); } else { values.put(parseDouble(value, key)); } } tick.put("values", values); } catch (JSONException je) { // Assume try block failed because labelString isn't an array. // Try parsing it as an object. try { // Object: each key is a location on the axis, // and the value is text with which to label it JSONObject labels = new JSONObject(labelString); JSONArray values = new JSONArray(); Iterator i = labels.keys(); while (i.hasNext()) { String location = (String)i.next(); if (isX && mData.getType().equals(GraphUtil.TYPE_TIME)) { values.put(parseTime(location, key)); } else { values.put(parseDouble(location, key)); } } tick.put("values", values); mVariables.put(varName, labels.toString()); usingCustomText = true; } catch (JSONException e) { // Assume labelString is just a scalar, which // represents the number of labels the user wants. tick.put("count", Math.round(Double.valueOf(labelString))); } } } if (isX && !usingCustomText && mData.getType().equals(GraphUtil.TYPE_TIME)) { tick.put("format", mData.getConfiguration("x-labels-time-format", "%Y-%m-%d")); } if (key.startsWith("secondary-y")) { // If there aren't any series for the secondary y axis, don't label it boolean hasSecondaryAxis = false; for (SeriesData s : mData.getSeries()) { hasSecondaryAxis = hasSecondaryAxis || Boolean.valueOf(s.getConfiguration("secondary-y", "false")); if (hasSecondaryAxis) { break; } } if (!hasSecondaryAxis) { tick.put("values", new JSONArray()); } } if (tick.length() > 0) { axis.put("tick", tick); } } /** * Add title to axis. * * @param axis Current axis configuration. Will be modified. * @param key One of "x-title", "y-title", "secondary-y-title" * @param position For horizontal axis, (inner|outer)-(right|center|left) * For vertical axis, (inner|outer)-(top|middle|bottom) */ private void addTitle(JSONObject axis, String key, String position) throws JSONException { String title = mData.getConfiguration(key, ""); // String.trim doesn't cover characters like unicode's non-breaking space title = title.replaceAll("^\\s*", ""); title = title.replaceAll("\\s*$", ""); // Show title regardless of whether or not it exists, to give all graphs consistent padding JSONObject label = new JSONObject(); label.put("text", title); label.put("position", position); axis.put("label", label); } /** * Generate axis configuration. * * @param prefix Prefix for commcare model's configuration: "x", "y", or "secondary-y" * @return JSONObject representing the axis's configuration */ private JSONObject getAxis(String prefix) throws GraphException, JSONException { final boolean showAxes = Boolean.valueOf(mData.getConfiguration("show-axes", "true")); if (!showAxes) { return new JSONObject("{ show: false }"); } JSONObject config = new JSONObject(); boolean isX = prefix.equals("x"); // X and primary Y axis show by default, but not secondary y. Force them all to show. // Display secondary y axis, regardless of if it has data; this makes the // whitespace around the graph look more reasonable. config.put("show", true); // Undo C3's automatic axis padding config.put("padding", new JSONObject("{top: 0, right: 0, bottom: 0, left: 0}")); addTitle(config, prefix + "-title", isX ? "outer-center" : "outer-middle"); addBounds(config, prefix); String jsPrefix = prefix.equals("secondary-y") ? "y2" : prefix; addTickConfig(config, prefix + "-labels", jsPrefix + "Labels"); return config; } }