Public
Edited
Jun 30, 2023
Insert cell
Insert cell
scatterSettings
Insert cell
makeScatter({data:rawData,settings:scatterSettings})
Insert cell
makeLine({data:rawData,settings:lineSettings})
Insert cell
binOnTime({data:rawData,binSize:'years'}).sort((a,b)=>+a.time - +b.time)
Insert cell
Insert cell
Insert cell
binSeasonal({data:rawData,y:'water_temperature(fahrenheit)'})
Insert cell
Insert cell
seasonalAxisTickFormat = {
return (d)=>months[d]
}
Insert cell
Insert cell
parent_view = {
const parent = DOM.element('div');
return parent;
}
Insert cell
{
const binnedData = binOnTime({data:rawData})
parent_view.innerHTML = ''
const element = parent_view
const chart = new BaseChart({
element,
settings: baseSettings,
plots:[
{
data:binnedData,
dimensions:{
x:{
property:'time',
parameter: 'time'
},
y:{
properties:['min','max'],
parameter:baseSettings.dimensions.y.property
}
},
style:{
fill:'#CCCCCC33'
},
type:'area',
layout:'xy'
},
{
data:rawData,
dimensions:{
x:{
property:'time',
parameter: 'time'
},
y:{
property:baseSettings.dimensions.y.property,
parameter:'a different one'
}
},
style:{
fill:'#CCCCCC33'
},
type:'line',
layout:'xy'
}
]
})
chart.draw()
//console.log('hi')
//return html`${element.node().outerHTML}`

}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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()
}
})







}


}

Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
rawData = {
const geojson = await(await fetch(baseSettings.url)).json()
return geojson.features.map(f=>({...f.properties,time:new Date(f.properties[baseSettings.time])})).sort((a,b)=>+a.time - +b.time)
}
Insert cell
Insert cell
baseSettings = {
const height = Math.min(400,width*.6)
const margin = {
top:10,
right:10,
left:60,
bottom:40
}
return {
url: 'https://data.axds.co/gs/wfs?service=WFS&version=1.1.0&request=GetFeature&outputFormat=application%2Fjson&typeName=secoora%3ASECOORA-ShellBase-NC&CQL_FILTER=INTERSECTS%28geometry%2C+POLYGON%28%2835.583035684074176+-75.51210937499853%2C34.991167186014664+-76.46791992187377%2C34.70716278026717+-76.27016601562393%2C35.05414604408112+-75.38027343749863%2C35.583035684074176+-75.51210937499853%29%29%29',
time: 'sample_datetime',
dimensions:{
x: {
property:'time',
range:[0,width - margin.left - margin.right],
offset:margin.left
},
y: {
property:'salinity(ppt)',
range:[height - margin.top - margin.bottom,0],
offset:margin.top
}
},
width,
height,
margin,
style:{
radius:1.5,
strokeWidth:.5,
fill:'#666',
stroke:'#FFFFFF'
}
}
}
Insert cell
scatterSettings = ({...baseSettings})
Insert cell
Insert cell
areaSettings = ({...baseSettings,style:{fill:'#9900CC33'}})
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more