Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom scales #55

Open
flekschas-ozette opened this issue Jan 5, 2024 · 3 comments
Open

Custom scales #55

flekschas-ozette opened this issue Jan 5, 2024 · 3 comments

Comments

@flekschas-ozette
Copy link
Contributor

I'm wondering what the easiest way might be to implement custom scales for CandyGraph. From the top of my head, I would think to just implement a custom class that extends from Scale but if I'm not mistaken, the Scale class isn't exported by CandyGraph.

I also wonder how to support tick marks for custom scales as it looks like OrthoAxis holds the implementation for ticks. Could it make sense to have the scale class implement a function called ticks() that receives as input { resolvedTickStep, resolvedAxisLow, resolvedAxisHigh, tickOrigin } and returns an array of tick values? That way, the user wouldn't have to also implement a custom OrthoAxis class. (I assume a similar function would be needed for minorTicks)

Thanks for your help!

@wwwtyro
Copy link
Owner

wwwtyro commented Jan 7, 2024

Hey @flekschas-ozette, good to hear from you 🙂 I think exporting Scale makes a lot of sense 👍

The OrthoAxis stuff is a little trickier. I don't think Scale should have to know about ticks. Since OrthoAxis primarily calculates ticks and passes them to Axis, I think the right way forward might be to use Axis directly. Another option might be to pass the ticks as customTicks and customMinorTicks to OrthoAxis, though it would be doing very little at that point.

@flekschas
Copy link
Collaborator

I agree that the tick mark computation does not fit to the scale class. On the other hand, it'd be a little bit annoying having to re-implement OrthoAxis or to duplicated the computation of the resolved resolved values (tick step, axis low, and axis high). Instead of having the option to pass customTicks and customMinorTicks to OrthoAxis, what do you think of the idea of having OrthoAxis accept two optional functions for computing custom ticks and custom minor ticks?

Something like this:

export type GetTicks = (tickStep: number, tickOrigin: number, axisLow: number, axisHigh: number) => number[];

const getLinearTicks: GetTicks = (tickStep, tickOrigin, axisLow, axisHigh) => {
  const ticks = [];

  let tickLocation =
    tickOrigin +
    tickStep * Math.floor((axisLow - tickOrigin) / tickStep) -
    tickStep * 2;

  while (tickLocation <= axisHigh + tickStep) {
    const tick = tickLocation - axisLow;
    ticks.push(tick);
    tickLocation += tickStep;
  }

  return ticks;
}

const getLogTicks: GetTicks = (tickStep, tickOrigin, axisLow, axisHigh) => {
  const ticks = [];

  const tickPowerLow = Math.log(axisLow) / Math.log(scale.base);
  const tickPowerHigh = Math.log(axisHigh) / Math.log(scale.base);

  let tickPower =
    tickOrigin + resolvedTickStep * Math.floor((tickPowerLow - tickOrigin) / tickStep) - tickStep;

  while (tickPower <= tickPowerHigh + tickStep) {
    const tickLocation = Math.pow(scale.base, tickPower);
    const tick = tickLocation - axisLow;
    ticks.push(tick);
    tickPower += tickStep;
  }

  return ticks;
}

export interface OrthoAxisOptions extends AxisOptions {
  /** The maximum value encompassed by this axis. */
  axisHigh?: number;
  /** The position on the opposing axis that this axis intercepts. */
  axisIntercept?: number;
  /** The minimum value encompassed by this axis. */
  axisLow?: number;
  /** The function to use to format the ticks. Default `(n: number) => n.toString()`. */
  labelFormatter?: (n: number) => string;
  /** The number of minor ticks between major ticks. None if undefined. Default undefined. */
  minorTickCount?: number;
  /** Used to anchor ticks to the axis. Using a value of 0.1 and a tickStep of
   * 1.0 will result in ticks at `[... -1.9, -0.9, 0.1, 1.1 ... ]`. Default 0.*/
  tickOrigin?: number;
  /** The distance between ticks. Default 1. */
  tickStep?: number;
  /** The function to use to compute tick values. Default undefined. */
  getTicks?: GetTicks;
}

 export class OrthoAxis extends Composite {
  public readonly computed: OrthoAxisComputed;
  private axis: Renderable = [];

  constructor(
    cg: CandyGraph,
    coords: CartesianCoordinateSystem,
    axis: "x" | "y",
    font: Font,
    options: OrthoAxisOptions = {}
  ) {
    super();
    const opts = { ...DEFAULTS, ...options };
    const { axisIntercept, axisLow, axisHigh, minorTickCount, tickOrigin, tickStep, labelFormatter } = opts;

    if (tickStep === 0) {
      throw new Error("tickStep must be non-zero.");
    }

    const resolvedTickStep = Math.abs(tickStep);

    const isx = axis === "x";

    const scale = isx ? coords.xscale : coords.yscale;
    const otherScale = isx ? coords.yscale : coords.xscale;

    const resolvedAxisIntercept = axisIntercept ?? otherScale.domain[0];

    const resolvedAxisLow = axisLow ?? scale.domain[0];
    const resolvedAxisHigh = axisHigh ?? scale.domain[1];

    // Defaults to no ticks
    let getTicks: GetTicks = () => [];

    if (opts.getTicks) {
      getTicks = opts.getTicks;
    } else if (scale.kind === ScaleKind.Linear) {
      getTicks = getLinearTicks;
    } else if (scale.kind === ScaleKind.Log) {
      getTicks = getLogTicks;
    }

    const ticks = getTicks(resolvedTickStep, tickOrigin, resolvedAxisLow, resolvedAxisHigh);

   ...

This is just an early quick idea. I don't know if the function signature (GetTicks) is the best or whether an object would be better suited and if we should maybe also pass the actual scale instance to it. But before I brainstorm further I want to get your thoughts :)

By the way, happy new year! 🎆 :)

@wwwtyro
Copy link
Owner

wwwtyro commented Jan 8, 2024

Yeah that seems fair to me, and your solution looks good. I think documenting that function might be the most difficult part.

Happy new year! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants