svend3 chart library
Svelte x D3 "plug and play" charting library

Scatter Chart

Chart Data Schema

<script>
    import { scaleLinear, Delaunay } from 'd3';
    import data from './scatter-data' // or pass data to component as prop
    
    const r = $ChartDocs[0].value; // (fixed) radius of dots, in pixels
    const marginTop = $ChartDocs[1].value; // the top margin, in pixels
    const marginRight = $ChartDocs[2].value; // the right margin, in pixels
    const marginBottom = $ChartDocs[3].value; // the bottom margin, in pixels
    const marginLeft = $ChartDocs[4].value; // the left margin, in pixels
    const inset = $ChartDocs[5].value; // inset the default range, in pixels
    const insetTop = inset; // inset the default y-range
    const insetRight = inset; // inset the default x-range
    const insetBottom = inset; // inset the default y-range
    const insetLeft = inset; // inset the default x-range
    const width = $ChartDocs[6].value; // the outer width of the chart, in pixels
    const height = $ChartDocs[7].value; // the outer height of the chart, in pixels
    const xLabel = $ChartDocs[8].value; // a label for the y-axis
    const yLabel = $ChartDocs[9].value; // a label for the y-axis
    const xFormat = $ChartDocs[10].value; // a format specifier string for the x-axis
    const yFormat = $ChartDocs[11].value; // a format specifier string for the y-axis
    const xScalefactor = $ChartDocs[12].value; //x-axis number of values
    const yScalefactor = $ChartDocs[13].value; //y-axis number of values
      // number of colors in fill array MUST match number of subsets in data
    const colors = $ChartDocs[14].value; // fill color for dots
    const filled = $ChartDocs[15].value; // whether dots should be filled or outlined
    const tooltipBackground = $ChartDocs[16].value; // background color of tooltip
    const tooltipTextColor = $ChartDocs[17].value; // text color of tooltip
  
    const xType = scaleLinear;
    const yType = scaleLinear;
    const xRange = [marginLeft + insetLeft, width - marginRight - insetRight]; // [left, right]
    const yRange = [height - marginBottom - insetBottom, marginTop + insetTop]; // [bottom, top]
  
    let x, y, selectedDot, dotInfo, subsets, xVals = [], yVals = [], points = [];
    let reactiveUnit, reactiveXTicks, reactiveYTicks;
    $: reactiveFilters = [...colors];

    // For a single set of data
    if (!Array.isArray(Object.values(data[0])[1])) {
        x = Object.keys(data[0])[0];
        y = Object.keys(data[0])[1];
        xVals = data.map((el) => el[x]);
        yVals = data.map((el) => el[y]);
        points = data.map((el) => [el[x], el[y], 0]);
    }
    // For data with subsets (NOTE: expects 'id' and 'data' keys)
    else {
        x = Object.keys(data[0]?.data[0])[0];
        y = Object.keys(data[0]?.data[0])[1];
        subsets = [];
        data.forEach((subset, i) => {
            subset.data.forEach((coordinate) => {
            xVals.push(coordinate[x]);
            yVals.push(coordinate[y]);
            points.push([coordinate[x], coordinate[y], i]);
            });
            subsets.push(subset.id);
        });
    }
  
    const xDomain = [0, Math.max(...xVals)];
    const yDomain = [0, Math.max(...yVals)];
    const xScale = xType(xDomain, xRange);
    const yScale = yType(yDomain, yRange);
  
    $: reactivePointsScaled = points.map((el) => [xScale(el[0]), yScale(el[1]), el[2]])
      .filter((el) => reactiveFilters.includes(colors[el[2]]));
    $: reactiveDelaunay = Delaunay.from(reactivePointsScaled);
    $: reactiveVoronoi = reactiveDelaunay.voronoi([0, 0, width, height]);

    $: {
        reactiveXTicks = [];
        reactiveUnit = Math.round((xDomain[1] - xDomain[0]) / xScalefactor);

        for (let i = 1; i < xScalefactor + 1; i++) {
            reactiveXTicks.push(i * reactiveUnit);
        }
    }
  
    $: {
        reactiveYTicks = [];
        reactiveUnit = Math.round((yDomain[1] - yDomain[0]) / yScalefactor);

        for (let i = 1; i < yScalefactor + 1; i++) {
            reactiveYTicks.push(i * reactiveUnit); // TODO make adjustable and account for optional %
        }
    }
  
    // Updates filter array according to input
    $: reactiveFilter = (color) => {
      if (reactiveFilters.includes(color)) reactiveFilters = reactiveFilters.filter((col) => col !== color);
      else reactiveFilters = [...reactiveFilters, color];
    };
  </script>
  
  <div class="scatter_plot_container">
    <svg {width} {height} viewBox="0 0 {width} {height}"
      on:mouseout="{() => {dotInfo = null; selectedDot = null}}"
      on:blur="{() => {dotInfo = null; selectedDot = null}}"
    >
      <g class="y-axis" transform="translate({marginLeft}, 0)">
        <path class="domain" stroke="black" d="M{insetLeft}, {marginTop} V{height}"/>
        {#each reactiveYTicks as tick, i}
          <g class="tick" transform="translate(0, {yScale(tick)})">
            <line class="tick-start" x1={insetLeft - 6} x2={insetLeft}/>
            <line class="tick-grid" x1={insetLeft} x2={width - marginLeft - marginRight}/>
            <text x={-marginLeft} y="10">{tick + yFormat}</text>
          </g>
        {/each}
        <text x="-{marginLeft}" y={marginTop-5}>{yLabel}</text>
      </g>
  
      <g class="x-axis" transform="translate(0,{height - marginBottom})">
        <path class="domain" stroke="black" d="M{marginLeft}, 0.5 H{width}"/>
        {#each reactiveXTicks as tick, i}
          <g class="tick" transform="translate({xScale(tick)}, 0)">
            <line class="tick-start" stroke='black' y2='6' />
            <line class="tick-grid" y2="-{height - marginTop}" />
            <text x={-marginLeft} y="20">{tick + xFormat}</text>
          </g>
        {/each}
        <text x={width - marginLeft - marginRight - 40} y={marginBottom}>{xLabel}</text>
      </g>
        
        {#each reactivePointsScaled as dot, i}
          <g class='dot' opacity='1'>
            {#if reactiveFilters.includes(colors[dot[2]])}
              {#if i === selectedDot}
                <circle
                  cx={dot[0]}
                  cy={dot[1]}
                  r={r + 2}
                  stroke={colors[dot[2]]}
                  fill={filled ? colors[dot[2]] : 'none'}
                />
              {:else}
                <circle
                  cx={dot[0]}
                  cy={dot[1]}
                  r={r}
                  stroke={colors[dot[2]]}
                  fill={filled ? colors[dot[2]] : 'none'}
                />
              {/if}
              <path
                  stroke="none"
                  fill-opacity="0"
                  class="voronoi-cell"
                  d={reactiveVoronoi.renderCell(i)}
                  on:mouseover="{(e) => { selectedDot = i; dotInfo = [dot, i, e]; }}"
                  on:focus="{(e) => { selectedDot = i; dotInfo = [dot, i, e]; e.target.classList.add('selectedDot'); }}"
              ></path>
            {/if}
          </g>
        {/each}
    </svg>
  
  <!-- Tooltip -->
    {#if dotInfo}  
      <div class="dot_info" style="left:{dotInfo[2].clientX + 12}px; top:{dotInfo[2].clientY + 12}px; background-color:{tooltipBackground}; color:{tooltipTextColor}">
        <span class="scatter_legend_span" style="background-color: {colors[points[dotInfo[1]][2]]}; height:{width/100}px; width:{width/100}px; " />
        {subsets ? subsets[points[dotInfo[1]][2]] : ''} 
        {x}: {points[dotInfo[1]][0]}, {y}: {points[dotInfo[1]][1]}
      </div>
    {/if}
  
  <!-- Legend/Filters -->
    <section class="scatter_legend" style="width:{width/10}px; font-size: {width/75}px">
      {#if subsets}
      <h1 class="legend_title"><b>Legend</b></h1>
      <h5 class="legend_note">Click to Filter</h5>
        {#each subsets as subset, i}  
          <div class="scatter_legend_info" on:click={() => reactiveFilter(colors[i])}>
            <span class="scatter_legend_span" style="background-color: {colors[i]}; height:{width/50}px; width:{width/50}px; " />
            {subset}
          </div>
        {/each}
      {/if}
    </section>
  </div>
  
  <style>
    svg {
      width: 80%;
      height: 100%;
      height: "intrinsic";
      margin-top: auto;
      margin-bottom: auto;
    }
  
    .scatter_plot_container{
      display: flex;
      justify-content: center;
    }
  
    .scatter_legend{
      color: black;
      margin-top: auto;
      margin-bottom: auto;
    }
  
    .scatter_legend_info{
      display: flex;
      align-items: center;
      justify-content: space-evenly;
    }
  
    .scatter_legend_span{
      border-radius:50%; 
      display: inline-block; 
      margin-top:10px;
    }
  
    .legend_title{
      margin-top: 0;
      margin-bottom: 15px;
      text-decoration-line: underline;
    }
  
    .legend_note{
      margin-top: 0;
      margin-bottom: 0;
    }
  
    .dot_info{
      position:absolute; 
      display:inline;
      margin: 0;
      border-radius: 5px;
      padding: 5px;
      box-shadow: rgba(0, 0, 0, 0.02) 0px 1px 3px 0px, rgba(27, 31, 35, 0.15) 0px 0px 0px 1px;
    }
  
    path {
      fill: "green"
    }
  
    .y-axis {
      font-size: "10px";
      font-family: sans-serif;
      text-anchor: "end";
    }
  
    .x-axis {
      font-size: "10px";
      font-family: sans-serif;
      text-anchor: "end";
    }
  
    .tick {
      opacity: 1;
    }
  
    .tick-start {
      stroke: black;
      stroke-opacity: 1;
    }
  
    .tick-grid {
      stroke: black;
      stroke-opacity: 0.2;
      font-size: "11px";
      color: black;
    }
  
    .tick text {
      fill: black;
      text-anchor: start;
    }
  </style>
export default [
  {
    'id': 'Group A',
    'data': [
      {
        'x': 35,
        'y': 92
      },
      {
        'x': 73,
        'y': 74
      },
      {
        'x': 5,
        'y': 107
      },
      {
        'x': 84,
        'y': 36
      },
      {
        'x': 28,
        'y': 99
      },
      {
        'x': 80,
        'y': 8
      },
      {
        'x': 47,
        'y': 2
      },
      {
        'x': 91,
        'y': 104
      },
      {
        'x': 24,
        'y': 15
      },
      {
        'x': 36,
        'y': 1
      },
      {
        'x': 12,
        'y': 46
      },
      {
        'x': 78,
        'y': 77
      },
      {
        'x': 59,
        'y': 103
      },
      {
        'x': 39,
        'y': 71
      },
      {
        'x': 61,
        'y': 6
      },
      {
        'x': 35,
        'y': 112
      },
      {
        'x': 89,
        'y': 40
      },
      {
        'x': 68,
        'y': 104
      },
      {
        'x': 37,
        'y': 3
      },
      {
        'x': 13,
        'y': 83
      },
      {
        'x': 39,
        'y': 94
      },
      {
        'x': 35,
        'y': 47
      },
      {
        'x': 65,
        'y': 62
      },
      {
        'x': 52,
        'y': 77
      },
      {
        'x': 68,
        'y': 50
      },
      {
        'x': 99,
        'y': 7
      },
      {
        'x': 46,
        'y': 98
      },
      {
        'x': 88,
        'y': 1
      },
      {
        'x': 59,
        'y': 90
      },
      {
        'x': 19,
        'y': 40
      },
      {
        'x': 33,
        'y': 42
      },
      {
        'x': 61,
        'y': 26
      },
      {
        'x': 74,
        'y': 31
      },
      {
        'x': 56,
        'y': 95
      },
      {
        'x': 45,
        'y': 58
      },
      {
        'x': 20,
        'y': 57
      },
      {
        'x': 41,
        'y': 49
      },
      {
        'x': 57,
        'y': 17
      },
      {
        'x': 12,
        'y': 13
      },
      {
        'x': 90,
        'y': 87
      },
      {
        'x': 64,
        'y': 21
      },
      {
        'x': 31,
        'y': 45
      },
      {
        'x': 4,
        'y': 113
      },
      {
        'x': 2,
        'y': 48
      },
      {
        'x': 39,
        'y': 63
      },
      {
        'x': 73,
        'y': 55
      },
      {
        'x': 68,
        'y': 118
      },
      {
        'x': 68,
        'y': 41
      },
      {
        'x': 94,
        'y': 24
      },
      {
        'x': 62,
        'y': 21
      }
    ]
  },
  {
    'id': 'Group B',
    'data': [
      {
        'x': 69,
        'y': 110
      },
      {
        'x': 30,
        'y': 48
      },
      {
        'x': 70,
        'y': 116
      },
      {
        'x': 68,
        'y': 13
      },
      {
        'x': 13,
        'y': 62
      },
      {
        'x': 20,
        'y': 96
      },
      {
        'x': 3,
        'y': 70
      },
      {
        'x': 32,
        'y': 5
      },
      {
        'x': 24,
        'y': 91
      },
      {
        'x': 83,
        'y': 52
      },
      {
        'x': 66,
        'y': 85
      },
      {
        'x': 67,
        'y': 60
      },
      {
        'x': 70,
        'y': 18
      },
      {
        'x': 84,
        'y': 105
      },
      {
        'x': 31,
        'y': 50
      },
      {
        'x': 94,
        'y': 99
      },
      {
        'x': 77,
        'y': 34
      },
      {
        'x': 25,
        'y': 43
      },
      {
        'x': 39,
        'y': 101
      },
      {
        'x': 62,
        'y': 24
      },
      {
        'x': 62,
        'y': 15
      },
      {
        'x': 31,
        'y': 104
      },
      {
        'x': 64,
        'y': 100
      },
      {
        'x': 85,
        'y': 77
      },
      {
        'x': 89,
        'y': 43
      },
      {
        'x': 40,
        'y': 26
      },
      {
        'x': 82,
        'y': 7
      },
      {
        'x': 84,
        'y': 59
      },
      {
        'x': 61,
        'y': 94
      },
      {
        'x': 82,
        'y': 57
      },
      {
        'x': 9,
        'y': 64
      },
      {
        'x': 86,
        'y': 108
      },
      {
        'x': 75,
        'y': 18
      },
      {
        'x': 26,
        'y': 59
      },
      {
        'x': 48,
        'y': 101
      },
      {
        'x': 16,
        'y': 57
      },
      {
        'x': 86,
        'y': 78
      },
      {
        'x': 50,
        'y': 70
      },
      {
        'x': 84,
        'y': 29
      },
      {
        'x': 67,
        'y': 79
      },
      {
        'x': 23,
        'y': 21
      },
      {
        'x': 22,
        'y': 69
      },
      {
        'x': 7,
        'y': 18
      },
      {
        'x': 36,
        'y': 0
      },
      {
        'x': 29,
        'y': 38
      },
      {
        'x': 100,
        'y': 24
      },
      {
        'x': 74,
        'y': 97
      },
      {
        'x': 65,
        'y': 61
      },
      {
        'x': 43,
        'y': 32
      },
      {
        'x': 3,
        'y': 55
      }
    ]
  },
  {
    'id': 'Group C',
    'data': [
      {
        'x': 6,
        'y': 106
      },
      {
        'x': 3,
        'y': 21
      },
      {
        'x': 89,
        'y': 37
      },
      {
        'x': 0,
        'y': 27
      },
      {
        'x': 34,
        'y': 48
      },
      {
        'x': 86,
        'y': 8
      },
      {
        'x': 80,
        'y': 25
      },
      {
        'x': 14,
        'y': 115
      },
      {
        'x': 65,
        'y': 93
      },
      {
        'x': 47,
        'y': 6
      },
      {
        'x': 72,
        'y': 77
      },
      {
        'x': 65,
        'y': 49
      },
      {
        'x': 45,
        'y': 81
      },
      {
        'x': 89,
        'y': 50
      },
      {
        'x': 29,
        'y': 89
      },
      {
        'x': 7,
        'y': 15
      },
      {
        'x': 95,
        'y': 112
      },
      {
        'x': 55,
        'y': 110
      },
      {
        'x': 48,
        'y': 105
      },
      {
        'x': 59,
        'y': 38
      },
      {
        'x': 92,
        'y': 61
      },
      {
        'x': 82,
        'y': 114
      },
      {
        'x': 76,
        'y': 53
      },
      {
        'x': 21,
        'y': 10
      },
      {
        'x': 66,
        'y': 28
      },
      {
        'x': 16,
        'y': 91
      },
      {
        'x': 1,
        'y': 60
      },
      {
        'x': 32,
        'y': 102
      },
      {
        'x': 24,
        'y': 33
      },
      {
        'x': 14,
        'y': 87
      },
      {
        'x': 30,
        'y': 44
      },
      {
        'x': 38,
        'y': 32
      },
      {
        'x': 11,
        'y': 29
      },
      {
        'x': 92,
        'y': 30
      },
      {
        'x': 12,
        'y': 23
      },
      {
        'x': 34,
        'y': 32
      },
      {
        'x': 70,
        'y': 64
      },
      {
        'x': 72,
        'y': 71
      },
      {
        'x': 93,
        'y': 14
      },
      {
        'x': 71,
        'y': 103
      },
      {
        'x': 56,
        'y': 30
      },
      {
        'x': 26,
        'y': 33
      },
      {
        'x': 84,
        'y': 110
      },
      {
        'x': 10,
        'y': 75
      },
      {
        'x': 95,
        'y': 21
      },
      {
        'x': 12,
        'y': 120
      },
      {
        'x': 21,
        'y': 65
      },
      {
        'x': 73,
        'y': 13
      },
      {
        'x': 81,
        'y': 56
      },
      {
        'x': 86,
        'y': 71
      }
    ]
  },
  {
    'id': 'Group D',
    'data': [
      {
        'x': 79,
        'y': 75
      },
      {
        'x': 47,
        'y': 6
      },
      {
        'x': 8,
        'y': 4
      },
      {
        'x': 77,
        'y': 7
      },
      {
        'x': 51,
        'y': 34
      },
      {
        'x': 86,
        'y': 102
      },
      {
        'x': 40,
        'y': 55
      },
      {
        'x': 96,
        'y': 94
      },
      {
        'x': 77,
        'y': 104
      },
      {
        'x': 26,
        'y': 51
      },
      {
        'x': 97,
        'y': 60
      },
      {
        'x': 97,
        'y': 17
      },
      {
        'x': 0,
        'y': 86
      },
      {
        'x': 52,
        'y': 5
      },
      {
        'x': 30,
        'y': 78
      },
      {
        'x': 22,
        'y': 93
      },
      {
        'x': 56,
        'y': 19
      },
      {
        'x': 62,
        'y': 80
      },
      {
        'x': 82,
        'y': 62
      },
      {
        'x': 48,
        'y': 22
      },
      {
        'x': 34,
        'y': 105
      },
      {
        'x': 67,
        'y': 95
      },
      {
        'x': 22,
        'y': 0
      },
      {
        'x': 9,
        'y': 100
      },
      {
        'x': 84,
        'y': 10
      },
      {
        'x': 84,
        'y': 40
      },
      {
        'x': 15,
        'y': 34
      },
      {
        'x': 18,
        'y': 117
      },
      {
        'x': 92,
        'y': 29
      },
      {
        'x': 6,
        'y': 73
      },
      {
        'x': 14,
        'y': 44
      },
      {
        'x': 39,
        'y': 4
      },
      {
        'x': 47,
        'y': 36
      },
      {
        'x': 21,
        'y': 73
      },
      {
        'x': 29,
        'y': 50
      },
      {
        'x': 94,
        'y': 27
      },
      {
        'x': 59,
        'y': 69
      },
      {
        'x': 54,
        'y': 108
      },
      {
        'x': 49,
        'y': 35
      },
      {
        'x': 82,
        'y': 3
      },
      {
        'x': 74,
        'y': 55
      },
      {
        'x': 39,
        'y': 29
      },
      {
        'x': 61,
        'y': 45
      },
      {
        'x': 75,
        'y': 36
      },
      {
        'x': 17,
        'y': 16
      },
      {
        'x': 28,
        'y': 65
      },
      {
        'x': 99,
        'y': 102
      },
      {
        'x': 43,
        'y': 28
      },
      {
        'x': 8,
        'y': 36
      },
      {
        'x': 91,
        'y': 3
      }
    ]
  },
  {
    'id': 'Group E',
    'data': [
      {
        'x': 17,
        'y': 19
      },
      {
        'x': 65,
        'y': 50
      },
      {
        'x': 79,
        'y': 76
      },
      {
        'x': 18,
        'y': 44
      },
      {
        'x': 73,
        'y': 23
      },
      {
        'x': 23,
        'y': 35
      },
      {
        'x': 95,
        'y': 78
      },
      {
        'x': 79,
        'y': 18
      },
      {
        'x': 35,
        'y': 87
      },
      {
        'x': 34,
        'y': 36
      },
      {
        'x': 51,
        'y': 67
      },
      {
        'x': 1,
        'y': 80
      },
      {
        'x': 6,
        'y': 32
      },
      {
        'x': 83,
        'y': 43
      },
      {
        'x': 82,
        'y': 11
      },
      {
        'x': 78,
        'y': 104
      },
      {
        'x': 23,
        'y': 2
      },
      {
        'x': 92,
        'y': 29
      },
      {
        'x': 23,
        'y': 22
      },
      {
        'x': 67,
        'y': 75
      },
      {
        'x': 11,
        'y': 12
      },
      {
        'x': 95,
        'y': 75
      },
      {
        'x': 66,
        'y': 48
      },
      {
        'x': 95,
        'y': 56
      },
      {
        'x': 14,
        'y': 37
      },
      {
        'x': 12,
        'y': 22
      },
      {
        'x': 22,
        'y': 30
      },
      {
        'x': 95,
        'y': 60
      },
      {
        'x': 27,
        'y': 20
      },
      {
        'x': 54,
        'y': 92
      },
      {
        'x': 27,
        'y': 43
      },
      {
        'x': 23,
        'y': 51
      },
      {
        'x': 84,
        'y': 95
      },
      {
        'x': 16,
        'y': 115
      },
      {
        'x': 41,
        'y': 43
      },
      {
        'x': 97,
        'y': 38
      },
      {
        'x': 77,
        'y': 45
      },
      {
        'x': 92,
        'y': 5
      },
      {
        'x': 4,
        'y': 118
      },
      {
        'x': 22,
        'y': 102
      },
      {
        'x': 75,
        'y': 101
      },
      {
        'x': 72,
        'y': 66
      },
      {
        'x': 94,
        'y': 83
      },
      {
        'x': 41,
        'y': 113
      },
      {
        'x': 13,
        'y': 82
      },
      {
        'x': 80,
        'y': 32
      },
      {
        'x': 15,
        'y': 115
      },
      {
        'x': 6,
        'y': 111
      },
      {
        'x': 97,
        'y': 93
      },
      {
        'x': 75,
        'y': 34
      }
    ]
  }
];
const data = [
    {
    'id': 'group A',
    'data': [
              { xKey: "xValue1",
                yKey: "yValue1"
              },
              { xKey: "xValue2",
                yKey: "yValue2"
              },
              //insert additional x y data sets
            ]
    },
    {
    'id': 'group B',
    'data': [
              { xKey: "xValue1",
                yKey: "yValue1"
              },
              { xKey: "xValue2",
                yKey: "yValue2"
              },
              //insert additional x y data sets
            ]
    }
    //insert additional object groups
  ];

Properties