charts/bubble.mjs (314 lines of code) (raw):
import dataTools from "./shared/dataTools"
import Tooltip from "./shared/tooltip"
import ColorScale from "./shared/colorscale"
import { numberFormat, mustache, mobileCheck, bufferize, validateString, dodge, wrap, getMaxDuplicate, getURLParams, isNumber, getLabelFromColumn } from './shared/toolbelt';
// import { addLabels } from "./shared/labels"
import { addLabel, clickLogging } from './shared/arrows'
import { makeTopLinedAxis } from './shared/makeAxis'
// https://svelte.dev/repl/e4cd6985a78a4d169fe5c54977a4336c?version=4.0.5
// // https://www.chartfleau.com/tutorials/d3swarm
export default class Scatterplot {
constructor(settings) {
this.settings = settings
this.init()
}
init() {
if (this.settings.tooltip != "") {
this.tooltip = new Tooltip(this.settings.tooltip)
}
this.render()
}
render() {
let { modules,
height,
width,
rowHeight,
isMobile,
colors,
datum,
keys,
data,
title,
subtitle,
source,
tooltip,
marginleft,
margintop,
marginbottom,
marginright,
trendline,
enableShowMore,
aria,
colorScheme,
labels,
userkey,
type,
dropdown,
xColumn,
yColumn,
zColumn,
xAxisLabel,
yAxisLabel,
minY,
minX,
maxX,
maxY,
hideKey,
dataLabels,
zMin,
zMax,
xScale,
yScale,
zScale,
zLabel,
xFormat,
opacity,
parseTime,
groupBy,
defaultRadius,
beeswarm,
columns
} = this.settings
datum = JSON.parse(JSON.stringify(data));
console.log("settings",this.settings)
const $tooltip = (this.tooltip) ? this.tooltip : false
d3.select("#graphicContainer svg").remove();
const chartKey = d3.select("#chartKey")
chartKey.html("")
isMobile = mobileCheck()
width = document.querySelector("#graphicContainer").getBoundingClientRect().width
height = isMobile ? width * 1.7 : width * 0.5;
if (!groupBy) {
height = datum.length * rowHeight + margintop + marginbottom
}
// svgHeight = datum.length * rowHeight + margintop + marginbottom
datum.forEach(function(d) {
if (xFormat.date) {
d[xColumn] = parseTime(d[xColumn])
}
})
// Bufferize now takes a percetange as the third argument
let extent = d3.extent(datum.map(d => d[xColumn]))
let buffer = bufferize(extent[0], extent[1])
console.log("extent", extent, "buffer",buffer, "minX", minX, isNaN(minX))
minX = (isNumber(minX)) ? minX : buffer[0]
maxX = (isNumber(maxX)) ? maxX : buffer[1]
const xRange = [minX, maxX]
console.log("xRange", xRange)
let yRange = d3.extent(datum.map(d => d[yColumn]))
if (yScale == "scaleBand") {
yRange = Array.from(new Set(datum.map(d => d[yColumn])))
}
console.log("yRange", yRange)
const zRange = (zColumn in datum[0]) ? d3.extent(datum.map(d => d[zColumn])) : null
let keyData = []
if (groupBy) {
keyData = Array.from(new Set(datum.map(d => d[groupBy])));
}
else {
keyData = [xColumn];
}
console.log("keyData", keyData)
let cats = []
if (beeswarm) {
cats = Array.from(new Set(datum.map(d => d[yColumn])));
const duplicates = getMaxDuplicate(datum, yColumn, xColumn)
console.log("duplicates", duplicates)
height = cats.length * duplicates * ( defaultRadius * 2 )
}
const svg = d3.select("#graphicContainer").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${marginleft}, ${margintop})`);
const svg2 = d3.select("#graphicContainer svg")
console.log("keys", keys)
colors = new ColorScale()
const keyColor = dataTools.getKeysColors({
keys: keyData,
userKey: userkey,
option: { colorScheme : colorScheme }
})
colors.set(keyColor.keys, keyColor.colors)
if (!hideKey) (
keyData.forEach((key, i) => {
const keyDiv = chartKey
.append("div")
.attr("class", "keyDiv")
keyDiv
.append("span")
.attr("class", "keyCircle")
.style("background-color", () => colors.get(key))
.style("opacity", opacity / 100)
keyDiv
.append("span")
.attr("class", "keyText")
.text(key) // getLabelFromColumn(columns, key)
})
)
const labelsXY = []
datum.forEach((d) => {
if ("label" in d) {
if (d.label === "TRUE") {
labelsXY.push(d)
}
}
})
const x = d3[xScale]()
.range([ zMax, width - marginright - marginleft - zMax ]) // .domain(bufferize(xMin,xMax))
.domain(xRange)
const xLabel = d3[xScale]()
.range([ zMax, width - marginright - marginleft - zMax ]) // .domain(bufferize(xMin,xMax))
.domain(datum.map(d => d[xColumn]))
let xAxis = makeTopLinedAxis(width - marginright - marginleft - zMax, height, margintop, marginbottom, xFormat, x)
svg
.append("g")
.attr("class", "x")
.attr("transform", "translate(0," + 0 + ")")
.call(xAxis)
.style("stroke-dasharray", "2 2")
const y = d3[yScale]()
.range([ margintop, height - margintop - marginbottom])
.domain(datum.map(d => d[yColumn]))
// console.log(y("Brazil"))
const z = (zRange != null) ? d3[zScale]()
.domain(zRange)
.range([zMin, zMax]) : null
datum.forEach(d => {
d.x = x(d[xColumn])
d.y = y(d[yColumn])
})
if (beeswarm) {
for (const cat of cats) {
let targs = datum.filter(d => d[yColumn] == cat)
let originY = targs[0].y
targs = dodge(targs, defaultRadius * 2)
for (var i = 0; i < targs.length; i++) {
targs[i].y = targs[i].y + (originY)
}
}
}
console.log("datum", datum)
svg.append('g')
.selectAll("dot")
.data(datum)
.enter()
.append("circle")
.attr("cx", (d) => {
return d.x
})
.attr("cy", (d) => {
return d.y + (y.bandwidth() / 2)
})
.attr("r", (d) => {
return (zRange) ? z(d[zColumn]) : defaultRadius
})
.style("fill", (d) => {
if (groupBy) {
return colors.get(d[groupBy])
}
else {
return colors.get(xColumn)
}
})
.style("opacity", opacity / 100)
if (dataLabels) {
svg.append('g')
.selectAll("dotLabels")
.data(datum)
.enter()
.append("text")
.attr("x", (d) => {
let radius = (zRange)? z(d[zColumn]) : defaultRadius
return (d.x + radius * 2)
})
.attr("y", (d) => {
return d.y + (y.bandwidth() / 2) + 4
})
.text((d) => d[xColumn])
}
svg.append("g")
.attr("class","axis y")
.call(d3.axisLeft(y))
.selectAll(".tick text")
.attr("text-anchor", "end")
.style("font-size", "12px")
.call(wrap, marginleft);
svg.selectAll(".domain").remove()
svg.selectAll("rect")
.data(d => y.domain())
.enter()
.append("rect")
.attr("x", 0)
.attr("y", d => y(d) + (y.bandwidth() / 4))
.attr("height", y.bandwidth() / 2)
.attr("width", 1)
.attr("fill", "#767676")
if ($tooltip) {
$tooltip.bindEvents(
d3.selectAll("circle"),
width,
height
)
}
if (xAxisLabel) {
svg
.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("fill", "#767676")
.attr("text-anchor", "end")
.text(xAxisLabel)
}
if (yAxisLabel) {
svg
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", "0.71em")
.attr("fill", "#767676")
.attr("text-anchor", "end")
.text(yAxisLabel)
}
// Highlight dots if their label property is set to TRUE
if (labelsXY.length > 0) {
svg
.selectAll(".dot-label")
.data(labelsXY)
.enter()
.append("text")
.attr("class", "dot-label")
.attr("x", (d) => x(d[xColumn]))
.attr("dy", (d) => {
return (z) ? z(d[zColumn]) + 10 : 15
})
.attr("text-anchor", "middle")
.attr("y", (d) => y(d[yColumn]))
.text(function (d) {
return zLabel != "" ? d[zLabel] : ""
})
}
if (labels.length > 0) {
const clickLoggingOn = getURLParams("labelling") ? true : false ;
console.log("clickLoggingOn", clickLoggingOn);
// Move this to wrangle later once we re-factor the labelling stuff
if (typeof labels[0].coords === 'string') {
labels.forEach(function(d) {
d.coords = JSON.parse(d.coords)
d.sweepFlag = +d.sweepFlag
d.largeArcFlag = +d.largeArcFlag
d.radius = +d.radius
})
}
console.log("annotations", labels)
labels.forEach((config) => {
addLabel(svg2, config, width, height, {
"left": marginleft,
"right": marginright,
"top": margintop,
"bottom": marginbottom
}, clickLoggingOn)
})
}
}
}