Published
Edited
Nov 21, 2020
1 fork
2 stars
Also listed in…
Visualizations
Insert cell
Insert cell
Insert cell
{
const container = html`<div style="width:${width}px;height:450px" ></div>`;
yield container;
const chart = new LineAreaChart()
chart .container(container)
.svgHeight(400)
.dimension(dateDimension)
.xTicksCount(5)
.xDateMin(new Date('1986 Jan'))
.title('Area chart')
.group([{
type:'line',
crossfilterGroup:dateGroupClose,
},{
type:'line',
crossfilterGroup:dateGroupOpen,
},{
type:'area',
crossfilterGroupMin:dateLowGroup,
crossfilterGroupMax:dateGroup
},{
type:'area',
crossfilterGroupMin:groups[0][0],
crossfilterGroupMax:groups[0][1]
},{
type:'area',
crossfilterGroupMin:groups[1][0],
crossfilterGroupMax:groups[1][1]
},{
type:'area',
crossfilterGroupMin:groups[2][0],
crossfilterGroupMax:groups[2][1]
}])
.labelX('Time')
.labelY('Unit')
.render();
// Data Update
setTimeout(d=>{
chart
.group(null)
.data([{
type:'line',
points:dateFilterGroup.all().map((d,i,arr)=>{
if(i>arr.length/2) return Object.assign(d,{dashed:true});
return d
}),
'stroke-width':2,
},{
type:'area',
points:dateFilterGroup.all()
.map(d=>Object.assign(d,{
min:d.value-10,
max:d.value+10
}))
}])
.render()
},5000)
}
Insert cell
class LineAreaChart {
// Define state getters and setters
getState() {
return this.state;
}
setState(d) {
return Object.assign(this.state, d)
};
createId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}

constructor() {
// d3-tip css
document.getElementsByTagName("head")[0].insertAdjacentHTML(
"beforeend",
`<style>.d3-tip tr{border-bottom:none}.d3-tip{font-family:Arial,Helvetica,sans-serif;line-height:1.4;padding:10px;pointer-events:none!important;color:#203d5d;box-shadow:0 4px 20px 4px rgba(0,20,60,.1),0 4px 80px -8px rgba(0,20,60,.2);background-color:#fff;border-radius:4px}.d3-tip:after{box-sizing:border-box;display:inline;font-size:10px;width:100%;line-height:1;color:#fff;position:absolute;pointer-events:none}.d3-tip.n:after{content:"▼";margin:-1px 0 0 0;top:100%;left:0;text-align:center}.d3-tip.e:after{content:"◀";margin:-4px 0 0 0;top:50%;left:-8px}.d3-tip.s:after{content:"▲";margin:0 0 1px 0;top:-8px;left:0;text-align:center}.d3-tip.w:after{content:"▶";margin:-4px 0 0 -1px;top:50%;left:100%}</style>`)

// Define state variables
const state = {
id: this.createId(),
resizeEventListenerId: this.createId(),
svgWidth: 400,
svgHeight: 400,
marginTop: 35,
marginBottom: 35,
marginRight: 10,
marginLeft: 55,
container: 'body',
defaultTextFill: '#2C3E50',
defaultFont: 'Helvetica, Sans-Serif',
ctx: document.createElement('canvas').getContext('2d'),
data: null,
groups: null,
dimensions: null,
scaleX: null,
scaleY: null,
xAxis: null,
yAxis: null,
firstXTick: null,
secondXTick: null,
lastXTick: null,
firstYTick: null,
lastYTick: null,
xLeftOffset: 0.01,
xRightOffset: 0.01,
yTopOffset: 0.1,
yBottomOffset: 0.9,
xDateMax: null,
xDateMin: null,
lineShadows: true,
dashedLineDasharray: '6 6',
disableResizeTransition: true,
centerTicks: false,
xTicksCount: 30,
xTickFormat: null,
transition: true,
title: "",
labelX: null,
labelY: null,
zeroBasis: true,
dropShadowId: 'drop-shadow',
tooltip: (EVENT, { key, values, colors }, state) => {
return `<table cellspacing="0" cellpadding="0" style="color:#7B8399;margin:0px;border:none;outline:none;border-collapse:collapse;border-bottom:none">
<tr><td style="font-weight:bold;font-size:20px" rowspan="${values.length}"><div style="text-align:center;margin-right:14px;width:40px;line-height:1.1">${key.toLocaleString(undefined, {
day: "numeric",
month: "short"
})}</div></td>
<td><div style="position:relative;top:-3px;margin-right:8px;display:inline-block;width:50px;height:3px;background-color:${colors[0]};margin-top:-10px;border-radius:5px;"></div>${Math.round(values[0] * 10) / 10}</td>
</tr>
${values.filter((d, i) => i > 0).map((value, i) => {
return ` <tr><td><div style="position:relative;top:-3px;margin-right:8px;display:inline-block;width:50px;height:3px;background-color:${colors[i + 1]};margin-top:-10px;border-radius:5px;"></div>${Math.round(value * 10) / 10}</td></tr>`
}).join('')}
</table>`
},
colors: ['#F3B52F', '#F4713D', '#663F59', '#6A6E93', '#4C88B2', '#01A6C4', '#04D8D7', '#73F3E4'].concat(schemeCategory10).concat(['#D34A7C']),
// This function takes care of different kind of data formatting setting
setData: state => {

// If we don't have crossfilter group, set normal data
if (!state.groups) {

// If passed data is not array, save it as array
if (!Array.isArray(state.data)) {
return [state.data]
};

// If passed data is array, just return it
return state.data;
};

// If we have crossfilter groups in place, derive normal data from it
const groups = state.groups;
let result = [];

// Attach points to crossfilter groups (usually line chart type) {points:[{key,value}]}
result = result.concat(groups
.filter(d => d.crossfilterGroup)
.map(d => Object.assign(d, {
points: d.crossfilterGroup.all()
}))
)

// Assemble area chart data in following form {points:[{key,max,min}]}
result = result.concat(groups
.filter(d => d.crossfilterGroupMin &&
d.crossfilterGroupMax)
.map(d => {
const maxes = d.crossfilterGroupMax.all();
const mins = d.crossfilterGroupMin.all();
return Object.assign(d, {
points: maxes.map((d, i) => ({
key: d.key,
max: d.value,
min: mins[i].value
}))
})
})
)

// If directly crossfilter group was passed, treat it as line chart
result = result.concat(groups.filter(group => {
return !(group.crossfilterGroup ||
group.crossfilterGroupMin ||
group.crossfilterGroupMax)
})
.map(g => g.all())
.map(points => ({
points: points,
type: 'line',
}))
)
return result;
}
};

// Save state
this.state = state;

// Define handful d3 enter, exit, update pattern method
this.initializeEnterExitUpdatePattern();
}

