Time-Series Chart, W Svelte, Pancake, TypeScript

<script lang="ts">

  import * as Pancake from '@sveltejs/pancake';

  interface Point {
    x: number,
    y: number,
  };

  interface IndividualPoint extends Point {
    langInfo: ChartItem,
  }

  interface ChartItem {
    name: string,
    data: Point[],
    color?: string,
  }

  export let programmingLanguages: ChartItem[] = [];

  // Axis for chart
  let x1 = +Infinity;
    let x2 = -Infinity
    let y1 = +Infinity;
    let y2 = -Infinity;

  // Set axis based on data
    programmingLanguages.forEach(progLang => {
        progLang.data.forEach(d => {
      x1 = d.x < x1 ? d.x : x1;
      x2 = d.x > x2 ? d.x : x2;
      y1 = d.y < y1 ? d.y : y1;
      y2 = d.y > y2 ? d.y : y2;
        });
    });

  // Highlighted point, closest to mouse
    let closest: IndividualPoint | undefined;

  // Value of filter search field
    let filterValue = '';

  // Filter programming languages based on search field
  const filterNodes = (progLang: ChartItem) => progLang.name.toLowerCase().includes(filterValue.toLowerCase());

  // Data to display, filter if search filed has value
    $: filtered = filterValue ? programmingLanguages.filter(filterNodes) : programmingLanguages;

  // Points to display, used for quadtree
  // $:points = [];
  $: points = filtered.reduce((points: ChartItem[] | any, langInfo: ChartItem) => {
    const reduced = langInfo.data.map(d => ({ x: d.x, y: d.y, langInfo}));
    return [...points, ...reduced]; 
    }, []);

</script>


<input placeholder="Type to filter" bind:value={filterValue}>

<div class="chart">
    <Pancake.Chart {x1} {x2} y1={y1} y2={y2}>

    <!-- Horizontal lines -->
        <Pancake.Grid horizontal count={5} let:value>
            <div class="grid-line horizontal"><span>{value}</span></div>
        </Pancake.Grid>

    <!-- Vertical Lines -->
        <Pancake.Grid vertical count={5} let:value>
            <span class="x-label">{value}</span>
        </Pancake.Grid>

    <!-- The actual charts -->
        <Pancake.Svg>
            {#each filtered as progLang}
                <Pancake.SvgLine data={progLang.data} let:d>
                    <path class="data" style={`--langColor: ${progLang.color}a8;`} {d}></path>
                </Pancake.SvgLine>
            {/each}

      <!-- Highlighted line (nearest mouse pointer) -->
            {#if closest?.langInfo}
                <Pancake.SvgLine data={closest.langInfo.data} let:d>
                    <path class="highlight" style={`--langColor: ${closest.langInfo.color};`} {d}></path>
                </Pancake.SvgLine>
            {/if}
        </Pancake.Svg>

    <!-- Shows circular dot, on data point nearest mouse pointer -->
        {#if closest?.langInfo}
            <Pancake.Point x={closest.x} y={closest.y}>
                <span class="annotation-point"></span>
                <div class="annotation" style="transform: translate(-{100 * ((closest.x - x1) / (x2 - x1))}%,0)">
                    <strong>{closest.langInfo.name}</strong>
                    <span>{closest.x}: {closest.y} years</span>
                </div>
            </Pancake.Point>
        {/if}

        <Pancake.Quadtree data={points} bind:closest/>
    </Pancake.Chart>
</div>

<style lang="scss">
    .chart {
        height: 400px;
        padding: 2rem 1rem;
        margin: 0 auto;
    }

    input {
        font-size: 0.75rem;
        font-family: inherit;
    padding: 0.5em;
    color: var(--accent);
    background: var(--background);
    border: var(--card-border);
    border-radius: var(--curve-factor);
    &:focus, &:hover {
      outline: none;
    }
    }

    .grid-line {
        position: relative;
        display: block;
    }

    .grid-line.horizontal {
        width: calc(100% + 2em);
        left: -2em;
        border-bottom: 1px dashed var(--dimmed-text);
    }

    .grid-line span {
        position: absolute;
        left: 0;
        bottom: 0.2rem;
        color: var(--foreground);
    opacity: 0.8;
    }

    .x-label {
        position: absolute;
        width: 4rem;
        left: -2rem;
        bottom: -2rem;
        color: var(--foreground);
    opacity: 0.8;
        text-align: center;
    }

    path.data {
        stroke: var(--langColor);
        stroke-linejoin: round;
        stroke-linecap: round;
        stroke-width: 1px;
        fill: none;
    }

    .highlight {
        stroke: var(--langColor);
        fill: none;
        stroke-width: 2;
    }

    .annotation {
        position: absolute;
        white-space: nowrap;
        bottom: 1rem;
        background-color: var(--card-background);
    border: var(--card-border);
    border-radius: var(--curve-factor);
        padding: 0.2rem 0.4rem;
    }

    .annotation-point {
        position: absolute;
        width: 0.75rem;
        height: 0.75rem;
        background-color: var(--foreground);
        border-radius: 50%;
        transform: translate(-50%,-50%);
    }

    .annotation strong {
        display: block;
        font-size: 20px;
    }

    .annotation span {
        display: block;
        font-size: 14px;
    }
</style>

Usage:

<script lang="ts">
  import TimeLines from '$src/components/charts/TimeLines.svelte';

  const programmingLanguages = [
    { name: 'United States', color: '#88ff88', data: [
      { x: 2020, y: 1 },
      { x: 2021, y: 2 },
      { x: 2022, y: 3 },
    ]},
    { name: 'Somewhere else', color: '#ff0099', data: [
      { x: 2020, y: 0 },
      { x: 2021, y: 4 },
      { x: 2022, y: 6 },
    ]},
  ];
</script>

<TimeLines programmingLanguages={programmingLanguages} />