Rich-Harris / pancake

Experimental charting library for Svelte
MIT License
1.29k stars 61 forks source link

Logarithmic scale #28

Open katyo opened 3 years ago

katyo commented 3 years ago

Have you plans to support logarithmic scale grids?

finnbear commented 3 years ago

Here's a workaround for the time being. Please understand that I extracted this from a larger codebase and didn't test it by itself. Some assembly required.

Usage

<Chart
    data={[{x: 1, y: 1}, {x: 2, y: 10}, {x: 3, y: 100}]}
    logarithmic=true
/>

Chart.svelte

<script>
    import * as Pancake from '@sveltejs/pancake';

    export let data;
    export let logarithmic = false;

    export let x = p => p.x;
    export let y = p => p.y;

    const identity = a => a;

    export let fmtX = identity;
    export let fmtY = identity

    function safeLog10(x) {
        if (x > 0) {
            return Math.log10(x);
        }
        return 0;
    }

    function exp10(x) {
        return Math.pow(10, x);
    }

    $: scaleY = logarithmic ? safeLog10 : identity;
    $: yScaled = p => scaleY(y(p));
    $: unScaleY = logarithmic ? exp10 : identity;

    let closest;

    let x1;
    let y1;
    let x2;
    let y2;

    $: {
        x1 = Infinity;
        y1 = Infinity;
        x2 = -Infinity;
        y2 = -Infinity;

        if (Array.isArray(data)) {
            data.forEach(point => {
                x1 = Math.min(x1, x(point));
                y1 = Math.min(y1, yScaled(point));
                x2 = Math.max(x2, x(point));
                y2 = Math.max(y2, yScaled(point));
            })
        }
    }
</script>

<div class=chart>
    <Pancake.Chart {x1} {y1} {x2} {y2}>
        <Pancake.Grid horizontal count={3} let:value>
            <div class="grid-line horizontal"><span>{fmtY(unScaleY(value))}</span></div>
        </Pancake.Grid>

        <Pancake.Grid vertical count={5} let:value>
            <div class="grid-line vertical"><span>{fmtX(value)}</span></div>
        </Pancake.Grid>

        <Pancake.Svg>
            <Pancake.SvgLine {data} {x} y={yScaled} let:d>
                <path class=line {d}/>
            </Pancake.SvgLine>

            <Pancake.SvgScatterplot {data} {x} y={yScaled} let:d>
                <path class=scatter {d}/>
            </Pancake.SvgScatterplot>

            {#if closest}
                <Pancake.SvgPoint x={x(closest)} y={yScaled(closest)} let:d>
                    <path class=highlight {d}/>
                </Pancake.SvgPoint>
            {/if}
        </Pancake.Svg>

        {#if closest}
            <Pancake.Point x={x(closest)} y={yScaled(closest)}>
                <span class="annotation-point"></span>
                <div class="annotation" style="transform: translate(-{100 * ((x(closest) - x1) / (x2 - x1))}%, 0)">
                    <span>{fmtX(x(closest))}, {fmtY(y(closest))}</span>
                </div>
            </Pancake.Point>
        {/if}

        <Pancake.Quadtree {data} {x} y={yScaled} bind:closest/>
    </Pancake.Chart>
</div>

<style>
    .chart {
        box-sizing: border-box;
        height: 300px;
        padding: 3em 2em 2em 3em;
    }

    .axes {
        width: 100%;
        height: 100%;
        border-left: 1px solid black;
        border-bottom: 1px solid black;
    }

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

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

    .grid-line.vertical {
        border-left: 1px dashed #ccc;
        height: 100%;
    }

    .grid-line.horizontal span {
        position: absolute;
        left: 0;
        bottom: 2px;
        font-family: sans-serif;
        font-size: 14px;
        color: #999;
    }

    .grid-line.vertical span {
        position: absolute;
        width: 4em;
        left: -2em;
        bottom: -30px;
        font-family: sans-serif;
        font-size: 14px;
        color: #999;
        text-align: center;
    }

    .x-label {
        position: absolute;
        width: 4em;
        left: -2em;
        bottom: -22px;
        font-family: sans-serif;
        font-size: 14px;
        color: #999;
        text-align: center;
    }

    path {
        stroke-linejoin: round;
        stroke-linecap: round;
        fill: none;
    }

    path.highlight {
        stroke: red;
        stroke-width: 10px;
    }

    path.line {
        stroke: black;
        stroke-width: 2px;
    }

    path.scatter {
        stroke: black;
        stroke-width: 7px;
    }

    div.annotation {
        position: absolute;
        white-space: nowrap;
        width: 8em;
        bottom: 1em;
        background-color: white;
        line-height: 1;
        text-shadow: 0 0 10px white, 0 0 10px white, 0 0 10px white, 0 0 10px white, 0 0 10px white, 0 0 10px white, 0 0 10px white;
    }
</style>

Image

image