// Expose container setting
container(container) {
this.setState({
container
});
return this;
}

// Expose x ticks max date settings
xDateMax(xDateMax) {
this.setState({
xDateMax
});
return this;
}

// Expose x ticks min date settings
xDateMin(xDateMin) {
this.setState({
xDateMin
});
return this;
}

// Expose data setting
data(data) {
this.setState({
data
});
return this;
}

// Expose x tick centering boolean setting
centerTicks(centerTicks) {
this.setState({
centerTicks
});
return this;
}

// Expose x tick format
xTickFormat(xTickFormat) {
this.setState({
xTickFormat
});
return this;
}

// Expose x ticks count
xTicksCount(xTicksCount) {
this.setState({
xTicksCount
});
return this;
}

// Make it possible to have uniq resize listener per chart
resizeEventListenerId(resizeEventListenerId) {
this.setState({
resizeEventListenerId
});
return this;
}

// Expose title setting
title(title) {
this.setState({
title
});
return this;
}


// Expose zeroBasis setting
zeroBasis(zeroBasis) {
this.setState({
zeroBasis
});
return this;
}

// Expose yBottomOffset proportion setting
yBottomOffset(yBottomOffset) {
this.setState({
yBottomOffset
});
return this;
}

// Expose label x setting
labelX(labelX) {
this.setState({
labelX
});
return this;
}


// Expose label y setting
labelY(labelY) {
this.setState({
labelY
});
return this;
}

// Expose tooltip override function
tooltip(tooltip) {
this.setState({
tooltip
});
return this;
}

// Expose SVG height setting
svgHeight(svgHeight) {
this.setState({
svgHeight
});
return this;
}

// Expose SVG width setting
svgWidth(svgWidth) {
this.setState({
svgWidth
});
return this;
}

// Get SVG reference
getSvgRef() {
const { svg } = this.getState();
return svg.node();
}

// Expose dimension setting
dimension(dimensions) {
if (Array.isArray(dimensions)) {
this.setState({
dimensions
});
} else if (dimensions) {
this.setState({
dimensions: [dimensions]
});
}
return this;
}

// Expose crossfilter group setting
group(groups) {
if (Array.isArray(groups)) {
this.setState({
groups
});
} else if (groups) {
this.setState({
groups: [groups]
});
} else {
this.setState({ groups: null });
}
return this;
}

// Define enter exit update pattern shorthand
initializeEnterExitUpdatePattern() {
selection.prototype.pattr = function (attribute, value, defaultProperty) {
const container = this;
container.attr(attribute, function (d, i, arr) {
if (defaultProperty && d[defaultProperty] !== undefined) return d[defaultProperty];
if (d[attribute] !== undefined) return d[attribute];
if (typeof value === 'function') return value(d, i, arr);
return value;
})
return this;
}
selection.prototype.patternify = function (params) {
const container = this;
const selector = params.selector;
const elementTag = params.tag;
const data = params.data || [selector];

// Pattern in action
let selection = container.selectAll('.' + selector).data(data, (d, i) => {
if (typeof d === 'object' && d.id) return d.id;
return i;
});
selection.exit().remove();
selection = selection.enter().append(elementTag).merge(selection);
selection.attr('class', selector);
return selection;
};
}

