<script>
import { scaleLinear, Delaunay } from 'd3';
import data from './scatter-data'
const r = $ChartDocs[0].value;
const marginTop = $ChartDocs[1].value;
const marginRight = $ChartDocs[2].value;
const marginBottom = $ChartDocs[3].value;
const marginLeft = $ChartDocs[4].value;
const inset = $ChartDocs[5].value;
const insetTop = inset;
const insetRight = inset;
const insetBottom = inset;
const insetLeft = inset;
const width = $ChartDocs[6].value;
const height = $ChartDocs[7].value;
const xLabel = $ChartDocs[8].value;
const yLabel = $ChartDocs[9].value;
const xFormat = $ChartDocs[10].value;
const yFormat = $ChartDocs[11].value;
const xScalefactor = $ChartDocs[12].value;
const yScalefactor = $ChartDocs[13].value;
const colors = $ChartDocs[14].value;
const filled = $ChartDocs[15].value;
const tooltipBackground = $ChartDocs[16].value;
const tooltipTextColor = $ChartDocs[17].value;
const xType = scaleLinear;
const yType = scaleLinear;
const xRange = [marginLeft + insetLeft, width - marginRight - insetRight];
const yRange = [height - marginBottom - insetBottom, marginTop + insetTop];
let x, y, selectedDot, dotInfo, subsets, xVals = [], yVals = [], points = [];
let reactiveUnit, reactiveXTicks, reactiveYTicks;
$: reactiveFilters = [...colors];
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]);
}
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);
}
}
$: 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"
},
]
},
{
'id': 'group B',
'data': [
{ xKey: "xValue1",
yKey: "yValue1"
},
{ xKey: "xValue2",
yKey: "yValue2"
},
]
}
];