Published
Edited
Jan 30, 2020
Importers
1 star
Insert cell
Insert cell
function barRangeSlider(initialDataArray, accessorFunction, aggregatorFunction, paramsObject) {


const chartWidth = width - 40;
let chartHeight = 100;
let startSelection = 100;

const argumentsArr = [...arguments];

const initialData = initialDataArray;
const accessor = accessorFunction;
const aggregator = aggregatorFunction;
let params = argumentsArr.filter(isPlainObj)[0]
if (!params) {
params = {};
}
params.minY = params.yScale ? 0.0001 : 0;
params.yScale = params.yScale || d3.scaleLinear();
chartHeight = params.height || chartHeight;
params.yTicks = params.yTicks || 4;
params.freezeMin = params.freezeMin||false;




var accessorFunc = d => d;
if (initialData[0].value != null) {
accessorFunc = d => d.value;
}
if (typeof accessor == 'function') {
accessorFunc = accessor;
}
const dataFinal = initialData; //
const isDate = Object.prototype.toString.call(accessor(dataFinal[0])) === '[object Date]';
var dateExtent, dateScale, scaleTime, dateRangesCount, dateRanges, scaleTime;
if (isDate) {
dateExtent = d3.extent(dataFinal.map(accessorFunc));
dateRangesCount = Math.round(width / 5);
dateScale = d3.scaleTime().domain(dateExtent).range([0, dateRangesCount])
scaleTime = d3.scaleTime().domain(dateExtent).range([0, chartWidth])
dateRanges = d3.range(dateRangesCount)
.map(d => [
dateScale.invert(d),
dateScale.invert(d + 1)
])

}


d3.selection.prototype.patternify = function(params) {
var container = this;
var selector = params.selector;
var elementTag = params.tag;
var data = params.data || [selector];

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

const handlerWidth = 2,
handlerFill = '#E1E1E3',
middleHandlerWidth = 10,
middleHandlerStroke = '#8E8E8E',
middleHandlerFill = '#EFF4F7';

const svg = d3.select(DOM.svg(chartWidth, chartHeight))
.style('overflow', 'visible')

const chart = svg.append('g').attr('transform', `translate(30,5)`);



const groupedInitial = group(dataFinal)
.by((d, i) => {
const field = accessorFunc(d);
if (isDate) {
return dateScale(field)
}
return field;
})
.orderBy(d => d.key)
.run()

const grouped = groupedInitial
.map(d => Object.assign(d, {
value: typeof aggregator == 'function' ? aggregator(d) : d.values.length
}));

const values = grouped.map(d => d.value);
const min = d3.min(values);
const max = d3.max(values);
const maxX = grouped[grouped.length - 1].key
const minX = grouped[0].key;

var minDiff = d3.min(grouped, (d, i, arr) => {
if (!i) return Infinity;
return d.key - arr[i - 1].key
})

let eachBarWidth = (chartWidth / minDiff) / (maxX - minX);

if (eachBarWidth > 20) {
eachBarWidth = 20;
}

if (minDiff < 1) {
eachBarWidth = eachBarWidth * minDiff;
}

if (eachBarWidth < 1) {
eachBarWidth = 1;
}

const scale = params.yScale.domain([params.minY, max]).range([0, chartHeight - 25])
const scaleY = scale.copy().domain([max, params.minY]).range([0, chartHeight - 25])

const scaleX = d3.scaleLinear().domain([minX, maxX]).range([0, chartWidth])
var axis = d3.axisBottom(scaleX);
if (isDate) {
axis = d3.axisBottom(scaleTime)
}
const axisY = d3.axisLeft(scaleY).tickSize(-chartWidth - 20).ticks(max == 1 ? 1 : params.yTicks).tickFormat(d3.format('.2s'))

const bars = chart.selectAll('.bar')
.data(grouped)
.enter()
.append('rect')
.attr('class', 'bar')
.attr('width', eachBarWidth)
.attr('height', d => scale(d.value))
.attr('fill', '#767B91')
.attr('y', d => -scale(d.value) + (chartHeight - 25))
.attr('x', (d, i) => scaleX(d.key) - eachBarWidth / 2)
.attr('opacity', 0.9)

const xAxisWrapper = chart.append('g')
.attr('transform', `translate(${0},${chartHeight-25})`)
.call(axis)

const yAxisWrapper = chart.append('g')
.attr('transform', `translate(${-10},${0})`)
.call(axisY)

const brush = chart.append("g")
.attr("class", "brush")
.call(d3.brushX()
.extent([
[0, 0],
[chartWidth, chartHeight]
])
.on("start", brushStarted)
.on("end", brushEnded)
.on("brush", brushed));

chart.selectAll('.selection').attr('fill-opacity', 0.1)

var handle = brush.patternify({
tag: 'g',
selector: 'custom-handle',
data: [{
left: true
}, {
left: false
}]
})
.attr("cursor", "ew-resize")
.attr('pointer-events', 'all')

handle.patternify({
tag: 'rect',
selector: 'custom-handle-rect',
data: d => [d]
})
.attr('width', handlerWidth)
.attr('height', 100)
.attr('fill', handlerFill)
.attr('stroke', handlerFill)
.attr('y', -50)
.attr('pointer-events', 'none')

handle.patternify({
tag: 'rect',
selector: 'custom-handle-rect-middle',
data: d => [d]
})
.attr('width', middleHandlerWidth)
.attr('height', 30)
.attr('fill', middleHandlerFill)
.attr('stroke', middleHandlerStroke)
.attr('y', -16)
.attr('x', -middleHandlerWidth / 4)
.attr('pointer-events', 'none')
.attr('rx', 3)


handle.patternify({
tag: 'rect',
selector: 'custom-handle-rect-line-left',
data: d => [d]
})
.attr('width', 0.7)
.attr('height', 20)
.attr('fill', middleHandlerStroke)
.attr('stroke', middleHandlerStroke)
.attr('y', -100 / 6 + 5)
.attr('x', -middleHandlerWidth / 4 + 3)
.attr('pointer-events', 'none')

handle.patternify({
tag: 'rect',
selector: 'custom-handle-rect-line-right',
data: d => [d]
})
.attr('width', 0.7)
.attr('height', 20)
.attr('fill', middleHandlerStroke)
.attr('stroke', middleHandlerStroke)
.attr('y', -100 / 6 + 5)
.attr('x', -middleHandlerWidth / 4 + middleHandlerWidth - 3)
.attr('pointer-events', 'none')

handle.attr("display", 'none')
function brushStarted(){
if (d3.event.selection) {
startSelection = d3.event.selection[0];
}
}

function brushEnded() {
console.log('ended')
if (!d3.event.selection) {
handle.attr("display", 'none')
output({
range: [minX, maxX]
})
return
}
if (d3.event.sourceEvent.type === "brush") return;
var d0 = d3.event.selection.map(scaleX.invert),
d1 = d0.map(d3.timeDay.round);


if (d1[0] >= d1[1]) {
d1[0] = d3.timeDay.floor(d0[0]);
d1[1] = d3.timeDay.offset(d1[0]);
}
console.log(d0, d1)
}

function brushed(d) {
if (d3.event.sourceEvent.type === "brush") return;
console.log('brushed',d3.event.selection)
if(params.freezeMin){
if(d3.event.selection[0]<startSelection){
d3.event.selection[1] = Math.min(d3.event.selection[0], d3.event.selection[1])
}
if(d3.event.selection[0]>=startSelection){
d3.event.selection[1] = Math.max(d3.event.selection[0], d3.event.selection[1])
}
d3.event.selection[0]= 0;
// console.log(d3.event.selection)

d3.select(this)
.call(d3.event.target.move,
d3.event.selection);
}
var d0 = d3.event.selection.map(scaleX.invert);
const s = d3.event.selection;
console.log(s)

handle.attr("display", null)
.attr("transform", function(d, i) {
console.log(d)
return "translate(" + (s[i] - 2) + "," + chartHeight / 2 + ")";
});
output({
range: d0
})
}


yAxisWrapper.selectAll('.domain').remove();
xAxisWrapper.selectAll('.domain').attr('opacity', 0.1)

chart.selectAll('.tick line').attr('opacity', 0.1)

function isPlainObj(o) {
return typeof o == 'object' && o.constructor == Object;
}

function output(value) {
const node = svg.node();
node.value = value;
node.value.data = getData(node.value.range);
if (isDate) {
node.value.range = value.range.map(d => dateScale.invert(d))
}
node.dispatchEvent(new CustomEvent('input'))
}

function getData(range) {
const dataBars = bars
.attr('fill', '#767B91')
.filter(d => {
return d.key >= range[0] && d.key <= range[1];
})
.attr('fill', '#F4911E')
.nodes()
.map(d => d.__data__)
.map(d => d.values)
.reduce((a, b) => a.concat(b), [])


return dataBars;

}

const returnValue = Object.assign(svg.node(), {
value: {
range: [minX, maxX],
data: initialData
}
})

if (isDate) {
returnValue.value.range = returnValue.value.range.map(d => dateScale.invert(d))
}

return returnValue;
}
Insert cell
Insert cell
Insert cell
d3 = require('d3')
Insert cell
import {group} from "@bumbeishvili/utils"
Insert cell
import {comments} from "@bumbeishvili/utils"
Insert cell
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