// Render Chart
render() {

// Define how data will be set
this.setDataProp();

// Define containers and set SVG width based on container size
this.setDynamicContainer();

// Calculate some properties
this.calculateProperties();

// Create chart scales
this.createScales();

// Draw SVG and its wrappers
this.drawSvgAndWrappers();

// Create drop shadows
this.createShadowsAndGradients();

// Invoke reusable chart redraw method
this.redraw();

// Attach interactions (tooltip, hover line)
this.attachInteractionElements();

// Attach zooming behavior to chart
this.attachZooming();

// listen for resize event and reRender accordingly
this.reRenderOnResize();

// Allow chaining
return this;
}

// Reusable redraw method
redraw() {
// Create chart axises
this.createAxises();

// Draw horizontal and vertical axises
this.drawAxises();

// Draw area shapes
this.drawAreas();

// Draw line shapes
this.drawLines();
}

// Create chart scales
createScales() {
const {
data,
calc,
xLeftOffset,
xRightOffset,
yBottomOffset,
yTopOffset,
xDateMax,
xDateMin,
zeroBasis
} = this.getState();
const {
chartWidth,
chartHeight,
dateMin,
dateMax
} = calc;

// Retrieve min, max dates and differences
const diffX = dateMax - dateMin;

// Calculate x domain min and max values (from where x axis ranges)
let domainXMin = new Date(dateMin - diffX * xLeftOffset);
let domainXMax = new Date(+dateMax + diffX * xRightOffset);

if (xDateMax) domainXMax = xDateMax;
if (xDateMin) domainXMin = xDateMin;

// Create x scale
const scaleX = scaleTime()
.domain([domainXMin, domainXMax])
.range([0, chartWidth]);
const zoomedX = scaleX;

// Retrieve min, max values and differences
const valueMax = max(data, gr => max(gr.points, d => {
if (d.value !== undefined) return d.value;
if (d.max !== undefined) return d.max;
if (d.min !== undefined) return d.min;
}));
const valueMin = min(data, gr => min(gr.points, d => {
if (d.value !== undefined) return d.value;
if (d.min !== undefined) return d.min;
if (d.max !== undefined) return d.max;
}));
const diffY = valueMax;

// Calculate domain min and max values
const domainYMin = zeroBasis ? 0 : valueMin - diffY * yBottomOffset;
const domainYMax = valueMax + diffY * yTopOffset;

// Create Y sca;e
const scaleY = scaleLinear()
.domain([domainYMax, domainYMin])
.range([0, chartHeight]);
const zoomedY = scaleY;

// Save scales into state
this.setState({
scaleX,
scaleY,
zoomedY,
zoomedX
});
}

// Create chart axises
createAxises() {
const {
// scaleX,
// scaleY,
xTicksCount,
xTickFormat,
zoomedX,
zoomedY
} = this.getState();

// Retrieve all x ticks
const xTicks = zoomedX.ticks(xTicksCount);

// Get first and last x ticks
const firstXTick = xTicks[0];
const secondXTick = xTicks[1];
const lastXTick = xTicks[xTicks.length - 1];

// Retrieve all y ticks
const yTicks = zoomedY.ticks();

// Get first and last y tick
const firstYTick = yTicks[0];
const lastYTick = yTicks[yTicks.length - 1];

// Create x axis
const xAxis = axisBottom(zoomedX)
.ticks(xTicksCount)
.tickFormat(xTickFormat)
.tickSize(-(zoomedY(lastYTick) - zoomedY(firstYTick)))

// Create y axis
const yAxis = axisLeft(zoomedY)
.tickSize(-(zoomedX(lastXTick) - zoomedX(firstXTick)));

// Save axises and tick values into state
this.setState({
xAxis, yAxis, firstXTick, secondXTick, lastXTick, firstYTick, lastYTick
});
}

