gt:     if (!color.equals(DEFAULT_COLOR)) context.put("color", color);
context: package tech.tablesaw.plotly.components;
  
  import static tech.tablesaw.plotly.components.Axis.Spikes.SpikeSnap.DATA;
  
  import com.google.common.base.Preconditions;
  import java.util.HashMap;
  import java.util.Map;
  import tech.tablesaw.plotly.Utils;
  import tech.tablesaw.plotly.traces.ScatterTrace;
  
  public class Axis extends Component {
  
    public enum CategoryOrder {
      TRACE("trace"),
      CATEGORY_ASCENDING("category ascending"),
      CATEGORY_DESCENDING("category descending"),
      ARRAY("array");
  
      private final String value;
  
      CategoryOrder(String value) {
        this.value = value;
      }
  
      @Override
      public String toString() {
        return value;
      }
    }
  
    /**
     * Sets the axis type. By default, plotly attempts to determined the axis type by looking into the
     * data of the traces that referenced the axis in question.
     */
    public enum Type {
      LINEAR("linear"),
      LOG("log"),
      DATE("date"),
      CATEGORY("category"),
      DEFAULT("-");
  
      private final String value;
  
      Type(String value) {
        this.value = value;
      }
  
      @Override
      public String toString() {
        return value;
      }
    }
  
    /**
     * Determines whether or not the range of this axis is computed in relation to the input data. See
     * `rangemode` for more info. If `range` is provided, then `autorange` is set to "False".
     */
    public enum AutoRange {
      TRUE("true"),
      FALSE("false"),
      REVERSED("reversed");
  
      private final String value;
  
      AutoRange(String value) {
        this.value = value;
      }
  
      @Override
      public String toString() {
        return value;
      }
    }
  
    /**
     * If this axis needs to be compressed (either due to its own `scaleanchor` and `scaleratio` or
     * those of the other axis), determines how that happens: by increasing the "range" (default), or
     * by decreasing the "domain".
     */
    public enum Constrain {
      RANGE("range"),
      DOMAIN("domain");
  
      private final String value;
  
      Constrain(String value) {
        this.value = value;
      }
  
      @Override
      public String toString() {
        return value;
      }
    }
  
    /**
     * If this axis needs to be compressed (either due to its own `scaleanchor` and `scaleratio` or
     * those of the other axis), determines which direction we push the originally specified plot
     * area. Options are "left", "center" (default), and "right" for x axes, and "top", "middle"
     * (default), and "bottom" for y axes.
     */
    public enum ConstrainToward {
      LEFT("left"),
      CENTER("center"),
      RIGHT("right"),
      TOP("top"),
      MIDDLE("middle"),
      BOTTOM("bottom");
  
      private final String value;
  
      ConstrainToward(String value) {
        this.value = value;
      }
  
      @Override
      public String toString() {
        return value;
      }
    }
  
    /**
     * If set to another axis id (e.g. `x2`, `y`), the range of this axis changes together with the
     * range of the corresponding axis such that the scale of pixels per unit is in a constant ratio.
     * Both axes are still zoomable, but when you zoom one, the other will zoom the same amount,
     * keeping a fixed midpoint. `constrain` and `constraintoward` determine how we enforce the
     * constraint. You can chain these, ie `yaxis: {scaleanchor: "x"}, xaxis2: {scaleanchor: "y"}` but
     * you can only link axes of the same `type`. The linked axis can have the opposite letter (to
     * constrain the aspect ratio) or the same letter (to match scales across subplots). Loops
     * (`yaxis: {scaleanchor: "x"}, xaxis: {scaleanchor: "y"}` or longer) are redundant and the last
     * constraint encountered will be ignored to avoid possible inconsistent constraints via
     * `scaleratio`.
     *
     * <p>TODO: Just make this a string?
     */
    public enum ScaleAnchor {
      X("/^x([2-9]|[1-9][0-9]+)?$/"),
      Y("/^y([2-9]|[1-9][0-9]+)?$/");
  
      private final String value;
  
      ScaleAnchor(String value) {
        this.value = value;
      }
  
      @Override
      public String toString() {
        return value;
      }
    }
  
    /**
     * If "normal", the range is computed in relation to the extrema of the input data. If "tozero"`,
     * the range extends to 0, regardless of the input data If "nonnegative", the range is
     * non-negative, regardless of the input data.
     */
    public enum RangeMode {
      NORMAL("normal"),
      TO_ZERO("tozero"),
      NON_NEGATIVE("nonnegative");
      private final String value;
  
      RangeMode(String value) {
        this.value = value;
      }
  
      @Override
      public String toString() {
        return value;
      }
    }
  
    /**
     * Determines whether an x (y) axis is positioned at the "bottom" ("left") or "top" ("right") of
     * the plotting area.
     */
    public enum Side {
      left("left"), // DEFAULT
      right("right"),
      top("top"),
      bottom("bottom");
  
      private final String value;
  
      Side(String value) {
        this.value = value;
      }
  
      @Override
      public String toString() {
        return value;
      }
    }
  
    private static final String DEFAULT_COLOR = "#444";
    private static final String DEFAULT_ZERO_LINE_COLOR = "#444";
    private static final String DEFAULT_LINE_COLOR = "#444";
    private static final String DEFAULT_GRID_COLOR = "#eee";
    private static final int DEFAULT_LINE_WIDTH = 1;
  
    private static final int DEFAULT_ZERO_LINE_WIDTH = 1;
    private static final int DEFAULT_GRID_WIDTH = 1;
    private static final boolean DEFAULT_SHOW_LINE = true;
    private static final boolean DEFAULT_SHOW_GRID = true;
    private static final boolean DEFAULT_ZERO_LINE = false;
    private static final double DEFAULT_SCALE_RATIO = 1.0;
    private static final Constrain DEFAULT_CONSTRAIN_RANGE = Constrain.RANGE;
  
    private static final AutoRange DEFAULT_AUTO_RANGE = AutoRange.TRUE;
    private static final Type DEFAULT_TYPE = Type.DEFAULT;
    private static final boolean DEFAULT_VISIBLE = true;
  
    private final String title;
    private final boolean visible;
    private final String color;
    private final Font font;
    private final Font titleFont;
    private final Type type;
  
    private final RangeMode rangeMode;
    private final AutoRange autoRange;
    private final Object[] range;
    private final boolean fixedRange; // true means the axis cannot be zoomed
    private final Constrain constrain;
    private final ConstrainToward constrainToward;
    private final double scaleRatio;
  
    private final Spikes spikes;
  
    private final int lineWidth;
    private final int zeroLineWidth;
    private final int gridWidth;
  
    private final String lineColor;
    private final String zeroLineColor;
    private final String gridColor;
  
    private final boolean showLine;
    private final boolean zeroLine;
    private final boolean showGrid;
  
    private final Side side;
    private final ScatterTrace.YAxis overlaying;
  
    private final CategoryOrder categoryOrder;
  
    private final TickSettings tickSettings;
  
    private final float[] domain;
  
    private final String rangeSlider;
  
    public static AxisBuilder builder() {
      return new AxisBuilder();
    }
  
    private Axis(AxisBuilder builder) {
      title = builder.title;
      titleFont = builder.titleFont;
      type = builder.type;
      visible = builder.visible;
      color = builder.color;
      font = builder.font;
      autoRange = builder.autoRange;
      range = builder.range;
      rangeMode = builder.rangeMode;
      fixedRange = builder.fixedRange;
      tickSettings = builder.tickSettings;
      side = builder.side;
      overlaying = builder.overlaying;
      spikes = builder.spikes;
  
      showLine = builder.showLine;
      zeroLine = builder.zeroLine;
      showGrid = builder.showGrid;
  
      lineColor = builder.lineColor;
      zeroLineColor = builder.zeroLineColor;
      gridColor = builder.gridColor;
  
      lineWidth = builder.lineWidth;
      zeroLineWidth = builder.zeroLineWidth;
      gridWidth = builder.gridWidth;
  
      constrain = builder.constrain;
      constrainToward = builder.constrainToward;
      scaleRatio = builder.scaleRatio;
      categoryOrder = builder.categoryOrder;
      domain = builder.domain;
      rangeSlider = builder.rangeSlider;
    }
  
    @Override
    public String asJavascript() {
      return asJavascript("axis_template.html");
    }
  
    @Override
    protected Map<String, Object> getContext() {
      Map<String, Object> context = new HashMap<>();
      context.put("title", title);
      context.put("titleFont", titleFont);
      if (visible != DEFAULT_VISIBLE) context.put("visible", visible);
      if (!type.equals(DEFAULT_TYPE)) context.put("type", type);
  