UNPKG

range-slider-d3

Version:
588 lines (511 loc) 15.4 kB
d3 = window.d3||require('d3'); class RangeSlider { constructor() { const attrs = { id: "ID" + Math.floor(Math.random() * 1000000), container: null, svgWidth: 400, svgHeight: 400, marginTop: 10, marginBottom: 0, marginRight: 0, marginLeft: 40, container: "body", defaultTextFill: "#2C3E50", defaultFont: "Helvetica", data: null, accessor: null, aggregator: null, onBrush: (d) => d, yScale: d3.scaleLinear(), yTicks: 4, freezeMin: false, startSelection: 100, svg:null, hideXAxis: false }; this.getChartState = () => attrs; Object.keys(attrs).forEach((key) => { //@ts-ignore this[key] = function (_) { var string = `attrs['${key}'] = _`; if (!arguments.length) { return eval(`attrs['${key}'];`); } eval(string); return this; }; }); this.initializeEnterExitUpdatePattern(); } // Fancy version of d3 join initializeEnterExitUpdatePattern() { 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; }; } drawChartTemplate() { const attrs = this.getChartState(); const calc = attrs.calc; //Drawing containers var container = d3.select(attrs.container); var containerRect = container.node().getBoundingClientRect(); if (containerRect.width > 0) attrs.svgWidth = containerRect.width; //Add svg var svg = container .patternify({ tag: "svg", selector: "svg-chart-container", }) .style("overflow", "visible") .attr("width", attrs.svgWidth) .attr("height", attrs.svgHeight) .attr("font-family", attrs.defaultFont); //Add container g element var chart = svg .patternify({ tag: "g", selector: "chart", }) .attr( "transform", "translate(" + calc.chartLeftMargin + "," + calc.chartTopMargin + ")" ); // Share chart attrs.chart = chart; attrs.svg = svg; } drawBrushHandles() { const attrs = this.getChartState(); const brush = attrs.brush; const calc = attrs.calc; const handlerWidth = 2, handlerFill = "#3F434D", middleHandlerWidth = 10, middleHandlerStroke = "#D4D7DF", middleHandlerFill = "#486878"; 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", calc.chartHeight) .attr("fill", handlerFill) .attr("stroke", handlerFill) .attr("y", -calc.chartHeight / 2) .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"); // Share props attrs.handle = handle } createScales() { const attrs = this.getChartState(); const dataFinal = attrs.dataFinal; const accessorFunc = attrs.accessorFunc; const isDate = attrs.isDate; const dateScale = attrs.dateScale; const calc = attrs.calc; const groupedInitial = this.group(dataFinal) .by((d, i) => { const field = accessorFunc(d); if (isDate) { return Math.round(dateScale(field)); } return field; }) .orderBy((d) => d.key) .run(); const grouped = groupedInitial.map((d) => Object.assign(d, { value: typeof attrs.aggregator == "function" ? attrs.aggregator(d) : d.values.length }) ); const values = grouped.map((d) => d.value); 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 = calc.chartWidth / minDiff / (maxX - minX); if (eachBarWidth > 20) { eachBarWidth = 20; } if (minDiff < 1) { eachBarWidth = eachBarWidth * minDiff; } if (eachBarWidth < 1) { eachBarWidth = 1; } const scale = attrs.yScale .domain([calc.minY, max]) .range([0, calc.chartHeight - 25]); const scaleY = scale .copy() .domain([max, calc.minY]) .range([0, calc.chartHeight - 25]); const scaleX = d3 .scaleLinear() .domain([minX, maxX]) .range([0, calc.chartWidth]); attrs.scale = scale; attrs.scaleX = scaleX; attrs.scaleY = scaleY; attrs.max = max; attrs.minX = minX; attrs.maxX = maxX; attrs.grouped = grouped; attrs.eachBarWidth = eachBarWidth; attrs.scale = scale; } render() { const that = this; const attrs = this.getChartState(); //Calculated properties var calc = { id: null, chartTopMargin: null, chartLeftMargin: null, chartWidth: null, chartHeight: null, }; calc.id = "ID" + Math.floor(Math.random() * 1000000); // id for event handlings calc.chartLeftMargin = attrs.marginLeft; calc.chartTopMargin = attrs.marginTop; calc.chartWidth = attrs.svgWidth - attrs.marginRight - calc.chartLeftMargin; calc.chartHeight = attrs.svgHeight - attrs.marginBottom - calc.chartTopMargin; calc.minY = attrs.yScale ? 0.0001 : 0; attrs.calc = calc; var accessorFunc = (d) => d; if (attrs.data[0].value != null) { accessorFunc = (d) => d.value; } if (attrs.accessor && typeof attrs.accessor == "function") { accessorFunc = attrs.accessor; } const dataFinal = attrs.data; attrs.accessorFunc = accessorFunc; const isDate = Object.prototype.toString.call(accessorFunc(dataFinal[0])) === "[object Date]"; attrs.isDate = isDate; var dateExtent, dateScale, scaleTime, dateRangesCount, dateRanges, scaleTime; if (isDate) { dateExtent = d3.extent(dataFinal.map(accessorFunc)); dateRangesCount = Math.round(calc.chartWidth / 5); dateScale = d3.scaleTime().domain(dateExtent).range([0, dateRangesCount]); scaleTime = d3.scaleTime().domain(dateExtent).range([0, calc.chartWidth]); dateRanges = d3 .range(dateRangesCount) .map((d) => [dateScale.invert(d), dateScale.invert(d + 1)]); } attrs.dateScale = dateScale; attrs.dataFinal = dataFinal; attrs.scaleTime = scaleTime; this.drawChartTemplate(); var chart = attrs.chart; var svg = attrs.svg; this.createScales(); const scaleX = attrs.scaleX; const scaleY = attrs.scaleY; const max = attrs.max; const grouped = attrs.grouped; const eachBarWidth = attrs.eachBarWidth; const scale = attrs.scale; var axis = d3.axisBottom(scaleX); if (isDate) { axis = d3.axisBottom(scaleTime); } if(attrs.hideXAxis){ axis = axis.tickValues([]); } const axisY = d3 .axisLeft(scaleY) .tickSize(-calc.chartWidth - 20) .ticks(max == 1 ? 1 : attrs.yTicks) .tickFormat(d3.format(".2s")); const bars = chart .patternify({ tag: "rect", selector: "bar", data: grouped }) .attr("class", "bar") .attr("pointer-events", "none") .attr("width", eachBarWidth) .attr("height", (d) => scale(d.value)) .attr("fill", "#424853") .attr("y", (d) => -scale(d.value) + (calc.chartHeight - 25)) .attr("x", (d, i) => scaleX(d.key) - eachBarWidth / 2) .attr("opacity", 0.9); const xAxisWrapper = chart .patternify({ tag: "g", selector: "x-axis" }) .attr("transform", `translate(${0},${calc.chartHeight - 25})`) .call(axis); const yAxisWrapper = chart .patternify({ tag: "g", selector: "y-axis" }) .attr("transform", `translate(${-10},${0})`) .call(axisY); const brush = chart.patternify({ tag: "g", selector: "brush" }).call( d3 .brushX() .extent([ [0, 0], [calc.chartWidth, calc.chartHeight], ]) .on("start", brushStarted) .on("end", brushEnded) .on("brush", brushed) ); attrs.brush = brush; this.drawBrushHandles(); const handle = attrs.handle; chart .selectAll(".selection") .attr("fill-opacity", 0.1) .attr("fill", "white") .attr("stroke-opacity", 0.4); function brushStarted(event) { if (event.selection) { attrs.startSelection = event.selection[0]; } } function brushEnded(event) { const attrs = that.getChartState(); var minX = attrs.minX; var maxX = attrs.maxX; if (!event.selection) { handle.attr("display", "none"); output({ range: [minX, maxX], }); return; } if (event.sourceEvent.type === "brush") return; var d0 = 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]); } } function brushed(event) { if (event.sourceEvent.type === "brush") return; if (attrs.freezeMin) { if (event.selection[0] < attrs.startSelection) { event.selection[1] = Math.min( event.selection[0], event.selection[1] ); } if (event.selection[0] >= attrs.startSelection) { event.selection[1] = Math.max( event.selection[0], event.selection[1] ); } event.selection[0] = 0; d3.select(this).call(event.target.move, event.selection); } var d0 = event.selection.map(scaleX.invert); const s = event.selection; handle.attr("display", null).attr("transform", function (d, i) { return "translate(" + (s[i] - 2) + "," + (calc.chartHeight / 2 - 25) + ")"; }); output({ range: d0, }); } yAxisWrapper.selectAll(".domain").remove(); xAxisWrapper.selectAll(".domain").attr("opacity", 0.1); xAxisWrapper.selectAll("text").attr("fill", "#9CA1AE"); yAxisWrapper.selectAll("text").attr("fill", "#9CA1AE"); svg.selectAll('.selection').attr('transform', 'translate(0,-25)') chart .selectAll(".tick line") .attr("opacity", 0.1) .attr("stroke-dasharray", "2 2"); function output(value) { const result = value; result.data = getData(result.range); if (isDate) { result.range = value.range.map((d) => dateScale.invert(d)); } attrs.onBrush(result); } function getData(range) { const dataBars = bars .attr("fill", "#535966") .filter((d) => { return d.key >= range[0] && d.key <= range[1]; }) .attr("fill", "#72A3B7") .nodes() .map((d) => d.__data__) .map((d) => d.values) .reduce((a, b) => a.concat(b), []); return dataBars; } return this; } updateData(data) { const attrs = this.getChartState(); return this; } // Advanced group by func group(arr) { const that = this; const operations = []; const initialData = arr; const resultObj = {}; let resultArr; let sort = function (a, b) { return a.values.length < b.values.length ? 1 : -1; }; // Group by this.group.by = function (groupFuncs) { const length = arguments.length; for (let j = 0; j < initialData.length; j++) { const dataObj = initialData[j]; const keys = []; for (let i = 0; i < length; i++) { const key = arguments[i]; keys.push(key(dataObj, j)); } const strKey = JSON.stringify(keys); if (!resultObj[strKey]) { resultObj[strKey] = []; } resultObj[strKey].push(dataObj); } operations.push("by"); return that.group; }; // Order by func this.group.orderBy = function (func) { sort = function (a, b) { var a = func(a); var b = func(b); if (typeof a === "string" || a instanceof String) { return a.localeCompare(b); } return a - b; }; operations.push("orderBy"); return that.group; }; // Order by descending func this.group.orderByDescending = function (func) { sort = function (a, b) { var a = func(a); var b = func(b); if (typeof a === "string" || a instanceof String) { return a.localeCompare(b); } return b - a; }; operations.push("orderByDescending"); return that.group; }; // Custom sort this.group.sort = function (v) { sort = v; operations.push("sort"); return that.group; }; // Run result this.group.run = function () { resultArr = Object.keys(resultObj).map((k) => { const result = {}; const keys = JSON.parse(k); if (keys.length == 1) { result.key = keys[0]; } else { result.keys = keys; } result.values = resultObj[k]; return result; }); if (sort) { resultArr.sort(sort); } return resultArr; }; return this.group; }; } function rangeSlider() { return new RangeSlider(); } rangeSlider.RangeSlider = RangeSlider; // Allows for { fastify } rangeSlider.rangeSlider = rangeSlider; // Allows for strict ES Module support rangeSlider.default = rangeSlider; // Sets the default export module.exports = rangeSlider;