// Draw axises
drawAxises() {
const {
xAxis,
yAxis,
chart,
firstXTick,
secondXTick,
lastYTick,
calc,
labelX,
labelY,
transition,
svg,
marginLeft,
title,
centerTicks,
zoomedX,
zoomedY,
data
} = this.getState();
const {
chartHeight,
chartWidth
} = calc;



// Draw title
svg.patternify({ tag: 'text', selector: 'title-label' })
.text(title)
.attr('transform', `translate(${20 + marginLeft},${20})`)
.attr('fill', '#66708a')
.attr('font-size', 16)
.attr('font-weight', 'bold')

// Draw x Label
chart.patternify({ tag: 'text', selector: 'label-x' })
.text(labelX)
.attr('transform', `translate(${chartWidth / 2 - this.getTextWidth(labelX, { fontSize: 16 }) / 2},${chartHeight + 50})`)
.attr('fill', '#66708a')
.attr('font-size', 16)

// Draw y label
chart.patternify({ tag: 'text', selector: 'label-y' })
.attr('transform', `translate(${-30},${chartHeight / 2 + this.getTextWidth(labelY, { fontSize: 16 }) / 2}) rotate(-90)`)
.text(labelY)
.attr('fill', '#66708a')
.attr('font-size', 16)

// Draw axis wrapper group
const axisWrapper = chart
.patternify({
tag: 'g',
selector: 'axis-wrapper'
})

// Draw x axis wrapper group
const xAxisWrapper = axisWrapper
.patternify({
tag: 'g',
selector: 'x-axis-wrapper'
})
.attr('transform', `translate(0,${chartHeight})`)

// Draw y axis wrapper
const yAxisWrapper = axisWrapper
.patternify({
tag: 'g',
selector: 'y-axis-wrapper'
})

if (data.every(d => d.points.length === 0)) return;

if (transition) {
// Draw and transition x axis
xAxisWrapper
.transition()
.call(xAxis)

// Transition and draw y axis
yAxisWrapper
.transition()
.call(yAxis);
} else {
xAxisWrapper.call(xAxis);
yAxisWrapper.call(yAxis);
}

// Remove domain lines
axisWrapper.selectAll('.domain').remove();

// Make all tick lines dashed and change color as well
axisWrapper.selectAll('.tick line')
.attr('stroke-dasharray', '5 5')
.attr('stroke', '#DADBDD')

// Change color of all axis texts
axisWrapper.selectAll('text').attr('fill', '#66708a');

// Change position of all texts
xAxisWrapper.selectAll('text')
.transition()
.attr('y', d => {
//centerTicks
return (zoomedY(lastYTick) || chartHeight) - chartHeight + 15
})
.attr('x', d => {
if (centerTicks) {
return (zoomedX(secondXTick) - zoomedX(firstXTick)) / 2;
} else {
return 0
}
})

// Move grid lines to first tick value
xAxisWrapper.selectAll('line')
.attr('transform', `translate(0,${(zoomedY(lastYTick) || chartHeight) - chartHeight})`);

// Change position of all texts
yAxisWrapper.selectAll('text')
.transition()
.attr('x', (zoomedX(firstXTick) || 0) - 10);

// Move grid lines to first tick value
yAxisWrapper.selectAll('line').attr('transform', `translate(${(zoomedX(firstXTick) || 0)})`);
}

// Draw line shapes
drawLines() {
const { data, chart, transition, lineShadows, zoomedX, zoomedY, dropShadowId, colors, dashedLineDasharray, firstXTick, lastXTick } = this.getState();

// Filter lines
const filteredLinesData = data.filter(d => d.type === 'line')
.map(l => Object.assign({}, l, {
points: l.points.filter(p => p.key >= firstXTick && p.key <= lastXTick)
}))

// Create line path func
const linePathDefined = line()
.curve(curveMonotoneX)
.x(d => zoomedX(d.key))
.y(d => zoomedY(d.value))
.defined(d => !d.dashed)

// Create line path func
const linePathUndefined = line()
.curve(curveMonotoneX)
.x(d => zoomedX(d.key))
.y(d => zoomedY(d.value))
.defined(d => d.dashed)

// Get colors
const getColor = (d, i) => {
return colors[colors.length - (i % colors.length) - 1]
}

// Set dashed colors
filteredLinesData.forEach((d, i) => {
d.dashedColor = getColor(d, i);
})

// Get dashed lines
const dashed = filteredLinesData.filter(d => d.points.some(p => p.dashed))
.map(v => Object.assign({ dashed: true }, v))

// Create line wrapper group
const linesWrapper = chart.patternify({ tag: 'g', selector: 'lines-wrapper' });

// Create line paths
const paths = linesWrapper
.patternify({
tag: 'path',
selector: "lines",
data: filteredLinesData.concat(dashed)
})
.pattr('stroke', (d, i) => d.dashedColor || getColor(d, i), 'color')
.pattr('stroke-width', 4)
.attr('stroke-linejoin', 'round')
.attr('stroke-linecap', 'round')
.attr('fill', 'none')
.pattr('stroke-dasharray', d => {
if (d.dashed) return dashedLineDasharray;
return 'auto';
})

if (lineShadows) {
paths.style('filter', `url(#${dropShadowId})`);
}

if (transition) {
// Transition line paths its actual shape
paths.transition()
.attrTween('d', function (d) {
var previous = select(this).attr('d');
var current = d.dashed ? linePathUndefined(d.points) : linePathDefined(d.points);
return interpolatePath(previous, current);
});
} else {
paths.attr('d', d => d.dashed ? linePathUndefined(d.points) : linePathDefined(d.points));
}
}

