<script lang="ts"> //@ts-nocheck import { line, curveLinear, Delaunay, range, scaleLinear } from 'd3' export let data const marginTop = 30 // the top margin, in pixels const marginRight = 30 // the right margin, in pixels const marginBottom = 30 // the bottom margin, in pixels const marginLeft = 100 // the left margin, in pixels const inset = 0 // inset the default range, in pixels const width = 600 // the outer width of the chart, in pixels const height = 400 // the outer height of the chart, in pixels const xLabel = '' // a label for the y-axis const yLabel = 'Résultat net' // a label for the y-axis const xFormat = '' // a format specifier string for the y-axis const yFormat = '€' // a format specifier string for the y-axis const horizontalGrid = true // show horizontal grid lines const verticalGrid = true // show vertical grid lines const colors = ['#b81111', '#ffbf00'] // fill color for dots && number of colors in fill array MUST match number of subsets in data const showDots = false // whether dots should be displayed const dotsFilled = true // whether dots should be filled or outlined const r = 0 // (fixed) radius of dots, in pixels const strokeWidth = 1 // stroke width of line, in pixels const strokeOpacity = [1] // stroke opacity of line const tooltipBackground = 'white' // background color of tooltip const tooltipTextColor = 'black' // 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 / 180 //y-axis number of values const yScalefactor = height / 40 //y-axis number of values const curve = curveLinear // method of interpolation between points const xType = scaleLinear // type of x-scale const insetTop = 50 // 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: string, y: string, dotInfo, lines, xVals = [], yVals = [], points = [], subsets = [], colorVals = [] let I, xScale, gaps, cleanData, xDomain, yDomain, yScale, niceY, chartLine, pointsScaled, delaunayGrid, voronoiGrid, xTicks, xTicksFormatted, yTicks $: { // For a single set of data xVals = [] yVals = [] if (!('data' in data[0])) { x = Object.keys(data[0])[0] y = Object.keys(data[0])[1] xVals = data.map((el: Record<string, any>) => el[x]) yVals = data.map((el: Record<string, any>) => el[y]) colorVals = data.map((_) => 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) }) } I = range(xVals.length) gaps = (_d, i) => !isNaN(xVals[i]) && !isNaN(yVals[i]) cleanData = points.map(gaps) xDomain = [xVals[0], xVals[xVals.length - 1]] yDomain = [Math.min(...yVals), Math.max(...yVals)] xScale = xType(xDomain, xRange) yScale = yType(yDomain, yRange) niceY = scaleLinear() .domain([Math.min(...yVals), Math.max(...yVals)]) .nice() 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)) }) console.log('lines', lines) pointsScaled = points.map((el) => [ xScale(el.x), yScale(el.y), el.color, ]) delaunayGrid = Delaunay.from(pointsScaled) voronoiGrid = delaunayGrid.voronoi([0, 0, width, height]) xTicks = xScale.ticks(xScalefactor) xTicksFormatted = xTicks.map((el) => el) 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} 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 && false} <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} stroke={colors[points[dotInfo[1]].color]} fill={dotsFilled} /> {:else} <path class="line" fill="none" stroke={colors[i]} d={subsetLine} stroke-opacity={strokeOpacity[i]} 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} <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="-10" y="5">{tick + yFormat}</text> </g> {/each} <text text-anchor="middle" x="0" 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 + insetTop + marginTop} /> {/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])} /> {/each} </svg> </div> <!-- Tooltip --> {#if dotInfo} <div class="tooltip" style="position:absolute; left:{dotInfo[2].pageX + 12}px; top:{dotInfo[2].pageY + 12}px; pointer-events:none; background-color:{tooltipBackground}; color:{tooltipTextColor}"> {points[dotInfo[1]].x}: {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 text { font-size: 80%; fill: white !important; } .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; } .y-axis .tick text { text-anchor: end !important; } @media (prefers-color-scheme: dark) { .y-axis text { fill: white; } .x-axis text { fill: white; } .tick-start { stroke: white; } .tick-grid { stroke: white; } .tick text { fill: white; } .domain { stroke: white; } } .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; opacity: 1; } </style>