| <script lang="ts"> | |
| import type { ChartType, LegendItem, Point, TooltipItem } from "chart.js"; | |
| import { Chart } from "chart.js/auto"; | |
| import { createEventDispatcher, onMount } from "svelte"; | |
| import { writable, type Writable } from "svelte/store"; | |
| import { saveToClipboard as save } from "../clipboard.js"; | |
| import { LegendPlugin } from "../plugins.js"; | |
| import type { Data } from "../types/chart.js"; | |
| import type { Controls, ControlsEvent } from "../types/events.js"; | |
| import Legend from "./Legend.svelte"; | |
| import { isSampled } from "../transforms/standard-mappers.js"; | |
| export let data: Data; | |
| export let chartType: ChartType = "line"; | |
| export let isExperimental: boolean = false; | |
| export let showHistogramControls: boolean = false; | |
| // State | |
| let controlsDispatcher = createEventDispatcher<ControlsEvent>(); | |
| let element: HTMLCanvasElement; | |
| let buckets: Writable<number> = writable(100); | |
| let chart: Writable<Chart | null> = writable(null); | |
| let items: Writable<LegendItem[] | null> = writable(null); | |
| $: { | |
| if ($chart) { | |
| $chart.data = data; | |
| $chart.update(); | |
| } | |
| } | |
| // Effects | |
| onMount(() => { | |
| const onUpdate = (updated: Chart) => { | |
| $chart = updated; | |
| // Bad typings. | |
| const legend = updated.options.plugins?.legend as any; | |
| $items = legend.labels.generateLabels($chart); | |
| }; | |
| const plugins = { | |
| tooltip: { | |
| callbacks: { | |
| label: (context: TooltipItem<typeof chartType>): string | void | string[] => { | |
| // TODO: Configure Tooltips | |
| // https://www.chartjs.org/docs/latest/configuration/tooltip.html | |
| const label = context.dataset.label; | |
| const rp = context.raw as Point; | |
| const frequency = context.parsed.y; | |
| if (isSampled(label)) { | |
| const fx = rp.x?.toFixed(2); | |
| return `${label}: ${fx} F(${frequency})`; | |
| } else { | |
| // Fallback to default behavior | |
| return; | |
| } | |
| }, | |
| }, | |
| }, | |
| legend: { | |
| display: false, | |
| }, | |
| benchmark: { | |
| onUpdate: onUpdate, | |
| }, | |
| }; | |
| $chart = new Chart(element, { | |
| data: data, | |
| type: chartType, | |
| plugins: [LegendPlugin], | |
| options: { | |
| plugins: plugins, | |
| }, | |
| }); | |
| }); | |
| // Copy to clip board | |
| async function copy(event: Event) { | |
| if ($chart) { | |
| await save($chart); | |
| } | |
| } | |
| function onHistogramChanged(event: Event) { | |
| const element = event.target as EventTarget & HTMLInputElement; | |
| const oldValue = $buckets; | |
| $buckets = parseInt(element.value, 10); | |
| if (oldValue != $buckets) { | |
| let controls: Controls = { | |
| buckets: $buckets, | |
| }; | |
| controlsDispatcher("controls", controls); | |
| } | |
| } | |
| </script> | |
| <article> | |
| <div class="toolbar"> | |
| <button | |
| class="btn outline" | |
| data-tooltip="Copy chart to clipboard." | |
| on:click={copy} | |
| > | |
| ⎘ | |
| </button> | |
| </div> | |
| <canvas class="chart" bind:this={element}> </canvas> | |
| {#if showHistogramControls} | |
| <div class="controls"> | |
| <label for="buckets"> | |
| Histogram | |
| <input | |
| type="range" | |
| data-tooltip={$buckets} | |
| data-placement="right" | |
| min="10" | |
| max="250" | |
| value={$buckets} | |
| id="buckets" | |
| name="buckets" | |
| on:change={onHistogramChanged} | |
| /> | |
| </label> | |
| </div> | |
| {/if} | |
| {#if isExperimental} | |
| <footer class="slim"> | |
| <section class="experimental"> | |
| <kbd>Experimental</kbd> | |
| </section> | |
| </footer> | |
| {/if} | |
| </article> | |
| {#if $chart && $items} | |
| <Legend chart={$chart} items={$items} /> | |
| {/if} | |
| <style> | |
| .chart { | |
| width: 100%; | |
| } | |
| .toolbar { | |
| padding: 0; | |
| display: flex; | |
| flex-direction: row; | |
| justify-content: flex-end; | |
| } | |
| .toolbar .btn { | |
| width: auto; | |
| height: auto; | |
| border: none; | |
| padding: 5px; | |
| } | |
| .controls { | |
| margin-top: 20px; | |
| width: 100%; | |
| } | |
| .slim { | |
| margin-bottom: 0px; | |
| padding: 0; | |
| } | |
| .experimental { | |
| display: flex; | |
| flex-direction: row; | |
| flex-wrap: nowrap; | |
| justify-content: center; | |
| margin-bottom: 0px; | |
| } | |
| </style> |