// Draw area shapes
drawAreas() {
const { chart, zoomedX, zoomedY, data, colors, transition, firstXTick, lastXTick } = this.getState();

// Filter areas
const filteredAreasData = data.filter(d => d.type === 'area')
.map(l => Object.assign({}, l, {
points: l.points.filter(p => p.key >= firstXTick && p.key <= lastXTick)
}))

// Create areas wrapper group
const areasWrapper = chart.patternify({
tag: 'g',
selector: 'lines-wrapper'
});

// Retrieve neccessary amounts of colors and reverse them
const colorsUsed = colors.filter((d, i) => i < filteredAreasData.length).reverse();

// Create ara path calculation function
const areaPath = area()
.curve(curveMonotoneX)
.x(d => zoomedX(d.key))
.y0(d => zoomedY(d.min))
.y1(d => zoomedY(d.max))

// Create area paths
const areas = areasWrapper.patternify({
tag: 'path',
selector: "areas",
data: filteredAreasData
})
.pattr('stroke', 'none')
.pattr('stroke-width', 1)
.attr('stroke-linejoin', 'round')
.attr('stroke-linecap', 'round')
.pattr('opacity', 1)
.pattr('fill-opacity', 1)
.pattr('fill', (d, i) => colorsUsed[i % colorsUsed.length], 'color')

if (transition) {
// Transition area to its shape
areas.transition()
.attrTween('d', function (d) {
var previous = select(this).attr('d');
var current = areaPath(d.points);
return interpolatePath(previous, current);
});
} else {
areas.attr('d', d => areaPath(d.points));
}
}

// Add interaction to chart
attachInteractionElements() {
const { svg, chart, calc, data, colors } = this.getState();
const { chartHeight } = calc;
const that = this;

// Create hover line wrapper element
const hoverLineWrapper = chart.patternify({
tag: 'g',
selector: 'vertical-line-wrapper'
})
.attr('opacity', 0)

// Create hover rectangle shape
hoverLineWrapper.patternify({ tag: 'rect', selector: 'hover-rect' })
.attr('width', 1)
.attr('height', chartHeight)
.attr('fill', 'url(#gradient)');

// Create points' white outline
hoverLineWrapper.patternify({
tag: 'circle',
selector: 'points-outer',
data: data.filter(d => d.type === 'line')
})
.attr('cx', 0)
.attr('cy', 10)
.attr('r', 7)
.attr('fill', 'white')

// Create points inner circle
hoverLineWrapper.patternify({
tag: 'circle',
selector: 'points-inner',
data: data.filter(d => d.type === 'line')
})
.attr('cx', 0)
.attr('cy', 10)
.pattr('r', 5)
.pattr('fill', (d, i) => colors[colors.length - (i % colors.length) - 1], 'color')

// Create circle, from which tip will be fired
hoverLineWrapper.patternify({
tag: 'circle',
selector: 'circle-tip',
data: ['tip']
})
.attr('cx', 0)
.attr('cy', 40)
.attr('r', 0)
.pattr('fill', (d, i) => colors[colors.length - (i % colors.length) - 1], 'color')

// Listen and handle svg events
svg
.on('mousemove', function (event, d) {
const { marginLeft, data, zoomedX, prevIndex, zoomedY, tip, calc } = that.getState();
const { chartWidth } = calc;

// Get actual x position (taking margin into account)
const actualX = pointer(event)[0] - marginLeft;

// Get value from position
const v = zoomedX.invert(actualX)

// Retrieve lines
const lines = data.filter(d => d.type === 'line');

// If lines not found, don't display hover line
if (!lines.length) return;

// Get nearest date value abnd index from data
const { value, index } = that.nearest(lines[0]?.points.map(d => d.key), v, prevIndex || 0) || {};

// If destructured value is not defined, return
if (value === undefined) return;

// Get all related points
const points = data.filter(d => d.type === 'line').map(d => d.points[index]);

// Get all related y values
const yValues = points.map(p => p?.value).filter(d => d !== null && d !== undefined);

// If y values not found, then break action
if (!yValues.length) return;

// Position hover points to respective y coordinates
hoverLineWrapper.selectAll('.points-inner')
.attr('cy', (d, i) => zoomedY(yValues[i]));
hoverLineWrapper.selectAll('.points-outer')
.attr('cy', (d, i) => zoomedY(yValues[i]));

// Calculate tip position
const tipPositionX = zoomedX(new Date(value));

// Move hoverline to its position
hoverLineWrapper
.attr('opacity', 1)
.attr('transform', `translate(${tipPositionX})`);

// Retrieve corresponding colors
const lineColors = data.filter(d => d.type === 'line')
.map((d, i) => (d.color || d.fill || d.stroke) || colors[colors.length - (i % colors.length) - 1]);

// Display eastside or westside tooptip, depending on current position
tip
.direction(tipPositionX < chartWidth / 2 ? 'e' : 'w')
.offset([0, tipPositionX < chartWidth / 2 ? 15 : -15])
.show(event, {
key: new Date(value),
values: yValues,
colors: lineColors,
lines: lines,
points: points
}, hoverLineWrapper.select('.circle-tip').node());

// Save date index
that.setState({ prevIndex: index })
})
.on('mouseleave', function (d) {
const { tip } = that.getState();

// Hide tip
tip.hide();

// Hide hover line
hoverLineWrapper.attr('opacity', 0)
})

// Save previous index into state
this.setState({ prevIndex: 0, hoverLineWrapper })
}

