charts/verticalbar.mjs (292 lines of code) (raw):
import dataTools from "./shared/dataTools"
import ColorScale from "./shared/colorscale"
import { numberFormat,getURLParams, mustache, mobileCheck, textPadding, textPaddingMobile, stackMin, stackMax, timeCheck, bufferize, getLabelFromColumn } from './shared/toolbelt';
import Dropdown from "./shared/dropdown";
import { addDrops } from "./shared/drops"
import Tooltip from "./shared/tooltip"
import { addPeriods } from "./shared/periods"
import { addLabel } from './shared/arrows'
import { addTrendline } from "./shared/trendline"
import Sonic from "./shared/sonic"
import { checkApp } from 'newsroom-dojo';
export default class Stackedbar {
constructor(settings) {
this.settings = settings
this.noisyChartsSetup = false
this.sonic = null
this.init()
}
init() {
if (this.settings.tooltip != "") {
this.tooltip = new Tooltip(this.settings.tooltip)
}
if (this.settings.dropdown.length > 0) {
addDrops()
this.dropdown = new Dropdown(
"dataPicker",
dataTools.getDropdown(this.settings.dropdown, this.settings.keys)
)
this.dropdown.on("dropdown-change", (label) => {
let data = this.settings.dropdown.find(d => d.label == label)
this.settings.stackedbars = data.values.split(',').map(d => d.trim())
if (this.settings.tooltip != "") {
this.tooltip.updateTemplate(data.tooltip)
}
this.render()
})
}
this.render()
}
render() {
let chart = this
let { modules,
height,
width,
isMobile,
colors,
datum,
data,
keys,
calcs,
title,
subtitle,
source,
dateFormat,
timeInterval,
xAxisLabel,
yAxisLabel,
tooltip,
periodDateFormat,
xAxisDateFormat,
xColumn,
xFormat,
yColumn,
marginleft,
margintop,
marginbottom,
marginright,
footnote,
minY,
maxY,
trendline,
aria,
colorScheme,
type,
labels,
userkey,
dropdown,
periods,
parseTime,
stackedbars,
hideKey,
columns
} = this.settings
d3.select("#graphicContainer svg").remove()
// console.log("type", type)
const chartKey = d3.select("#chartKey")
chartKey.html("")
colors = new ColorScale()
isMobile = mobileCheck()
width = document.querySelector("#graphicContainer").getBoundingClientRect().width
height = width * 0.4
width = width - marginleft - marginright
height = height - margintop - marginbottom
datum = JSON.parse(JSON.stringify(data))
// console.log("data", data)
const keyColor = dataTools.getKeysColors({
keys: stackedbars,
userKey: userkey,
option: { colorScheme : colorScheme }
})
// console.log("stackedbars",stackedbars)
colors.set(keyColor.keys, keyColor.colors)
if (!hideKey) {
stackedbars.forEach((key, i) => {
const keyDiv = chartKey
.append("div")
.attr("class", "keyDiv")
keyDiv.append("span")
.attr("class", "keyCircle")
.style("background-color", () => colors.get(key))
keyDiv.append("span")
.attr("class", "keyText")
.text(getLabelFromColumn(columns, key))
})
}
datum.forEach((d) => {
if (xFormat.date) {
d[xColumn] = parseTime(d[xColumn])
}
stackedbars.forEach((key, i) => {
d[key] = (d[key] == null) ? null : +d[key]
})
d.Total = d3.sum(stackedbars, (k) => +d[k])
})
let playButton = d3.select("#playChart")
playButton
.on("click", () => {sonic.playPause(datum)})
let xRange = timeCheck(timeInterval, datum, xColumn)
var layers = d3.stack()
.offset(d3.stackOffsetDiverging)
.keys(stackedbars)(datum)
layers.forEach(function(layer) {
layer.forEach(function(subLayer) {
subLayer.group = layer.key
subLayer.groupValue = subLayer.data[layer.key]
subLayer.total = subLayer.data.Total
})
})
// console.log(layers)
const svg = d3.select("#graphicContainer")
.append("svg")
.attr("width", width + marginleft + marginright)
.attr("height", height + margintop + marginbottom)
.attr("id", "svg")
.attr("overflow", "hidden")
const features = svg
.append("g")
.attr("transform", "translate(" + marginleft + "," + margintop + ")")
.attr("id", "features")
const x = d3.scaleBand()
.range([0, width])
x.domain(xRange)
const y = d3.scaleLinear()
.range([height, 0])
let extent = [d3.min(layers, stackMin), d3.max(layers, stackMax)]
console.log("extent", extent)
let buffered = bufferize(extent[0], extent[1])
console.log("buffered", buffered)
console.log(minY, !isNaN(minY))
minY = (isNaN(minY)) ? buffered[0] : +minY
maxY = (isNaN(maxY)) ? buffered[1] : +maxY
console.log(minY, maxY)
y.domain([minY, maxY])
let xAxis
let yAxis
let ticks = 3
let tickMod = Math.round(x.domain().length / 10)
if (isMobile) {
tickMod = Math.round(x.domain().length / 5)
}
ticks = x.domain()
.filter((d, i) => !(i % tickMod))
if (isMobile) {
xAxis = d3.axisBottom(x)
.tickValues(ticks)
if (xFormat.date) {
xAxis.tickFormat(xAxisDateFormat)
}
yAxis = d3.axisLeft(y)
.tickFormat((d) => numberFormat(d)).ticks(5)
} else {
xAxis = d3.axisBottom(x)
.tickValues(ticks)
if (xFormat.date) {
xAxis.tickFormat(xAxisDateFormat)
}
yAxis = d3.axisLeft(y)
.tickFormat((d) => numberFormat(d))
}
// Don't run noisycharts in the apps until we can build a workaround
let isApp = checkApp();
if (!isApp) {
if (!chart.noisyChartsSetup) {
chart.sonic = new Sonic(this.settings, datum, x, y, colors, keys=stackedbars)
chart.sonic.addInteraction('buttonContainer', 'showAudioControls')
chart.noisyChartsSetup = true
}
chart.sonic.updateData(datum, x, y, colors, keys=stackedbars)
}
if (isApp) {
d3.select("#showAudioControls").remove();
d3.select("#audioControl").remove();
}
features
.append("g")
.attr("class", "y")
.call(yAxis)
d3.selectAll(".y .tick line")
.style("stroke", "#dcdcdc")
.style("stroke-dasharray", "2 2")
.attr("x2", width)
d3.selectAll(".y path")
.style("stroke-width", "0")
const layer = features
.selectAll("layer")
.data(layers, (d) => d.key)
.enter()
.append("g")
.attr("class", (d) => "layer " + d.key)
.style("fill", (d, i) => colors.get(d.key))
layer
.selectAll("rect")
.data((d) => d)
.enter()
.append("rect")
.attr("x", (d) => x(d.data[xColumn]))
.attr("y", (d) => y(d[1]))
.attr("class", "barPart")
.attr("title", (d) => d.data[d.key])
.attr("data-group", (d) => d.group)
.attr("data-count", (d) => d.data[d.key])
.attr("height", (d) => y(d[0]) - y(d[1]))
.attr("width", (d) => {
let band = x.bandwidth()
return (band < 4) ? band : band - 2
}) //x(data[data.length - 1][xColumn]) / data.length
features
.append("g")
.attr("class", "x")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
if (xAxisLabel) {
svg
.append("text")
.attr("x", width + marginleft)
.attr("y", height + margintop + marginbottom)
.attr("fill", "#767676")
.attr("text-anchor", "end")
.text(xAxisLabel)
}
if (yAxisLabel) {
svg
.append("text")
//.attr("transform", "rotate(-90)")
.attr("x", marginleft)
.attr("y", 0)
.attr("dy", "0.71em")
.attr("fill", "#767676")
.attr("text-anchor", "end")
.text(yAxisLabel)
}
if (periods.length > 0) {
addPeriods(periods, parseTime, features, x, height, xFormat)
}
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
})
}
labels.forEach((config) => {
addLabel(svg, config, width + marginleft + marginright, height + margintop + marginbottom, {"left":marginleft, "right":marginright, "top":margintop, "bottom":marginbottom}, clickLoggingOn)
})
}
if (trendline.length > 0) {
addTrendline(trendline, data, xColumn, parseTime, x, y, features, xFormat)
}
if (this.settings.tooltip != "") {
this.tooltip.bindEvents(
d3.selectAll(".barPart"),
width,
height + margintop + marginbottom
)
}
}
}