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

Line Chart

Chart Data Schema

<script>
  import { line, curveLinear, Delaunay, range, scaleLinear, scaleUtc } from 'd3';
  import data from './line-data';
  
  const marginTop = $ChartDocs[0].value; // the top margin, in pixels
  const marginRight = $ChartDocs[1].value; // the right margin, in pixels
  const marginBottom = $ChartDocs[2].value; // the bottom margin, in pixels
  const marginLeft = $ChartDocs[3].value; // the left margin, in pixels
  const inset = $ChartDocs[4].value; // inset the default range, in pixels
  const width = $ChartDocs[5].value; // the outer width of the chart, in pixels
  const height = $ChartDocs[6].value; // the outer height of the chart, in pixels
  const xLabel = $ChartDocs[7].value; // a label for the y-axis
  const yLabel = $ChartDocs[8].value; // a label for the y-axis
  const xFormat = $ChartDocs[9].value; // a format specifier string for the y-axis
  const yFormat = $ChartDocs[10].value; // a format specifier string for the y-axis
  const horizontalGrid = $ChartDocs[11].value; // show horizontal grid lines
  const verticalGrid = $ChartDocs[12].value; // show vertical grid lines
  const colors = $ChartDocs[13].value; // fill color for dots && number of colors in fill array MUST match number of subsets in data
  const showDots = $ChartDocs[14].value; // whether dots should be displayed
  const dotsFilled = $ChartDocs[15].value; // whether dots should be filled or outlined
  const r = $ChartDocs[16].value; // (fixed) radius of dots, in pixels
  const strokeWidth = $ChartDocs[17].value; // stroke width of line, in pixels
  const strokeOpacity = $ChartDocs[18].value; // stroke opacity of line
  const tooltipBackground = $ChartDocs[19].value; // background color of tooltip
  const tooltipTextColor = $ChartDocs[20].value; // text color of tooltip
  const strokeLinecap = 'round'; // stroke line cap of the line
  const strokeLinejoin = 'round'; // stroke line join of the line
  const xScalefactor = width / 80; //y-axis number of values
  const yScalefactor = height / 40; //y-axis number of values
  const curve = curveLinear; // method of interpolation between points
  const xType = scaleUtc; // type of x-scale
  const insetTop = inset; // inset from top
  const insetRight = inset; // inset from right
  const insetBottom = inset; // inset fro bottom
  const insetLeft = inset; // inset from left
  const xRange = [marginLeft + insetLeft, width - marginRight - insetRight]; // [left, right]
  const yType = scaleLinear; // type of y-scale
  const yRange = [height - marginBottom - insetBottom, marginTop + insetTop]; // [bottom, top]

  let x, y, dotInfo, lines, xVals = [], yVals = [], points = [], subsets = [], colorVals = [];
  
  // For a single set of data
  if (!('data' in data[0])) {
    x = Object.keys(data[0])[0];
    y = Object.keys(data[0])[1];
    xVals = data.map((el) => el[x]);
    yVals = data.map((el) => el[y]);
    colorVals = data.map((el) => 0);
    points = data.map((el) => ({
      x: el[x],
      y: el[y],
      color: 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];
    data.forEach((subset, i) => {
    subset.data.forEach((coordinate) => {
      xVals.push(coordinate[x]);
      yVals.push(coordinate[y]);
      colorVals.push(i);
      points.push(
        { 
          x: coordinate[x],
          y: coordinate[y],
          color: i
        });
    });
    subsets.push(subset.id);
  });
  }

  const I = range(xVals.length);
  const gaps = (d, i) => !isNaN(xVals[i]) && !isNaN(yVals[i]);
  const cleanData = points.map(gaps);

  const xDomain = [xVals[0], xVals[xVals.length - 1]];
  const yDomain = [0, Math.max(...yVals)];
  const xScale = xType(xDomain, xRange);
  const yScale = yType(yDomain, yRange);
  const niceY = scaleLinear().domain([0, Math.max(...yVals)]).nice();

  const chartLine = line()
    .defined(i => cleanData[i])
    .curve(curve)
    .x(i => xScale(xVals[i]))
    .y(i => yScale(yVals[i]));

  $: {
    lines = [];
    colors.forEach((color, j) => {
      const filteredI = I.filter((el, i) => colorVals[i] === j);
      lines.push(chartLine(filteredI));
    });
  }

  const pointsScaled = points.map((el) => [xScale(el.x), yScale(el.y), el.color]);
  const delaunayGrid = Delaunay.from(pointsScaled);
  const voronoiGrid = delaunayGrid.voronoi([0, 0, width, height]);
  
  const  xTicks = xScale.ticks(xScalefactor);
  const  xTicksFormatted = xTicks.map((el) => el.getFullYear());
  const  yTicks = niceY.ticks(yScalefactor);
</script>

<div class="chart-container">
  <svg {width} {height} viewBox="0 0 {width} {height}"
    cursor='crosshair'
    on:mouseout="{() => dotInfo = null}"
    on:blur="{() => dotInfo = null}"
  >
    <!-- Dots (if enabled) -->
    {#if showDots && !dotInfo}
      {#each I as i}
        <g class='dot' pointer-events='none'>
          <circle
            cx={xScale(xVals[i])}
            cy={yScale(yVals[i])}
            r={r}
            stroke={colors[colorVals[i]]}
            fill={dotsFilled ? colors[colorVals[i]] : 'none'}
          />
        </g>
      {/each}
    {/if}
    <!-- Chart lines -->
    {#each lines as subsetLine, i}
    <g class='chartlines' pointer-events='none'>
      {#if dotInfo}
        <path class="line" fill='none' stroke-opacity={points[dotInfo[1]].color === i ? '1' : '0.1'} stroke={colors[i]} d={subsetLine} stroke-width={strokeWidth} stroke-linecap={strokeLinecap} stroke-linejoin={strokeLinejoin}/>
        <circle cx={xScale(points[dotInfo[1]].x)} cy={yScale(points[dotInfo[1]].y)} r={r} stroke={colors[points[dotInfo[1]].color]} fill={dotsFilled} />
      {:else}
        <path class="line" fill='none' stroke={colors[i]} d={subsetLine}
          stroke-opacity={strokeOpacity} stroke-width={strokeWidth} stroke-linecap={strokeLinecap} stroke-linejoin={strokeLinejoin} />
      {/if}
    </g>
    {/each}
    
    <!-- Y-axis and horizontal grid lines -->
    <g class="y-axis" transform="translate({marginLeft}, 0)" pointer-events='none'>
      <path class="domain" stroke="black" d="M{insetLeft}, {marginTop} V{height - marginBottom + 6}"/>
      {#each yTicks as tick, i}
        <g class="tick" transform="translate(0, {yScale(tick)})">
          <line class="tick-start" x1={insetLeft - 6} x2={insetLeft}/>
          {#if horizontalGrid}
            <line class="tick-grid" x1={insetLeft} x2={width - marginLeft - marginRight}/>
          {/if}
          <text  x="-{marginLeft}" y="5">{tick + yFormat}</text>
        </g>
      {/each}
      <text x="-{marginLeft}" y={marginTop - 10}>{yLabel}</text>
    </g>
    <!-- X-axis and vertical grid lines -->
    <g class="x-axis" transform="translate(0,{height - marginBottom - insetBottom})" pointer-events='none'>
      <path class="domain" stroke="black" d="M{marginLeft},0.5 H{width - marginRight}"/>
      {#each xTicks as tick, i}
        <g class="tick" transform="translate({xScale(tick)}, 0)">
          <line class="tick-start" stroke='black' y2='6' />
          {#if verticalGrid}
            <line class="tick-grid" y2={-height + 70} />
          {/if}
          <text font-size='8px' x={-marginLeft/4} y="20">{xTicksFormatted[i] + xFormat}</text>
        </g>
      {/each}
      <text x={width - marginLeft - marginRight - 40} y={marginBottom}>{xLabel}</text>
    </g>

    {#each pointsScaled as point, i}
      <path
        stroke="none"
        fill-opacity="0"
        class="voronoi-cell"
        d={voronoiGrid.renderCell(i)}
        on:mouseover="{(e) => dotInfo = [point, i, e] }"
        on:focus="{(e) => dotInfo = [point, i, e] }"
      ></path>
    {/each}
  </svg>
</div>
<!-- Tooltip -->
{#if dotInfo}
  <div class="tooltip" style='position:absolute; left:{dotInfo[2].clientX + 12}px; top:{dotInfo[2].clientY + 12}px; pointer-events:none; background-color:{tooltipBackground}; color:{tooltipTextColor}'>
    {subsets ? subsets[points[dotInfo[1]].color] : ''}:  
    {points[dotInfo[1]].x.getFullYear()}: {points[dotInfo[1]].y.toFixed(2)}{yFormat}
  </div>
{/if}

<style>
  .chart-container {
    justify-content: center;
    align-items: center;
    margin-top: 50px;
    margin-left: 8
    0px;
  }
  svg {
    max-width: 100%;
    height: auto;
    height: "intrinsic";
    margin: auto;
  }
  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;
  }

  .tooltip{
    border-radius: 5px;
    padding: 5px;
    box-shadow: rgba(0, 0, 0, 0.4) 0px 2px 4px, rgba(0, 0, 0, 0.3) 0px 7px 13px -3px, rgba(0, 0, 0, 0.2) 0px -3px 0px inset;
  }
</style>
const csvNortheast = `Year,Population
1920,29.662053
1930,34.427091
1940,35.976777
1950,39.477986
1960,44.677819
1970,49.040703
1980,49.135283
1990,50.809229
2000,53.594378
2010,55.317240
2020,57.609148`;

const csvMidwest = `Year,Population
1920,34.019792
1930,38.594100
1940,40.143332
1950,44.460762
1960,51.619139
1970,56.571663
1980,58.865670
1990,59.668632
2000,64.392776
2010,66.927001
2020,68.985454`;

const csvSouth = `Year,Population
1920,33.125803
1930,37.857633
1940,41.665901
1950,47.197088
1960,54.973113
1970,62.795367
1980,75.372362
1990,85.445930
2000,100.236820
2010,114.555744
2020,126.266107`;

const csvWest = `Year,Population
1920,9.213920
1930,12.323836
1940,14.379119
1950,20.189962
1960,28.053104
1970,34.804193
1980,43.172490
1990,52.786082
2000,63.197932
2010,71.945553
2020,78.588572`;

function csvConvert(csv) {
  return csv.split('\n').slice(1).map(str => {
    const [date, population] = str.split(',')
    .map((el) => (el > 1900 ? new Date(el, 0) : parseFloat(el)));
    return { date, population };
  });
}
const northeast = csvConvert(csvNortheast);
const midwest = csvConvert(csvMidwest);
const south = csvConvert(csvSouth);
const west = csvConvert(csvWest);

export default [
  {
    id: 'Northeast',
    data: northeast
  },
  {
    id: 'Midwest',
    data: midwest
  },
  {
    id: 'South',
    data: south
  },
  {
    id: 'West',
    data: west
  }
]
const firstDataSet = 
  "title_X,title_Y
  x-value, y-value
  x-value, y-value
  x-value, y-value
  //insert additional csv data ..."

const secondDataSet = 
  "title_X,title_Y
  x-value, y-value
  x-value, y-value
  x-value, y-value
  //insert additional csv data ..."
 
const thirdDataSet = 
  "title_X,title_Y
  x-value, y-value
  x-value, y-value
  x-value, y-value
  //insert additional csv data ..."
 
export { firstDataSet, secondDataSet, thirdDataSet };

Properties