// Make it possible to zoom using mouse wheel
attachZooming() {
// Get svg from state
const { svgWidth, svgHeight, savedZoom, innerWrapper, scaleX, calc } = this.getState();
const { dateMin } = calc;

// Define and attach zoom event and handlers
const zoomBehavior = zoom()
.scaleExtent([1, 1])
.translateExtent([
[scaleX(dateMin), 0],
[svgWidth, svgHeight],
])
.on('start', (event) => this.zoomStarted(event))
.on("zoom", (event) => this.zoomed(event))
.on('end', (event) => this.zoomEnded(event));

if (savedZoom) {
innerWrapper
.transition()
.delay(300)
.duration(0)
.call(zoomBehavior.transform, zoomIdentity);
}

// Call zoom behavior over g group element
innerWrapper.call(zoomBehavior);

// Disable annoying double click zooming
innerWrapper.on("dblclick.zoom", null);

// Save zoom into state
this.setState({ savedZoom: zoomBehavior });
}

// Handle zoom start event
zoomStarted(event) {
// Get state items
const { tip, hoverLineWrapper } = this.getState();

// Hide tip
tip.hide();

// Hide hover line
hoverLineWrapper.attr('opacity', 0)

// Disable transition
this.setState({ transition: false });
}

// Handle zoom end event
zoomEnded(event) {

// Enable transition again
this.setState({ transition: true })
}

// Handle zoom event
zoomed(event) {
const {
svg,
scaleX,
// scaleY,
} = this.getState();

// Get transform object
const transform = event.transform;

// Hide overlay line
svg.select(".overlay-line-g").style("display", "none");

// Rescale x and y scale based on zoom event
const zoomedX = transform.rescaleX(scaleX);

// Incase we will need y zooming in future
// const zoomedY = transform.rescaleY(scaleY);

// Save scales into state
this.setState({
transform,
zoomedX,
//zoomedY, Don't save zoomed y into state
});

// Redraw based on change state
this.redraw()
}


// Calculate what size will text take when drew
getTextWidth(text, {
fontSize = 14,
fontWeight = 400
} = {}) {
const { defaultFont, ctx } = this.getState();
ctx.font = `${fontWeight || ''} ${fontSize}px ${defaultFont} `
const measurement = ctx.measureText(text);
return measurement.width;
}

// Find nearest index and value
nearest(arr, target, prevIndex) {
// Check if search is valid
if (!(arr) || arr.length === 0)
return null;
if (arr.length === 1)
return { value: arr[0], index: 0 };

// first check if it's much different from old index for faster access
if (arr.length > 80) {
const start = Math.max(prevIndex - 40, 1);
const end = Math.min(prevIndex + 40, arr.length);
for (let i = start; i < end; i++) {
if (arr[i] > target) {
let p = arr[i - 1];
let c = arr[i]
const result = Math.abs(p - target) < Math.abs(c - target) ? { value: p, index: i - 1 } : { value: c, index: i };
return result;
}
}
}

// Loop over all array items and find nearest value
for (let i = 1; i < arr.length; i++) {
// As soon as a number bigger than target is found, return the previous or current
// number depending on which has smaller difference to the target.
if (arr[i] > target) {
let p = arr[i - 1];
let c = arr[i]
const result = Math.abs(p - target) < Math.abs(c - target) ? { value: p, index: i - 1 } : { value: c, index: i };
return result;
}
}

// No number in array is bigger so return the last.
return { value: arr[arr.length - 1], index: arr.length - 1 };
}

