charts/linechart.mjs (431 lines of code) (raw):
import helpers from "./shared/helpers"
import dataTools from "./shared/dataTools"
import Tooltip from "./shared/tooltip"
import ColorScale from "./shared/colorscale"
import { addPeriods } from "./shared/periods"
import { addLines } from "./shared/lines"
//import { addLabels } from "./shared/labels"
import { addLabel, clickLogging } from './shared/arrows'
import { addDrops } from "./shared/drops"
import { getURLParams, getLongestKeyLength, numberFormat, mustache, mobileCheck, sorter, relax, bufferize, isNumber, getLabelFromColumn} from './shared/toolbelt';
import Dropdown from "./shared/dropdown";
import Sonic from "./shared/sonic"
import { checkApp } from 'newsroom-dojo';
export default class Linechart {
constructor(settings) {
console.log("Settings", settings)
this.settings = settings
this.noisyChartsSetup = false
this.sonic = null
this.init()
}
init() {
this.settings.chartlines = this.settings.chartlines.filter(d => d != "")
this.settings.chartlines = this.settings.chartlines.filter(d => d != this.settings.xColumn)
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.chartlines = [...data.values.split(',').map(d => d.trim()) ] //d.trim()
this.settings.chartlines = this.settings.chartlines.filter(d => d != "")
// this.settings.chartlines = this.settings.chartlines.filter(d => d != this.settings.xColumn)
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,
title,
subtitle,
source,
dateFormat,
yScaleType,
xAxisLabel,
yAxisLabel,
tooltip,
periodDateFormat,
marginleft,
margintop,
marginbottom,
marginright,
minX,
minY,
maxY,
footnote,
aria,
colorScheme,
lineLabelling,
type,
userkey,
periods,
labels,
dropdown,
xAxisDateFormat,
breaks,
lines,
xColumn,
xFormat,
chartlines,
current,
parseTime,
invertY,
curve,
zeroLineX,
zeroLineY,
tooltipModule,
columns } = this.settings
console.log("curve", curve)
d3.select("#graphicContainer svg").remove()
const chartKey = d3.select("#chartKey")
chartKey.html("")
isMobile = mobileCheck()
datum = JSON.parse(JSON.stringify(data))
let keyCopy = JSON.parse(JSON.stringify(keys))
keyCopy = keyCopy.filter(d => d != xColumn)
const containerwidth = document
.querySelector("#graphicContainer")
.getBoundingClientRect().width
const containerheight = containerwidth * 0.4
width = containerwidth - marginleft - marginright
height = containerheight - margintop - marginbottom
// Reverses the yAxis
let yRange = [height, 0]
if (invertY) {
yRange = [0, height]
}
let y = d3.scaleLinear()
.rangeRound(yRange)
//console.log("invertY", invertY)
if (yScaleType != "" && yScaleType != null) {
y = d3[yScaleType]()
.range(yRange)
.nice()
}
let lineGenerators = {}
let chartValues = []
let chartKeyData = {}
colors = new ColorScale()
const keyColor = dataTools.getKeysColors({
keys: keyCopy,
userKey: userkey,
option: { colorScheme : colorScheme }
})
//console.log("keyColor",keyColor)
colors.set(keyColor.keys, keyColor.colors)
const svg = d3
.select("#graphicContainer")
.append("svg")
.attr("width", containerwidth)
.attr("height", containerheight)
.attr("id", "svg")
// .attr("aria-hidden", "true")
.attr("overflow", "hidden")
let buffer = (lineLabelling) ? getLongestKeyLength(svg, keyCopy, isMobile, lineLabelling) : 0 ;
//console.log("xFormat",xFormat)
// Set a default x scale
let x = d3.scaleLinear()
.rangeRound([0, width - buffer])
if (xFormat.date) {
x = d3.scaleTime()
.rangeRound([0, width - buffer])
} else if (xFormat.string) {
x = d3.scaleLinear()
.rangeRound([0, width - buffer])
} else if (xFormat.number) {
x = d3.scaleLinear()
.rangeRound([0, width - buffer])
}
const features = svg
.append("g")
.attr("transform","translate(" + marginleft + "," + margintop + ")")
.attr("id", "features")
//console.log("chartlines", chartlines)
console.log("linebreaks", breaks)
chartlines.forEach((key) => {
if (curve) {
lineGenerators[key] = d3
.line()
.x((d) => x(d[xColumn]))
.y((d) => y(d[key]))
.curve(d3[curve])
}
else {
lineGenerators[key] = d3
.line()
.x((d) => x(d[xColumn]))
.y((d) => y(d[key]))
}
if (breaks) {
lineGenerators[key].defined( (d) => d)
}
datum.forEach((d) => {
if (key != xColumn) {
chartValues.push(d[key])
//console.log(`${key}: ${d[key]}`)
}
})
})
if (chartlines.length > 1) {
if (isMobile && !lineLabelling || lineLabelling === false) {
let chartKeys = JSON.parse(JSON.stringify(chartlines))
//chartKeys.splice(chartKeys.indexOf(xColumn), 1)
chartKeys.forEach((key) => {
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])
}
})
keyCopy.forEach((key) => {
chartKeyData[key] = []
datum.forEach((d) => {
if (d[key] != null) {
let newData = {}
newData[xColumn] = d[xColumn]
newData[key] = d[key]
chartKeyData[key].push(newData)
} else if (breaks) {
chartKeyData[key].push(null)
}
})
})
// const max = (maxY && maxY !== "")
// ? parseInt(maxY)
// : d3.max(chartValues)
// const min = (minY && minY !== "")
// ? parseInt(minY)
// : d3.min(chartValues)
let extentY = d3.extent(chartValues)
console.log("extentY",extentY)
let bufferY = bufferize(extentY[0], extentY[1])
console.log("bufferY",bufferY)
console.log("minY", minY)
minY = (isNumber(minY)) ? +minY : bufferY[0]
maxY = (isNumber(maxY)) ? +maxY : bufferY[1]
console.log(minY, maxY)
let range = datum.map( d => d[xColumn])
//console.log("renage",range)
x.domain(d3.extent(range))
y.domain([minY, maxY])
const xTicks = Math.round(width / 110)
const yTicks = (yScaleType === "scaleLog") ? 3 : 5
const xAxis = d3.axisBottom(x)
.ticks(xTicks)
if (xFormat.date) {
xAxis
.tickFormat(d3.timeFormat(xAxisDateFormat))
}
// Don't run noisycharts in the apps until we can build a workaround
let isApp = checkApp();
console.log("aria", aria)
if (!isApp && aria != false) {
console.log("Setting up noisycharts...")
if (!chart.noisyChartsSetup) {
chart.sonic = new Sonic(this.settings, datum, x, y, colors)
chart.sonic.addInteraction('buttonContainer', 'showAudioControls')
chart.noisyChartsSetup = true
}
chart.sonic.updateData(datum, x, y, colors)
}
else if (isApp || aria == false) {
d3.select("#showAudioControls").remove();
d3.select("#audioControl").remove();
}
const yAxis = d3
.axisLeft(y)
.tickFormat((d) => numberFormat(d))
.ticks(yTicks)
.tickSize(-(width - buffer))
features.append("g")
.attr("class", "y dashed")
.call(yAxis)
.style("stroke-dasharray", "2 2")
console.log("zeroLineY", zeroLineY)
if (zeroLineY) {
features.append("line")
.attr("x1", 0)
.attr("x2", width - buffer)
.attr("y1", y(0))
.attr("y2", y(0))
.attr("stroke-width", 2)
.attr("class", "zeroLine")
}
features
.append("g")
.attr("class", "x")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
features
.select(".y .domain")
.remove()
features
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", "0.71em")
.attr("fill", "#767676")
.attr("text-anchor", "end")
.text(yAxisLabel)
features
.append("text")
.attr("x", () => {
return lineLabelling ? width - buffer : width
})
.attr("y", height - 6)
.attr("fill", "#767676")
.attr("text-anchor", "end")
.text(xAxisLabel)
d3.selectAll(".tick line")
.attr("stroke", "#767676")
d3.selectAll(".tick text")
.attr("fill", "#767676")
d3.selectAll(".domain")
.attr("stroke", "#767676")
var keyOrder = []
let labelPos = []
/*
if (lineLabelling) {
chartlines.forEach((key) => {
if (key != xColumn) {
let value = y(chartKeyData[key][chartKeyData[key].length - 1][key]) + 4
labelPos.push({ key : key , value : value , labelY : value })
}
})
relax(labelPos);
}
*/
if (lineLabelling) {
chartlines.forEach((key) => {
if (key != xColumn) {
let lastValueIndex = chartKeyData[key].length - 1;
// Find the last value that is not null, an empty string, or undefined
while (
lastValueIndex >= 0 &&
(
chartKeyData[key][lastValueIndex] === null || // Check if the data point itself is null
chartKeyData[key][lastValueIndex][key] === "" || // Check for an empty string
chartKeyData[key][lastValueIndex][key] === null // Check if the property value is null
)
) {
lastValueIndex--;
}
if (lastValueIndex >= 0 && chartKeyData[key][lastValueIndex] !== null) {
let value = y(chartKeyData[key][lastValueIndex][key]) + 4;
labelPos.push({ key: key, value: value, labelY: value });
}
}
});
relax(labelPos);
}
chartlines.forEach((key) => {
//console.log("key",key)
features
.append("path")
.datum(chartKeyData[key])
.attr("fill", "none")
.attr("stroke", (d) => colors.get(key))
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 3)
.attr("d", lineGenerators[key])
const tempLabelData = chartKeyData[key].filter((d) => d != null)
let lineLabelAlign = "start"
let lineLabelOffset = 10
keyOrder.push(key)
if (lineLabelling) {
features
.append("circle")
.attr("cy", (d) => y(tempLabelData[tempLabelData.length - 1][key]))
.attr("fill", (d) => colors.get(key))
.attr("cx", (d) => x(tempLabelData[tempLabelData.length - 1][xColumn]))
.attr("r", 4)
.style("opacity", 1)
features
.append("text")
.attr("class", "lineLabels")
.style("font-weight","bold")
.style("font-size","15px")
.attr("y", (d) => {
let pos = (labelPos.find(d => d.key == key)) ? labelPos.find(d => d.key === key).labelY : - 100
return pos
})
.attr("x", (d) => x(tempLabelData[tempLabelData.length - 1][xColumn]) + 5)
.style("opacity", 1)
.attr("text-anchor", lineLabelAlign)
.attr("fill", (d) => colors.get(key))
.text((d) => `${key.replace(/_/g, "")}`)
}
})
d3.select("#annotations").text("")
if (periods.length > 0) {
addPeriods(periods, features, x, height, xFormat, isMobile)
}
if (lines.length > 0) {
addLines(lines, x, y, features, parseTime)
}
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) => {
/*
console.log(`Width: ${width},
marginleft: ${marginleft},
marginright: ${marginright},
height: ${height},
margintop: ${margintop},
marginbottom: ${marginbottom}
buffer: ${buffer}`)
*/
addLabel(svg, config, width + marginleft + marginright - buffer, height + margintop + marginbottom, {"left":marginleft, "right":marginright, "top":margintop, "bottom":marginbottom}, clickLoggingOn)
})
}
/*
if (labels.length > 0) {
addLabels(labels, parseTime, features, isMobile, x, y)
}
*/
if (this.settings.tooltip != "") {
this.tooltip.drawHoverFeature(features, height, width, xColumn, marginleft, chartKeyData, x, datum, keyCopy)
}
}
}