Skip to content

wavy ​

Warps the plane with a sinusoidal ripple, so a straight edge is drawn as a wave. Used for decorative geometry — a balloon string, a flag, a squiggly connector.

ts
import { color, color6, ellipse, layer, rect, wavy } from "gofish-graphics";
import { mix } from "spectral.js";
import _ from "lodash";
import { catchLocations, seafood } from "./dataset";

const scatterData = _(seafood)
  .groupBy("lake")
  .map((lakeData, lake) => ({
    lake,
    x: catchLocations[lake].x,
    y: catchLocations[lake].y,
    collection: lakeData.map((item) => ({
      species: item.species,
      count: item.count,
    })),
  }))
  .value();

const container = document.getElementById("app");

const colorMap = {
  0: color.red,
  1: color.blue,
  2: color.green,
  3: color.yellow,
  4: color.purple,
  5: color.orange,
};

const Balloon = (options) =>
  layer(
    {
      x: options?.x - 15 * (options?.scale ?? 1),
      y: options?.y + 27 * (options?.scale ?? 1),
      box: true,
      transform: {
        scale: { x: options?.scale ?? 1, y: (options?.scale ?? 1) * -1 },
      },
    },
    [
      ellipse({
        cx: 15,
        cy: 15,
        w: 24,
        h: 30,
        fill: (options?.color ?? color.red)[4],
      }),
      ellipse({
        cx: 12,
        cy: 11,
        w: 7,
        h: 11,
        fill: (options?.color ?? color.red)[3],
      }),
      rect({
        cx: 15,
        cy: 32,
        w: 8,
        h: 4,
        fill: (options?.color ?? color.red)[5],
        rx: 3,
        ry: 2,
      }),
      rect({
        cx: 15,
        cy: 32,
        w: 5,
        h: 2.4,
        fill: (options?.color ?? color.red)[6],
        rx: 2,
        ry: 1,
      }),
    ]
  );

layer(
  { coord: wavy(), x: 0, y: 0 },
  scatterData.map((data, i) =>
    layer({ x: data.x }, [
      rect({
        x: 0,
        y: 0,
        w: 1,
        h: data.y,
        emY: true,
        fill: color.black,
      }),
      Balloon({
        scale: 1,
        x: 0,
        y: data.y,
        color: /* colorMap[i % 6] */[
          null,
          null,
          null,
          mix(color6[i % 6], color.white, 0.5),
          color6[i % 6],
          mix(color6[i % 6], color.black, 0.1),
          mix(color6[i % 6], color.black, 0.35),
        ],
      }),
    ])
  )
).render(container, {
  axes: true,
});
Dataset
ts
export type Lakes =
  | "Lake A"
  | "Lake B"
  | "Lake C"
  | "Lake D"
  | "Lake E"
  | "Lake F";

export type CatchData = {
  lake: Lakes;
  species: "Bass" | "Trout" | "Catfish" | "Perch" | "Salmon";
  count: number;
};

export const catchLocations: Record<Lakes, { x: number; y: number }> = {
  "Lake A": { x: 5.26, y: 22.64 },
  "Lake B": { x: 30.87, y: 120.75 },
  "Lake C": { x: 50.01, y: 60.94 },
  "Lake D": { x: 115.13, y: 94.16 },
  "Lake E": { x: 133.05, y: 50.44 },
  "Lake F": { x: 85.99, y: 172.78 },
};

export const catchLocationsArray = Object.entries(catchLocations).map(
  ([lake, { x, y }]) => ({
    lake,
    x,
    y,
  })
);

export const seafood: CatchData[] = [
  {
    lake: "Lake A",
    species: "Bass",
    count: 23,
  },
  {
    lake: "Lake A",
    species: "Trout",
    count: 31,
  },
  {
    lake: "Lake A",
    species: "Catfish",
    count: 29,
  },
  {
    lake: "Lake A",
    species: "Perch",
    count: 12,
  },
  {
    lake: "Lake A",
    species: "Salmon",
    count: 8,
  },
  // … 25 more rows
];

export const catchData = seafood;

export const catchDataWithLocations = seafood.map((catchItem) => {
  const location = catchLocationsArray.find(
    (loc) => loc.lake === catchItem.lake
  );
  return {
    ...catchItem,
    x: location?.x,
    y: location?.y,
  };
});

export type CatchDataWithLocation = (typeof catchDataWithLocations)[0];

Signature ​

ts
wavy();

Pass it as the coord of a layer (or of a chart) to warp everything inside that scope:

ts
layer({ coord: wavy(), x: 0, y: 0 }, [rect({ w: 4, h: 120 })]);

Parameters ​

A sinusoidal warp of the plane. No options.

No options.