class BaseChart {
months = []
plots = []
settings
element
constructor ({ plots, settings, element }) {
this.months = ['Jan', 'Feb', 'March', 'April', 'May', 'June', 'July', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec']
this.plots = plots
this.settings = settings
this.element = element
}
getDimensionAccessor (dimension) {
if (dimension.accessor !== undefined) {
return dimension.accessor
}
if (dimension.properties !== undefined) {
return (d) => dimension.properties.map(p => d[p])
}
if (dimension.property !== undefined) {
return (d) => d[dimension.property]
}
return (d) => d
}
getDimensionDomain (data, dimension) {
if (dimension.domain !== undefined) {
return dimension.domain
}
const accessor = this.getDimensionAccessor(dimension)
return d3.extent(data.map(accessor).flat())
}
makeChartElements () {
const {width,height} = this.settings
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("position","absolute")
const canvas = d3.create("canvas")
.attr('width',width)
.attr('height', height)
.style('position','absolute')
const ctx = canvas.node().getContext('2d')
ctx.imageSmoothingQuality = 'high'
return {svg,canvas,ctx}
}
getYAxis (scale,settings, axisSettings = {}) {
const {margin} = settings
const axis = d3.axisLeft(scale)
if(axisSettings.ticks){
axis.ticks(axisSettings.ticks)
}
if(axisSettings.tickFormat){
axis.tickFormat(axisSettings.tickFormat)
}
return g => g
.attr("transform", `translate(${margin.left},${margin.top})`)
.call(axis)
}
getXAxis (scale,settings, axisSettings = {}) {
const {margin, height} = settings
const axis = d3.axisBottom(scale)
if(axisSettings.ticks){
axis.ticks(axisSettings.ticks)
}
if(axisSettings.tickFormat){
axis.tickFormat(axisSettings.tickFormat)
}
return g => g
.attr("transform", `translate(${margin.left},${height - margin.bottom})`)
.call(axis)
}
drawLine ({data, ctx, svg, settings}) {
const xScale = settings.xScale || this.makeScale(data,{
property:settings.dimensions.x.property,
offset:settings.margin.left,
range:[0,settings.width - settings.margin.left- settings.margin.right]
})
const yScale = settings.yScale || this.makeScale(data,{
property:settings.dimensions.y.property,
offset:settings.margin.top,
range:[settings.height - settings.margin.top - settings.margin.bottom,0]
})
ctx.strokeStyle = settings?.style?.strokeColor || '333'
ctx.lineWidth = settings?.style?.strokeWidth || 1
const line = d3.line()
.x(xScale.position)
.y(yScale.position)
.defined(d=>!isNaN(yScale.position(d)))
line.context(ctx)
ctx.beginPath()
if(settings?.style?.strokeDash){
ctx.setLineDash(settings.style.strokeDash)
}
line(data)
ctx.stroke();
return {
x:xScale,
y:yScale
}
}
makeScale (data,dimension){
const accessor = getAccessor(dimension)
const extent = getExtent(data,dimension)
const range = (dimension.range || [0,width]).slice()
const scale = (dimension.scale || ((accessor(data[0]) instanceof Date) ?
d3.scaleTime().domain(extent).range(range) :
d3.scaleLinear().domain(extent).range(range))
)
return {
scale,
position: (d)=>(dimension.offset || 0) + scale(accessor(d))
}
}
drawArea ({data, ctx, svg, settings}) {
const xRange = settings.xScale ? settings.xScale.scale.range() : [
0,
settings.width - settings.margin.left - settings.margin.right
]
const yRange = settings.yScale ? settings.yScale.scale.range() : [
settings.height - settings.margin.top - settings.margin.bottom,
0
]
const yDim = settings?.dimensions?.y || {}
const yMinProp = yDim?.properties ? yDim.properties[0] : 'min'
const yMaxProp = yDim?.properties ? yDim.properties[1] : 'max'
const yExt = settings.yScale ? settings.yScale.scale.domain() : d3.extent(data.map(d=>[d[yMinProp],d[yMaxProp]]).flat())
const yScale = settings.yScale ? settings.yScale.scale : d3.scaleLinear().domain(yExt).range(yRange.slice())
console.log(`yMin: ${yMinProp}, yMax: ${yMaxProp}`)
const scales = {
x:settings.xScale || this.makeScale(data,{
property: settings.dimensions.x.property,
range:xRange,
offset:settings.margin.left
}),
y0: this.makeScale(data, {
property:yMinProp,
scale:yScale,
offset:settings.margin.top
}),
y1: this.makeScale(data,{
property:yMaxProp,
scale:yScale,
offset:settings.margin.top
})
}
ctx.fillStyle = settings.style.fill
const area = d3.area()
.x(scales.x.position)
.y0(scales.y0.position)
.y1(scales.y1.position)
.defined((d)=>!isNaN(scales.y0.position(d)))
area.context(ctx);
ctx.beginPath();
area(data);
ctx.fill();
return scales
}
drawScatter ({data, ctx, svg, settings}) {
const xScale = settings.xScale || this.makeScale(data,{
property:settings.dimensions.x.property,
offset:settings.margin.left,
range:[0,settings.width - settings.margin.left- settings.margin.right]
})
const yScale = settings.yScale || this.makeScale(data,{
property:settings.dimensions.y.property,
offset:settings.margin.top,
range:[settings.height - settings.margin.top - settings.margin.bottom,0]
})
ctx.strokeStyle = settings?.style?.strokeColor
ctx.strokeWidth = (settings?.style?.strokeWidth || settings?.style?.strokeColor) ? (settings.style.strokeWidth || 1) : 0
ctx.fillStyle = settings?.style?.fill
data.map(d => {
ctx.beginPath();
ctx.arc(xScale.position(d), yScale.position(d), settings.style.radius, 0, 2 * Math.PI);
ctx.stroke()
ctx.fill()
});
return {
x:xScale,
y:yScale
}
}
draw () {
const {margin,width,height} = this.settings
this.plots.forEach(plot => {
Object.keys(plot.dimensions).forEach(axis => {
const dimension = plot.dimensions[axis]
dimension.accessor = this.getDimensionAccessor(dimension)
dimension.domain = this.getDimensionDomain(plot.data, dimension)
})
})
const xGroups = _.groupBy(this.plots,p=>p.dimensions.x.parameter)
const chartHeight = height - margin.top - (margin.bottom * Object.keys(xGroups).length)
const yGroups = _.groupBy(this.plots,p=>p.dimensions.y.parameter)
const yAxes = []
const yRange = [chartHeight,0]
Object.keys(yGroups).forEach(key=>{
const p = yGroups[key]
const domain = d3.extent(
Object.values(yGroups[key])
.map(p=>p.dimensions.y.domain)
.flat()
)
const scale = (
(domain[0] instanceof Date) ? d3.scaleTime() : d3.scaleLinear())
.domain(domain)
.range(yRange)
const axis = {
plots:p,
key,
domain,
scale,
}
p.forEach(_p=>{
_p.axes = _p.axes || {}
_p.axes.y = axis
})
yAxes.push(axis)
})
const {canvas,ctx,svg} = this.makeChartElements()
const elementSelection = typeof this.element.node === 'function' ? this.element : d3.select(this.element)
elementSelection
.style('width',`${width}px`)
.style('height', `${height}px`)
.style('position','relative')
elementSelection.node().append(canvas.node())
elementSelection.node().append(svg.node())
yAxes.forEach((yAxis,index)=>{
const yAxisSettings = {
...this.settings,
margin:{
...margin,
left: margin.left * (index + 1)
}
}
const axis = this.getYAxis(yAxis.scale,
yAxisSettings,
this.settings.yAxisSettings
)
yAxis.g = svg.append('g').call(axis)
})
const yAxesWidth = margin.left * yAxes.length
const chartWidth = width - yAxesWidth - margin.right
const xRange = [0,chartWidth]
const xAxes = []
Object.keys(xGroups).forEach(key=>{
const p = xGroups[key]
const domain = d3.extent(Object.values(xGroups[key]).map(p=>p.dimensions.x.domain).flat())
const sc = ((domain[0] instanceof Date) ? d3.scaleTime() : d3.scaleLinear())
.domain(domain)
.range(xRange)
const axis = {
plots:p,
key:key || 'time',
extent: d3.extent(Object.values(p).map(p=>p.dimensions.x.extent).flat()),
scale: sc
}
p.forEach(_p=>{
_p.axes = _p.axes || {}
_p.axes.x = axis
})
xAxes.push(axis)
})
xAxes.forEach((xAxis,index)=>{
xAxis.g = svg.append('g').call(this.getXAxis(
xAxis.scale,
{
...this.settings,
margin:{
...this.settings.margin,
left: yAxesWidth,
bottom:margin.bottom * (index + 1)
}
},
(xAxis.key === 'months' ? {tickFormat:(d)=>this.months[d]} : {})
))
})
const drawFunctions = {
line:this.drawLine,
area:this.drawArea,
scatter:this.drawScatter
}
this.plots.forEach(p=>{
if(drawFunctions[p.type] !== undefined){
ctx.save()
const xAxis = p.axes.x
const yAxis = p.axes.y
drawFunctions[p.type].call(this,{
data:p.data,
ctx,
svg,
settings:{
...this.settings,
dimensions:p.dimensions || this.settings.dimensions,
style:{
...this.settings.style,
...p.style
},
xScale:{
scale:xAxis.scale,
position:(d)=>(this.settings.margin.left * yAxes.length) + xAxis.scale(p.dimensions.x.accessor(d))
},
yScale:{
scale:yAxis.scale,
position:(d)=>this.settings.margin.top + yAxis.scale(p.dimensions.y.accessor(d))
}
}
})
ctx.restore()
}
})
}
}