Menu

CalendarMonth.svelte component

Generates an SVG calendar chart.

Param Default Required Description
calcCellSize (w: number, h: number) => number (w, h) => Math.min(w / 7, h / 5)
no
A function given the canvas width and height as arguments and expects a return number that will be used as the width and height for each cell. The default will choose a size that fits seven cells across and five rows top to bottom.

Used in these examples:

SSR Examples:

<!--
  @component
  Generates an SVG calendar chart.
 -->
<script>
  import { getContext } from 'svelte';
  import { utcFormat } from 'd3-time-format';
  import { utcDay } from 'd3-time';

  const { width, height, data, x, z, zScale, extents } = getContext('LayerCake');

  /**
   * @typedef {Object} Props
   * @property {(w: number, h: number) => number} [calcCellSize] - A function given the canvas width and height as arguments and expects a return number that will be used as the width and height for each cell. The default will choose a size that fits seven cells across and five rows top to bottom.
   */

  /** @type {Props} */
  let { calcCellSize = (w, h) => Math.min(w / 7, h / 5) } = $props();

  const getDate = utcFormat('%Y-%m-%d');
  const getDayOfWeek = utcFormat('%w');
  const getWeekOfYear = utcFormat('%U');

  let count = $derived(date => {
    const stringDate = date.toISOString().split('T')[0];
    const days = $data.filter(d => $x(d) === stringDate)[0];
    if (days) {
      return $z(days);
    }
    return 0;
  });

  let fillColor = $derived(day => {
    const n = count(day);
    return n ? $zScale(n) : '#fff';
  });

  let cellSize = $derived(calcCellSize($width, $height));

  /**
   * Calculate what month we're in and generate the full days of that month
   */
  /** @type {Date[]} */
  let days = $derived.by(() => {
    const minDate = $extents.x[0];
    const parts = minDate.split('-').map(d => +d);

    return utcDay.range(
      new Date(Date.UTC(parts[0], parts[1] - 1, 1)),
      new Date(Date.UTC(parts[0], parts[1], 1))
    );
  });

  let rectX = $derived(day => +getDayOfWeek(day) * cellSize);
  let rectY = $derived(day => {
    const startWeek = +getWeekOfYear(
      new Date(Date.UTC(day.getUTCFullYear(), day.getUTCMonth(), 1))
    );
    const thisWeek = +getWeekOfYear(day);
    const weekDiff = thisWeek - startWeek;
    return weekDiff * cellSize;
  });

  /**
   * @param {Date} day
   */
  function showCount(day) {
    console.log(day, count(day));
  }
</script>

{#each days as day}
  <rect
    class="day"
    width={cellSize}
    height={cellSize}
    x={rectX(day)}
    y={rectY(day)}
    style="fill:{fillColor(day)};"
    onmouseenter={() => showCount(day)}
    role="tooltip"><title>{getDate(day)}</title></rect
  >
{/each}

<style>
  .day {
    stroke: #000;
    stroke-width: 1;
    fill: #fff;
  }
</style>