understanding_rl_vision/rl_clarity/svelte/graph.svelte (136 lines of code) (raw):
<script>
import Scrubber from './scrubber.svelte';
export let titles = [];
export let series = [];
export let dones = [];
export let colors = [];
export let state;
export let height;
export let width;
export let visible_duration = 1;
export let scrubber = visible_duration > 1;
export let label_precision = 3;
let graph_div;
$: vertical_scale = (function () {
let length_ratio = parseFloat(height) / parseFloat(width);
if (typeof(graph_div) !== "undefined") {
let suffix = (s => s.match(/[^0-9]*$/)[0].trim());
if (suffix(height) !== suffix(width)) {
length_ratio = graph_div.offsetHeight / graph_div.offsetWidth;
}
}
return 0.98 * visible_duration * length_ratio;
})();
$: series_rescaled = series.map(function(l) {
let min = Math.min.apply(null, l);
let max = Math.max.apply(null, l);
return l.map(x => min == max ? 0 : vertical_scale * (x - min) / (max - min));
});
$: max_duration = Math.max.apply(null, series.map(arr => arr.length));
$: svg_display_for_refresh = (function() {
if (state.position % 2 == 0) {
return "block";
}
else {
return "initial";
}
})();
const format_number = function(n, precision = label_precision) {
let base = n.toPrecision(precision).replace("-", "−");
let exponent = "";
if (base.includes("e")) {
[base, exponent] = base.split("e");
}
base = base.replace(/\.0+$/, "");
if (exponent !== "") {
return base + "×10" + "<sup>" + exponent + "</sup>";
}
else {
return base;
}
};
</script>
<style>
.container {
position: absolute;
overflow: hidden;
height: 100%;
width: 100%;
}
line {
stroke-width: 0.5;
}
.label {
background-color: rgba(255, 255, 255, 0.9);
font-weight: bold;
}
</style>
<div bind:this={graph_div} style="position: relative; height: {height}; width: {width};">
<div class="container" style="z-index: 0;">
<svg
viewBox="0 -{vertical_scale} {max_duration} {vertical_scale}"
style="display: {svg_display_for_refresh};
position: absolute;
height: 100%;
width: {(max_duration / visible_duration) * 100}%;
left: {(0.5 - (state.position + 0.5) / visible_duration) * 100}%;
background: white;
outline: 2px solid gray;
outline-offset: -1px;"
>
{#each Array.from(series.keys()) as index}
{#each Array.from(Array(series[index].length - 1).keys()) as position}
{#if !dones[index][position]}
<line
x1={position + 0.5}
y1={- series_rescaled[index][position]}
x2={position + 1.5}
y2={- series_rescaled[index][position + 1]}
style="stroke: {colors[index]};"
></line>
{:else}
<line
x1={position + 1}
y1={0}
x2={position + 1}
y2={- vertical_scale}
style="stroke: lightgray;"
></line>
{/if}
{/each}
{/each}
</svg>
</div>
<div class="container" style="z-index: 1;">
{#each Array.from(series.keys()) as index}
{#if typeof(series[index][state.position]) !== "undefined"}
<div
class="label"
style="position: absolute;
bottom: {((series.length - index - 0.5) / series.length) * 100}%;
left: 3px;
margin-bottom: -0.75em;
padding: 0.25em;
color: {colors[index]};"
>
{@html titles[index]}
</div>
<div
class="label"
style="position: absolute;
bottom: {((series.length - index - 0.5) / series.length) * 100}%;
right: calc(50% + 8px);
margin-bottom: -0.75em;
padding: 0.25em;"
>
{@html format_number(series[index][state.position])}
</div>
{/if}
{/each}
</div>
<div class="container" style="z-index: 2;">
{#if scrubber}
<Scrubber bind:state={state} visible_duration={visible_duration} max_duration={max_duration}/>
{/if}
</div>
</div>