// Create shadows for lines and gradient for hover lines
createShadowsAndGradients() {
const { svg, dropShadowId } = this.getState();

// Initialize shadow properties
const color = 'black';
const opacity = 0.2;
const filterX = -70;
const filterY = -70;
const filterWidth = 400;
const filterHeight = 400;
const feOffsetDx = 10;
const feOffsetDy = 10;
const feOffsetX = -20;
const feOffsetY = -20;
const feGaussianBlurStdDeviation = 3.1;

// Add Gradients
var defs = svg.patternify({
tag: 'defs',
selector: 'defs-element'
});


// Add linear gradient
const gradients = defs
.patternify({
tag: 'linearGradient',
selector: 'gradients',
data: ['#B450EE']
})
.attr('id', 'gradient')
.attr('x1', '0%')
.attr('y1', '0%')
.attr('x2', '0%')
.attr('y2', '100%')

// Add color stops for each hover line gradient color
gradients
.patternify({
tag: 'stop',
selector: 'gradient-stop-top',
data: (d) => [d]
})
.attr('stop-color', 'white')
.attr('offset', '0%');
gradients
.patternify({
tag: 'stop',
selector: 'gradient-stop-middle',
data: (d) => [d]
})
.attr('stop-color', '#475A8B')
.attr('offset', '50%');
gradients
.patternify({
tag: 'stop',
selector: 'gradient-stop-bottom',
data: (d) => [d]
})
.attr('stop-color', 'white')
.attr('offset', '100%');

// Add Shadows
var filter = defs
.patternify({
tag: 'filter',
selector: 'shadow-filter-element'
})
.attr('id', dropShadowId)
.attr('y', `${filterY}%`)
.attr('x', `${filterX}%`)
.attr('height', `${filterHeight}%`)
.attr('width', `${filterWidth}%`);
filter
.patternify({
tag: 'feGaussianBlur',
selector: 'feGaussianBlur-element'
})
.attr('in', 'SourceAlpha')
.attr('stdDeviation', feGaussianBlurStdDeviation)
.attr('result', 'blur');
filter
.patternify({
tag: 'feOffset',
selector: 'feOffset-element'
})
.attr('in', 'blur')
.attr('result', 'offsetBlur')
.attr('dx', feOffsetDx)
.attr('dy', feOffsetDy)
.attr('x', feOffsetX)
.attr('y', feOffsetY);
filter
.patternify({
tag: 'feFlood',
selector: 'feFlood-element'
})
.attr('in', 'offsetBlur')
.attr('flood-color', color)
.attr('flood-opacity', opacity)
.attr('result', 'offsetColor');

filter
.patternify({
tag: 'feComposite',
selector: 'feComposite-element'
})
.attr('in', 'offsetColor')
.attr('in2', 'offsetBlur')
.attr('operator', 'in')
.attr('result', 'offsetBlur');
var feMerge = filter.patternify({
tag: 'feMerge',
selector: 'feMerge-element'
});
feMerge.patternify({
tag: 'feMergeNode',
selector: 'feMergeNode-blur'
}).attr('in', 'offsetBlur');
feMerge.patternify({
tag: 'feMergeNode',
selector: 'feMergeNode-graphic'
}).attr('in', 'SourceGraphic');
}

// Listen resize event and resize on change
reRenderOnResize() {
const {
resizeEventListenerId,
d3Container,
svgWidth
} = this.getState();
select(window).on('resize.' + resizeEventListenerId, () => {
const { timeoutId, transationTimeoutId } = this.getState();
if (timeoutId) clearTimeout(timeoutId);
if (transationTimeoutId) clearTimeout(transationTimeoutId);
const newTimeoutId = setTimeout(d => {
const { disableResizeTransition } = this.getState();
const containerRect = d3Container.node().getBoundingClientRect();
const newSvgWidth = containerRect.width > 0 ? containerRect.width : svgWidth;
this.setState({
svgWidth: newSvgWidth
});
if (disableResizeTransition) {
this.setState({ transition: false })
}
this.render();
this.setState({});
const newTransationTimeoutId = setTimeout(v => {
this.setState({ transition: true })
}, 500)
this.setState({ transationTimeoutId: newTransationTimeoutId })
}, 1000)
this.setState({ timeoutId: newTimeoutId })
});
}

// Draw SVG and g wrapper
drawSvgAndWrappers() {
const {
d3Container,
svgWidth,
svgHeight,
defaultFont,
calc,
tooltip
} = this.getState();
const {
chartLeftMargin,
chartTopMargin
} = calc;

// Create tip instance
const tip = d3Tip()
.direction('e')
.offset([0, 15])
.attr('class', 'd3-tip')
.html((EVENT, d) => tooltip(EVENT, d, this.getState));

// Draw SVG
const svg = d3Container
.patternify({
tag: 'svg',
selector: 'svg-chart-container'
})
.attr('width', svgWidth)
.attr('height', svgHeight)
.attr('font-family', defaultFont)
.style('background-color', '#FFFFFF')
// .style('overflow', 'visible')

// Call tip on SVG
svg.call(tip)

// Add wraper group element
const innerWrapper = svg
.patternify({
tag: 'g',
selector: 'inner-wrapper'
})
.attr('transform', 'translate(' + chartLeftMargin + ',' + chartTopMargin + ')');

innerWrapper.selectAll('.drag-handler-rect').remove();

// Add background rect , which will receive and handle zoom events
innerWrapper
.patternify({ tag: "rect", selector: "drag-handler-rect" })
.attr("width", svgWidth)
.attr("height", svgHeight)
.attr("fill", "none")
.attr("pointer-events", "all");

// Add container g element
const chart = innerWrapper
.patternify({
tag: 'g',
selector: 'chart'
})

this.setState({
chart,
innerWrapper,
svg,
tip
});
}

// Calculate some properties
calculateProperties() {
const { data } = this.getState();
const {
marginTop,
marginLeft,
marginRight,
marginBottom,
svgWidth,
svgHeight
} = this.getState();

// Calculated properties
const calc = {
id: this.createId(), // id for event handlings,
chartTopMargin: marginTop,
chartLeftMargin: marginLeft,
chartWidth: svgWidth - marginRight - marginLeft,
chartHeight: svgHeight - marginBottom - marginTop,
dateMin: min(data, gr => min(gr.points, d => d.key)),
dateMax: max(data, gr => max(gr.points, d => d.key)),
};

this.setState({
calc
})
}

// Set dynamic width for chart
setDynamicContainer() {
const {
container,
svgWidth
} = this.getState();

// Drawing containers
const d3Container = select(container);
const containerRect = d3Container.node().getBoundingClientRect();
let newSvgWidth = containerRect.width > 0 ? containerRect.width : svgWidth;
this.setState({
d3Container,
svgWidth: newSvgWidth
});
}

// Get current date from state
getData() {
const state = this.getState();
const {
setData
} = state;
return setData(state);
}

// Set data property
setDataProp() {
const data = this.getData();

// Support additional properties for convenience
data.forEach(d => {
d.points.forEach(p => {
if (p.x !== undefined) p.key = p.x;
if (p.y !== undefined) {
if (d.type === 'area') p.max = p.y;
if (d.type === 'line') p.value = p.y;
}
if (p.y0 !== undefined) p.min = p.y0;
})
})
this.setState({
data
})
}
}
Insert cell
Insert cell
Insert cell
data = dataFiltered
.filter((d,i)=>(i<165))
Insert cell
dataFiltered = dataLoad.map(d=>{
return Object.assign(d,{
month:new Date(d.date).getMonth(),
date:new Date(d.date)
})
})
.filter((d,i)=>(i<355))
Insert cell
wrangled = wrangle(data)
Insert cell
ndx = crossfilter(data)
Insert cell
ndx1 = crossfilter(dataFiltered)
Insert cell
dateDimension = ndx.dimension(d=>d.date)
Insert cell
dateDimensionFilter = ndx1.dimension(d=>d.date)
Insert cell
dateGroup.all()
Insert cell
dateGroup
Insert cell
dateGroup = dateDimension.group().reduceSum(d=>d.high)
Insert cell
dateLowGroup = dateDimension.group().reduceSum(d=>d.low-3)
Insert cell
dateFilterGroup = dateDimensionFilter.group().reduceSum(d=>d.low-10)
Insert cell
groups = {
return [[
dateDimension.group().reduceSum(d=>+d.low-50),
dateDimension.group().reduceSum(d=>+d.low-27+Math.random()*5),
],[
dateDimension.group().reduceSum(d=>+d.low-45),
dateDimension.group().reduceSum(d=>+d.low-30+Math.random()*5),
],[
dateDimension.group().reduceSum(d=>+d.low-40),
dateDimension.group().reduceSum(d=>+d.low-35+Math.random()*5),
]]
}
Insert cell
dateGroupOpen = dateDimension.group().reduceSum(d=>+d.open+10)
Insert cell
dateGroupClose = dateDimension.group().reduceSum(d=>d.close-10)
Insert cell
dateGroup.all()
Insert cell
Insert cell
Insert cell
d3 = require('d3')
Insert cell
select = d3.select
Insert cell
selection = d3.selection
Insert cell
scaleTime = d3.scaleTime
Insert cell
min = d3.min
Insert cell
max = d3.max
Insert cell
axisBottom = d3.axisBottom
Insert cell
axisLeft = d3.axisLeft
Insert cell
scaleLinear = d3.scaleLinear
Insert cell
curveNatural = d3.curveNatural
Insert cell
line = d3.line
Insert cell
area = d3.area
Insert cell
d3Tip = d3V6Tip.tip
Insert cell
pointer = d3.pointer
Insert cell
interpolatePath = d3InterpolatePath.interpolatePath
Insert cell
zoomIdentity = d3.zoomIdentity
Insert cell
zoom = d3.zoom
Insert cell
schemeCategory10 = d3.schemeCategory10
Insert cell
curveMonotoneX = d3.curveMonotoneX
Insert cell
Insert cell
Insert cell
d3V6Tip = require('d3-v6-tip')
Insert cell
d3InterpolatePath = require('d3-interpolate-path')
Insert cell
import {load} from "@bumbeishvili/fetcher"
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more