UNPKG

timelines-chart

Version:

A parallel (swimlanes) timelines D3 chart for representing state of time-series over time.

971 lines (935 loc) 42.4 kB
import Kapsule from 'kapsule'; import { min, max, range, ascending } from 'd3-array'; import { axisBottom, axisTop, axisRight, axisLeft } from 'd3-axis'; import { scaleUtc, scaleTime, scalePoint, scaleOrdinal, scaleLinear, scaleSequential } from 'd3-scale'; import { select, pointer } from 'd3-selection'; import { utcFormat, timeFormat } from 'd3-time-format'; import d3Tip from 'd3-tip'; import { interpolateRdYlBu, schemeCategory10, schemeSet3 } from 'd3-scale-chromatic'; import { moveToFront, gradient } from 'svg-utils'; import { fitToBox } from 'svg-text-fit'; import ColorLegend from 'd3-color-legend'; import { brushX } from 'd3-brush'; function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); } function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function styleInject(css, ref) { if (ref === void 0) ref = {}; var insertAt = ref.insertAt; if (typeof document === 'undefined') { return; } var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; if (insertAt === 'top') { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } } var css_248z = ".timelines-chart {\n\n text-align: center;\n\n /* Cancel selection interaction */\n -webkit-touch-callout: none;\n -webkit-user-select: none;\n -khtml-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n .timelines-chart .axises line, .timelines-chart .axises path {\n stroke: #808080;\n }\n\n .timelines-chart .axises .x-axis {\n font: 12px sans-serif;\n }\n\n .timelines-chart .axises .x-grid line {\n stroke: #D3D3D3;\n }\n\n .timelines-chart .axises .y-axis line, .timelines-chart .axises .y-axis path, .timelines-chart .axises .grp-axis line, .timelines-chart .axises .grp-axis path {\n stroke: none;\n }\n\n .timelines-chart .axises .y-axis text, .timelines-chart .axises .grp-axis text {\n fill: #2F4F4F;\n }\n\n .timelines-chart line.x-axis-date-marker {\n stroke-width: 1;\n stroke: #293cb7;\n fill: \"none\";\n }\n\n .timelines-chart .series-group {\n fill-opacity: 0.6;\n stroke: #808080;\n stroke-opacity: 0.2;\n }\n\n .timelines-chart .series-segment {\n stroke: none;\n }\n\n .timelines-chart .series-group, .timelines-chart .series-segment {\n cursor: crosshair;\n }\n\n .timelines-chart .legend {\n font-family: Sans-Serif;\n }\n\n .timelines-chart .legend .legendText {\n fill: #666;\n }\n\n .timelines-chart .reset-zoom-btn {\n font-family: sans-serif;\n fill: blue;\n opacity: .6;\n cursor: pointer;\n }\n\n.brusher .grid-background {\n fill: lightgrey;\n }\n\n.brusher .axis path {\n display: none;\n }\n\n.brusher .tick text {\n text-anchor: middle;\n }\n\n.brusher .grid line, .brusher .grid path {\n stroke: #fff;\n }\n\n.chart-zoom-selection, .brusher .brush .selection {\n stroke: blue;\n stroke-opacity: 0.6;\n fill: blue;\n fill-opacity: 0.3;\n shape-rendering: crispEdges;\n}\n\n.chart-tooltip {\n color: #eee;\n background: rgba(0,0,140,0.85);\n padding: 5px;\n border-radius: 3px;\n font: 11px sans-serif;\n z-index: 4000;\n}\n\n.chart-tooltip.group-tooltip {\n font-size: 14px;\n }\n\n.chart-tooltip.line-tooltip {\n font-size: 13px;\n }\n\n.chart-tooltip.group-tooltip, .chart-tooltip.line-tooltip {\n font-weight: bold;\n }\n\n.chart-tooltip.segment-tooltip {\n text-align: center;\n }"; styleInject(css_248z); var TimeOverview = Kapsule({ props: { width: { "default": 300 }, height: { "default": 20 }, margins: { "default": { top: 0, right: 0, bottom: 20, left: 0 } }, scale: {}, domainRange: {}, currentSelection: {}, tickFormat: {}, onChange: { "default": function _default(selectionStart, selectionEnd) {} } }, init: function init(el, state) { state.xGrid = axisBottom().tickFormat(''); state.xAxis = axisBottom().tickPadding(0); state.brush = brushX().handleSize(24).on('end', function (event) { if (!event.sourceEvent) return; var selection = event.selection ? event.selection.map(state.scale.invert) : state.scale.domain(); state.onChange.apply(state, _toConsumableArray(selection)); }); // Build dom state.svg = select(el).append('svg').attr('class', 'brusher'); var brusher = state.svg.append('g').attr('class', 'brusher-margins'); brusher.append('rect').attr('class', 'grid-background'); brusher.append('g').attr('class', 'x grid'); brusher.append('g').attr('class', 'x axis'); brusher.append('g').attr('class', 'brush'); }, update: function update(state) { if (state.domainRange[1] <= state.domainRange[0]) return; var brushWidth = state.width - state.margins.left - state.margins.right, brushHeight = state.height - state.margins.top - state.margins.bottom; state.scale.domain(state.domainRange).range([0, brushWidth]); state.xAxis.scale(state.scale).tickFormat(state.tickFormat); state.xGrid.scale(state.scale).tickSize(-brushHeight); state.svg.attr('width', state.width).attr('height', state.height); state.svg.select('.brusher-margins').attr('transform', "translate(".concat(state.margins.left, ",").concat(state.margins.top, ")")); state.svg.select('.grid-background').attr('width', brushWidth).attr('height', brushHeight); state.svg.select('.x.grid').attr('transform', 'translate(0,' + brushHeight + ')').call(state.xGrid); state.svg.select('.x.axis').attr("transform", "translate(0," + brushHeight + ")").call(state.xAxis).selectAll('text').attr('y', 8); state.svg.select('.brush').call(state.brush.extent([[0, 0], [brushWidth, brushHeight]])).call(state.brush.move, state.currentSelection.map(state.scale)); } }); function alphaNumCmp(a, b) { var alist = a.split(/(\d+)/), blist = b.split(/(\d+)/); alist.length && alist[alist.length - 1] == '' ? alist.pop() : null; // remove the last element if empty blist.length && blist[blist.length - 1] == '' ? blist.pop() : null; // remove the last element if empty for (var i = 0, len = Math.max(alist.length, blist.length); i < len; i++) { if (alist.length == i || blist.length == i) { // Out of bounds for one of the sides return alist.length - blist.length; } if (alist[i] != blist[i]) { // find the first non-equal part if (alist[i].match(/\d/)) // if numeric { return +alist[i] - +blist[i]; // compare as number } else { return alist[i].toLowerCase() > blist[i].toLowerCase() ? 1 : -1; // compare as string } } } return 0; } var timelines = Kapsule({ props: { data: { "default": [], onChange: function onChange(data, state) { parseData(data); state.zoomX = [min(state.completeFlatData, function (d) { return d.timeRange[0]; }), max(state.completeFlatData, function (d) { return d.timeRange[1]; })]; state.zoomY = [null, null]; if (state.overviewArea) { state.overviewArea.domainRange(state.zoomX).currentSelection(state.zoomX); } // function parseData(rawData) { state.completeStructData = []; state.completeFlatData = []; state.totalNLines = 0; for (var i = 0, ilen = rawData.length; i < ilen; i++) { var group = rawData[i].group; state.completeStructData.push({ group: group, lines: rawData[i].data.map(function (d) { return d.label; }) }); for (var j = 0, jlen = rawData[i].data.length; j < jlen; j++) { for (var k = 0, klen = rawData[i].data[j].data.length; k < klen; k++) { var _rawData$i$data$j$dat = rawData[i].data[j].data[k], timeRange = _rawData$i$data$j$dat.timeRange, val = _rawData$i$data$j$dat.val, labelVal = _rawData$i$data$j$dat.labelVal; state.completeFlatData.push({ group: group, label: rawData[i].data[j].label, timeRange: timeRange.map(function (d) { return new Date(d); }), val: val, labelVal: labelVal !== undefined ? labelVal : val, data: rawData[i].data[j].data[k] }); } state.totalNLines++; } } } } }, width: { "default": window.innerWidth }, maxHeight: { "default": 640 }, maxLineHeight: { "default": 12 }, leftMargin: { "default": 90 }, rightMargin: { "default": 100 }, topMargin: { "default": 26 }, bottomMargin: { "default": 30 }, useUtc: { "default": false }, xTickFormat: {}, dateMarker: {}, timeFormat: { "default": '%Y-%m-%d %-I:%M:%S %p', triggerUpdate: false }, zoomX: { // Which time-range to show (null = min/max) "default": [null, null], onChange: function onChange(zoomX, state) { if (state.svg) state.svg.dispatch('zoom', { detail: { zoomX: zoomX, zoomY: null, redraw: false } }); } }, zoomY: { // Which lines to show (null = min/max) [0 indexed] "default": [null, null], onChange: function onChange(zoomY, state) { if (state.svg) state.svg.dispatch('zoom', { detail: { zoomX: null, zoomY: zoomY, redraw: false } }); } }, minSegmentDuration: {}, minSegmentWidth: { "default": 1 }, zColorScale: { "default": scaleSequential(interpolateRdYlBu) }, zQualitative: { "default": false, onChange: function onChange(discrete, state) { state.zColorScale = discrete ? scaleOrdinal([].concat(_toConsumableArray(schemeCategory10), _toConsumableArray(schemeSet3))) : scaleSequential(interpolateRdYlBu); // alt: d3.interpolateInferno } }, zDataLabel: { "default": '', triggerUpdate: false }, // Units of z data. Used in the tooltip descriptions zScaleLabel: { "default": '', triggerUpdate: false }, // Units of colorScale. Used in the legend label enableOverview: { "default": true }, // True/False enableAnimations: { "default": true, onChange: function onChange(val, state) { state.transDuration = val ? 700 : 0; } }, segmentTooltipContent: { triggerUpdate: false }, // Callbacks onZoom: {}, // When user zooms in / resets zoom. Returns ([startX, endX], [startY, endY]) onLabelClick: {}, // When user clicks on a group or y label. Returns (group) or (label, group) respectively onSegmentClick: {} // When user clicks on a segment. Returns (segment object) respectively }, methods: { getNLines: function getNLines(s) { return s.nLines; }, getTotalNLines: function getTotalNLines(s) { return s.totalNLines; }, getVisibleStructure: function getVisibleStructure(s) { return s.structData; }, getSvg: function getSvg(s) { return select(s.svg.node().parentNode).html(); }, zoomYLabels: function zoomYLabels(state, _) { if (!_) { return [y2Label(state.zoomY[0]), y2Label(state.zoomY[1])]; } return this.zoomY([label2Y(_[0], true), label2Y(_[1], false)]); // function y2Label(y) { if (y == null) return y; var cntDwn = y; for (var i = 0, len = state.completeStructData.length; i < len; i++) { if (state.completeStructData[i].lines.length > cntDwn) return getIdxLine(state.completeStructData[i], cntDwn); cntDwn -= state.completeStructData[i].lines.length; } // y larger than all lines, return last return getIdxLine(state.completeStructData[state.completeStructData.length - 1], state.completeStructData[state.completeStructData.length - 1].lines.length - 1); // function getIdxLine(grpData, idx) { return { 'group': grpData.group, 'label': grpData.lines[idx] }; } } function label2Y(label, useIdxAfterIfNotFound) { useIdxAfterIfNotFound = useIdxAfterIfNotFound || false; var subIdxNotFound = useIdxAfterIfNotFound ? 0 : 1; if (label == null) return label; var idx = 0; for (var i = 0, lenI = state.completeStructData.length; i < lenI; i++) { var grpCmp = state.grpCmpFunction(label.group, state.completeStructData[i].group); if (grpCmp < 0) break; if (grpCmp == 0 && label.group == state.completeStructData[i].group) { for (var j = 0, lenJ = state.completeStructData[i].lines.length; j < lenJ; j++) { var cmpRes = state.labelCmpFunction(label.label, state.completeStructData[i].lines[j]); if (cmpRes < 0) { return idx + j - subIdxNotFound; } if (cmpRes == 0 && label.label == state.completeStructData[i].lines[j]) { return idx + j; } } return idx + state.completeStructData[i].lines.length - subIdxNotFound; } idx += state.completeStructData[i].lines.length; } return idx - subIdxNotFound; } }, sort: function sort(state, labelCmpFunction, grpCmpFunction) { if (labelCmpFunction == null) { labelCmpFunction = state.labelCmpFunction; } if (grpCmpFunction == null) { grpCmpFunction = state.grpCmpFunction; } state.labelCmpFunction = labelCmpFunction; state.grpCmpFunction = grpCmpFunction; state.completeStructData.sort(function (a, b) { return grpCmpFunction(a.group, b.group); }); for (var i = 0, len = state.completeStructData.length; i < len; i++) { state.completeStructData[i].lines.sort(labelCmpFunction); } state._rerender(); return this; }, sortAlpha: function sortAlpha(state, asc) { if (asc == null) { asc = true; } var alphaCmp = function alphaCmp(a, b) { return alphaNumCmp(asc ? a : b, asc ? b : a); }; return this.sort(alphaCmp, alphaCmp); }, sortChrono: function sortChrono(state, asc) { if (asc == null) { asc = true; } function buildIdx(accessFunction) { var idx = {}; var _loop = function _loop() { var key = accessFunction(state.completeFlatData[i]); if (idx.hasOwnProperty(key)) { return 1; // continue } var itmList = state.completeFlatData.filter(function (d) { return key == accessFunction(d); }); idx[key] = [min(itmList, function (d) { return d.timeRange[0]; }), max(itmList, function (d) { return d.timeRange[1]; })]; }; for (var i = 0, len = state.completeFlatData.length; i < len; i++) { if (_loop()) continue; } return idx; } var timeCmp = function timeCmp(a, b) { var aT = a[1], bT = b[1]; if (!aT || !bT) return null; // One of the two vals is null if (aT[1].getTime() == bT[1].getTime()) { if (aT[0].getTime() == bT[0].getTime()) { return alphaNumCmp(a[0], b[0]); // If first and last is same, use alphaNum } return aT[0] - bT[0]; // If last is same, earliest first wins } return bT[1] - aT[1]; // latest last wins }; function getCmpFunction(accessFunction, asc) { return function (a, b) { return timeCmp(accessFunction(asc ? a : b), accessFunction(asc ? b : a)); }; } var grpIdx = buildIdx(function (d) { return d.group; }); var lblIdx = buildIdx(function (d) { return d.label; }); var grpCmp = getCmpFunction(function (d) { return [d, grpIdx[d] || null]; }, asc); var lblCmp = getCmpFunction(function (d) { return [d, lblIdx[d] || null]; }, asc); return this.sort(lblCmp, grpCmp); }, overviewDomain: function overviewDomain(state, _) { if (!state.enableOverview) { return null; } if (!_) { return state.overviewArea.domainRange(); } state.overviewArea.domainRange(_); return this; }, refresh: function refresh(state) { state._rerender(); return this; } }, stateInit: { height: null, overviewHeight: 20, // Height of overview section in bottom minLabelFont: 2, groupBkgGradient: ['#FAFAFA', '#E0E0E0'], yScale: null, grpScale: null, xAxis: null, xGrid: null, yAxis: null, grpAxis: null, dateMarkerLine: null, svg: null, graph: null, overviewAreaElem: null, overviewArea: null, graphW: null, graphH: null, completeStructData: null, structData: null, completeFlatData: null, flatData: null, totalNLines: null, nLines: null, minSegmentDuration: 0, // ms transDuration: 700, // ms for transition duration labelCmpFunction: alphaNumCmp, grpCmpFunction: alphaNumCmp }, init: function init(el, state) { var elem = select(el).attr('class', 'timelines-chart'); state.svg = elem.append('svg'); state.overviewAreaElem = elem.append('div'); // Initialize scales and axes state.yScale = scalePoint(); state.grpScale = scaleOrdinal(); state.xAxis = axisBottom(); state.xGrid = axisTop(); state.yAxis = axisRight(); state.grpAxis = axisLeft(); buildDomStructure(); addTooltips(); addZoomSelection(); setEvents(); // function buildDomStructure() { state.yScale.invert = invertOrdinal; state.grpScale.invert = invertOrdinal; state.groupGradId = gradient().colorScale(scaleLinear().domain([0, 1]).range(state.groupBkgGradient)).angle(-90)(state.svg.node()).id(); var axises = state.svg.append('g').attr('class', 'axises'); axises.append('g').attr('class', 'x-axis'); axises.append('g').attr('class', 'x-grid'); axises.append('g').attr('class', 'y-axis'); axises.append('g').attr('class', 'grp-axis'); state.yAxis.scale(state.yScale).tickSize(0); state.grpAxis.scale(state.grpScale).tickSize(0); state.colorLegend = ColorLegend()(state.svg.append('g').attr('class', 'legendG').node()); state.graph = state.svg.append('g'); state.dateMarkerLine = state.svg.append('line').attr('class', 'x-axis-date-marker'); if (state.enableOverview) { addOverviewArea(); } // Applies to ordinal scales (invert not supported in d3) function invertOrdinal(val, cmpFunc) { cmpFunc = cmpFunc || function (a, b) { return a >= b; }; var scDomain = this.domain(); var scRange = this.range(); if (scRange.length === 2 && scDomain.length !== 2) { // Special case, interpolate range vals scRange = range(scRange[0], scRange[1], (scRange[1] - scRange[0]) / scDomain.length); } var bias = scRange[0]; for (var i = 0, len = scRange.length; i < len; i++) { if (cmpFunc(scRange[i] + bias, val)) { return scDomain[Math.round(i * scDomain.length / scRange.length)]; } } return this.domain()[this.domain().length - 1]; } function addOverviewArea() { state.overviewArea = TimeOverview().margins({ top: 1, right: 20, bottom: 20, left: 20 }).onChange(function (startTime, endTime) { state.svg.dispatch('zoom', { detail: { zoomX: [startTime, endTime], zoomY: null } }); }).domainRange(state.zoomX).currentSelection(state.zoomX)(state.overviewAreaElem.node()); state.svg.on('zoomScent', function (event) { var zoomX = event.detail.zoomX; if (!state.overviewArea || !zoomX) return; // Out of overview bounds > extend it if (zoomX[0] < state.overviewArea.domainRange()[0] || zoomX[1] > state.overviewArea.domainRange()[1]) { state.overviewArea.domainRange([new Date(Math.min(zoomX[0], state.overviewArea.domainRange()[0])), new Date(Math.max(zoomX[1], state.overviewArea.domainRange()[1]))]); } state.overviewArea.currentSelection(zoomX); }); } } function addTooltips() { state.groupTooltip = d3Tip().attr('class', 'chart-tooltip group-tooltip').direction('w').offset([0, 0]).html(function (event, d) { var leftPush = d.hasOwnProperty('timeRange') ? state.xScale(d.timeRange[0]) : 0; var topPush = d.hasOwnProperty('label') ? state.grpScale(d.group) - state.yScale(d.group + '+&+' + d.label) : 0; state.groupTooltip.offset([topPush, -leftPush]); return d.group; }); state.svg.call(state.groupTooltip); state.lineTooltip = d3Tip().attr('class', 'chart-tooltip line-tooltip').direction('e').offset([0, 0]).html(function (event, d) { var rightPush = d.hasOwnProperty('timeRange') ? state.xScale.range()[1] - state.xScale(d.timeRange[1]) : 0; state.lineTooltip.offset([0, rightPush]); return d.label; }); state.svg.call(state.lineTooltip); state.segmentTooltip = d3Tip().attr('class', 'chart-tooltip segment-tooltip').direction('s').offset([5, 0]).html(function (event, d) { if (state.segmentTooltipContent) { return state.segmentTooltipContent(d); } var normVal = state.zColorScale.domain()[state.zColorScale.domain().length - 1] - state.zColorScale.domain()[0]; var dateFormat = (state.useUtc ? utcFormat : timeFormat)("".concat(state.timeFormat).concat(state.useUtc ? ' (UTC)' : '')); return '<strong>' + d.labelVal + ' </strong>' + state.zDataLabel + (normVal ? ' (<strong>' + Math.round((d.val - state.zColorScale.domain()[0]) / normVal * 100 * 100) / 100 + '%</strong>)' : '') + '<br>' + '<strong>From: </strong>' + dateFormat(d.timeRange[0]) + '<br>' + '<strong>To: </strong>' + dateFormat(d.timeRange[1]); }); state.svg.call(state.segmentTooltip); } function addZoomSelection() { var getPointerCoords = function getPointerCoords(event) { return pointer(event, state.graph.node()); }; state.graph.on('mousedown', function (event) { if (select(window).on('mousemove.zoomRect') != null) // Selection already active return; var startCoords = getPointerCoords(event); if (startCoords[0] < 0 || startCoords[0] > state.graphW || startCoords[1] < 0 || startCoords[1] > state.graphH) return; state.disableHover = true; var rect = state.graph.append('rect').attr('class', 'chart-zoom-selection'); select(window).on('mousemove.zoomRect', function (event) { event.stopPropagation(); var pointerCoords = getPointerCoords(event); var newCoords = [Math.max(0, Math.min(state.graphW, pointerCoords[0])), Math.max(0, Math.min(state.graphH, pointerCoords[1]))]; rect.attr('x', Math.min(startCoords[0], newCoords[0])).attr('y', Math.min(startCoords[1], newCoords[1])).attr('width', Math.abs(newCoords[0] - startCoords[0])).attr('height', Math.abs(newCoords[1] - startCoords[1])); state.svg.dispatch('zoomScent', { detail: { zoomX: [startCoords[0], newCoords[0]].sort(ascending).map(state.xScale.invert), zoomY: [startCoords[1], newCoords[1]].sort(ascending).map(function (d) { return state.yScale.domain().indexOf(state.yScale.invert(d)) + (state.zoomY && state.zoomY[0] ? state.zoomY[0] : 0); }) } }); }).on('mouseup.zoomRect', function (event) { select(window).on('mousemove.zoomRect', null).on('mouseup.zoomRect', null); select('body').classed('stat-noselect', false); rect.remove(); state.disableHover = false; var pointerCoords = getPointerCoords(event); var endCoords = [Math.max(0, Math.min(state.graphW, pointerCoords[0])), Math.max(0, Math.min(state.graphH, pointerCoords[1]))]; if (startCoords[0] == endCoords[0] && startCoords[1] == endCoords[1]) return; var newDomainX = [startCoords[0], endCoords[0]].sort(ascending).map(state.xScale.invert); var newDomainY = [startCoords[1], endCoords[1]].sort(ascending).map(function (d) { return state.yScale.domain().indexOf(state.yScale.invert(d)) + (state.zoomY && state.zoomY[0] ? state.zoomY[0] : 0); }); var changeX = newDomainX[1] - newDomainX[0] > 1000; // Zoom damper var changeY = newDomainY[0] != state.zoomY[0] || newDomainY[1] != state.zoomY[1]; if (changeX || changeY) { state.svg.dispatch('zoom', { detail: { zoomX: changeX ? newDomainX : null, zoomY: changeY ? newDomainY : null } }); } }, true); event.stopPropagation(); }); state.resetBtn = state.svg.append('text').attr('class', 'reset-zoom-btn').text('Reset Zoom').style('text-anchor', 'end').on('mouseup', function () { state.svg.dispatch('resetZoom'); }).on('mouseover', function () { select(this).style('opacity', 1); }).on('mouseout', function () { select(this).style('opacity', .6); }); } function setEvents() { state.svg.on('zoom', function (event) { var evData = event.detail, zoomX = evData.zoomX, zoomY = evData.zoomY, redraw = evData.redraw == null ? true : evData.redraw; if (!zoomX && !zoomY) return; if (zoomX) state.zoomX = zoomX; if (zoomY) state.zoomY = zoomY; state.svg.dispatch('zoomScent', { detail: { zoomX: zoomX, zoomY: zoomY } }); if (!redraw) return; state._rerender(); if (state.onZoom) state.onZoom(state.zoomX, state.zoomY); }); state.svg.on('resetZoom', function () { var prevZoomX = state.zoomX; var prevZoomY = state.zoomY || [null, null]; var newZoomX = state.enableOverview ? state.overviewArea.domainRange() : [min(state.flatData, function (d) { return d.timeRange[0]; }), max(state.flatData, function (d) { return d.timeRange[1]; })], newZoomY = [null, null]; if (prevZoomX[0] < newZoomX[0] || prevZoomX[1] > newZoomX[1] || prevZoomY[0] != newZoomY[0] || prevZoomY[1] != newZoomX[1]) { state.zoomX = [new Date(Math.min(prevZoomX[0], newZoomX[0])), new Date(Math.max(prevZoomX[1], newZoomX[1]))]; state.zoomY = newZoomY; state.svg.dispatch('zoomScent', { detail: { zoomX: state.zoomX, zoomY: state.zoomY } }); state._rerender(); } if (state.onZoom) state.onZoom(null, null); }); } }, update: function update(state) { applyFilters(); setupDimensions(); adjustXScale(); adjustYScale(); adjustGrpScale(); renderAxises(); renderGroups(); renderTimelines(); adjustLegend(); // function applyFilters() { // Flat data based on segment length state.flatData = state.minSegmentDuration > 0 ? state.completeFlatData.filter(function (d) { return d.timeRange[1] - d.timeRange[0] >= state.minSegmentDuration; }) : state.completeFlatData; // zoomY if (state.zoomY == null || state.zoomY == [null, null]) { state.structData = state.completeStructData; state.nLines = 0; for (var i = 0, len = state.structData.length; i < len; i++) { state.nLines += state.structData[i].lines.length; } return; } state.structData = []; var cntDwn = [state.zoomY[0] == null ? 0 : state.zoomY[0]]; // Initial threshold cntDwn.push(Math.max(0, (state.zoomY[1] == null ? state.totalNLines : state.zoomY[1] + 1) - cntDwn[0])); // Number of lines state.nLines = cntDwn[1]; var _loop2 = function _loop2(_i) { var validLines = state.completeStructData[_i].lines; if (state.minSegmentDuration > 0) { // Use only non-filtered (due to segment length) groups/labels if (!state.flatData.some(function (d) { return d.group == state.completeStructData[_i].group; })) { return 0; // continue // No data for this group } validLines = state.completeStructData[_i].lines.filter(function (d) { return state.flatData.some(function (dd) { return dd.group == state.completeStructData[_i].group && dd.label == d; }); }); } if (cntDwn[0] >= validLines.length) { // Ignore whole group (before start) cntDwn[0] -= validLines.length; return 0; // continue } var groupData = { group: state.completeStructData[_i].group, lines: null }; if (validLines.length - cntDwn[0] >= cntDwn[1]) { // Last (or first && last) group (partial) groupData.lines = validLines.slice(cntDwn[0], cntDwn[1] + cntDwn[0]); state.structData.push(groupData); cntDwn[1] = 0; return 1; // break } if (cntDwn[0] > 0) { // First group (partial) groupData.lines = validLines.slice(cntDwn[0]); cntDwn[0] = 0; } else { // Middle group (full fit) groupData.lines = validLines; } state.structData.push(groupData); cntDwn[1] -= groupData.lines.length; }, _ret; for (var _i = 0, _len = state.completeStructData.length; _i < _len; _i++) { _ret = _loop2(_i); if (_ret === 0) continue; if (_ret === 1) break; } state.nLines -= cntDwn[1]; } function setupDimensions() { state.graphW = state.width - state.leftMargin - state.rightMargin; state.graphH = min([state.nLines * state.maxLineHeight, state.maxHeight - state.topMargin - state.bottomMargin]); state.height = state.graphH + state.topMargin + state.bottomMargin; state.svg.transition().duration(state.transDuration).attr('width', state.width).attr('height', state.height); state.graph.attr('transform', 'translate(' + state.leftMargin + ',' + state.topMargin + ')'); if (state.overviewArea) { state.overviewArea.width(state.width * 0.8).height(state.overviewHeight + state.overviewArea.margins().top + state.overviewArea.margins().bottom); } } function adjustXScale() { state.zoomX[0] = state.zoomX[0] || min(state.flatData, function (d) { return d.timeRange[0]; }); state.zoomX[1] = state.zoomX[1] || max(state.flatData, function (d) { return d.timeRange[1]; }); state.xScale = (state.useUtc ? scaleUtc : scaleTime)().domain(state.zoomX).range([0, state.graphW]).clamp(true); if (state.overviewArea) { state.overviewArea.scale(state.xScale.copy()).tickFormat(state.xTickFormat); } } function adjustYScale() { var labels = []; var _loop3 = function _loop3(i) { labels = labels.concat(state.structData[i].lines.map(function (d) { return state.structData[i].group + '+&+' + d; })); }; for (var i = 0, len = state.structData.length; i < len; i++) { _loop3(i); } state.yScale.domain(labels); state.yScale.range([state.graphH / labels.length * 0.5, state.graphH * (1 - 0.5 / labels.length)]); } function adjustGrpScale() { state.grpScale.domain(state.structData.map(function (d) { return d.group; })); var cntLines = 0; state.grpScale.range(state.structData.map(function (d) { var pos = (cntLines + d.lines.length / 2) / state.nLines * state.graphH; cntLines += d.lines.length; return pos; })); } function adjustLegend() { state.svg.select('.legendG').transition().duration(state.transDuration).attr('transform', "translate(".concat(state.leftMargin + state.graphW * 0.05, ",2)")); state.colorLegend.width(Math.max(120, state.graphW / 3 * (state.zQualitative ? 2 : 1))).height(state.topMargin * .6).scale(state.zColorScale).label(state.zScaleLabel); state.resetBtn.transition().duration(state.transDuration).attr('x', state.leftMargin + state.graphW * .99).attr('y', state.topMargin * .8); fitToBox().bbox({ width: state.graphW * .4, height: Math.min(13, state.topMargin * .8) })(state.resetBtn.node()); } function renderAxises() { state.svg.select('.axises').attr('transform', 'translate(' + state.leftMargin + ',' + state.topMargin + ')'); // X var nXTicks = Math.max(2, Math.min(12, Math.round(state.graphW * 0.012))); state.xAxis.scale(state.xScale).ticks(nXTicks).tickFormat(state.xTickFormat); state.xGrid.scale(state.xScale).ticks(nXTicks).tickFormat(''); state.svg.select('g.x-axis').style('stroke-opacity', 0).style('fill-opacity', 0).attr('transform', 'translate(0,' + state.graphH + ')').transition().duration(state.transDuration).call(state.xAxis).style('stroke-opacity', 1).style('fill-opacity', 1); /* Angled x axis labels state.svg.select('g.x-axis').selectAll('text') .style('text-anchor', 'end') .attr('transform', 'translate(-10, 3) rotate(-60)'); */ state.xGrid.tickSize(state.graphH); state.svg.select('g.x-grid').attr('transform', 'translate(0,' + state.graphH + ')').transition().duration(state.transDuration).call(state.xGrid); if (state.dateMarker && state.dateMarker >= state.xScale.domain()[0] && state.dateMarker <= state.xScale.domain()[1]) { state.dateMarkerLine.style('display', 'block').transition().duration(state.transDuration).attr('x1', state.xScale(state.dateMarker) + state.leftMargin).attr('x2', state.xScale(state.dateMarker) + state.leftMargin).attr('y1', state.topMargin + 1).attr('y2', state.graphH + state.topMargin); } else { state.dateMarkerLine.style('display', 'none'); } // Y var fontVerticalMargin = 0.6; var labelDisplayRatio = Math.ceil(state.nLines * state.minLabelFont / Math.sqrt(2) / state.graphH / fontVerticalMargin); var tickVals = state.yScale.domain().filter(function (d, i) { return !(i % labelDisplayRatio); }); var fontSize = Math.min(12, state.graphH / tickVals.length * fontVerticalMargin * Math.sqrt(2)); var maxChars = Math.ceil(state.rightMargin / (fontSize / Math.sqrt(2))); state.yAxis.tickValues(tickVals); state.yAxis.tickFormat(function (d) { return reduceLabel(d.split('+&+')[1], maxChars); }); state.svg.select('g.y-axis').transition().duration(state.transDuration).attr('transform', 'translate(' + state.graphW + ', 0)').style('font-size', fontSize + 'px').call(state.yAxis); // Grp var minHeight = min(state.grpScale.range(), function (d, i) { return i > 0 ? d - state.grpScale.range()[i - 1] : d * 2; }); fontSize = Math.min(14, minHeight * fontVerticalMargin * Math.sqrt(2)); maxChars = Math.floor(state.leftMargin / (fontSize / Math.sqrt(2))); state.grpAxis.tickFormat(function (d) { return reduceLabel(d, maxChars); }); state.svg.select('g.grp-axis').transition().duration(state.transDuration).style('font-size', fontSize + 'px').call(state.grpAxis); // Make Axises clickable if (state.onLabelClick) { state.svg.selectAll('g.y-axis,g.grp-axis').selectAll('text').style('cursor', 'pointer').on('click', function (ev, d) { var segms = d.split('+&+'); state.onLabelClick.apply(state, _toConsumableArray(segms.reverse())); }); } // function reduceLabel(label, maxChars) { return label.length <= maxChars ? label : label.substring(0, maxChars * 2 / 3) + '...' + label.substring(label.length - maxChars / 3, label.length); } } function renderGroups() { var groups = state.graph.selectAll('rect.series-group').data(state.structData, function (d) { return d.group; }); groups.exit().transition().duration(state.transDuration).style('stroke-opacity', 0).style('fill-opacity', 0).remove(); var newGroups = groups.enter().append('rect').attr('class', 'series-group').attr('x', 0).attr('y', 0).attr('height', 0).style('fill', 'url(#' + state.groupGradId + ')').on('mouseover', state.groupTooltip.show).on('mouseout', state.groupTooltip.hide); newGroups.append('title').text('click-drag to zoom in'); groups = groups.merge(newGroups); groups.transition().duration(state.transDuration).attr('width', state.graphW).attr('height', function (d) { return state.graphH * d.lines.length / state.nLines; }).attr('y', function (d) { return state.grpScale(d.group) - state.graphH * d.lines.length / state.nLines / 2; }); } function renderTimelines(maxElems) { var hoverEnlargeRatio = .4; var dataFilter = function dataFilter(d, i) { return state.grpScale.domain().indexOf(d.group) + 1 && d.timeRange[1] >= state.xScale.domain()[0] && d.timeRange[0] <= state.xScale.domain()[1] && state.yScale.domain().indexOf(d.group + '+&+' + d.label) + 1; }; state.lineHeight = state.graphH / state.nLines * 0.8; var timelines = state.graph.selectAll('rect.series-segment').data(state.flatData.filter(dataFilter), function (d) { return d.group + d.label + d.timeRange[0]; }); timelines.exit().transition().duration(state.transDuration).style('fill-opacity', 0).remove(); var newSegments = timelines.enter().append('rect').attr('class', 'series-segment').attr('rx', 1).attr('ry', 1).attr('x', state.graphW / 2).attr('y', state.graphH / 2).attr('width', 0).attr('height', 0).style('fill', function (d) { return state.zColorScale(d.val); }).style('fill-opacity', 0).on('mouseover.groupTooltip', state.groupTooltip.show).on('mouseout.groupTooltip', state.groupTooltip.hide).on('mouseover.lineTooltip', state.lineTooltip.show).on('mouseout.lineTooltip', state.lineTooltip.hide).on('mouseover.segmentTooltip', state.segmentTooltip.show).on('mouseout.segmentTooltip', state.segmentTooltip.hide); newSegments.on('mouseover', function () { if ('disableHover' in state && state.disableHover) return; this.nextElementSibling && moveToFront()(this); var hoverEnlarge = state.lineHeight * hoverEnlargeRatio; select(this).transition().duration(70).attr('x', function (d) { return state.xScale(d.timeRange[0]) - hoverEnlarge / 2; }).attr('width', function (d) { return max([state.minSegmentWidth, state.xScale(d.timeRange[1]) - state.xScale(d.timeRange[0])]) + hoverEnlarge; }).attr('y', function (d) { return state.yScale(d.group + '+&+' + d.label) - (state.lineHeight + hoverEnlarge) / 2; }).attr('height', state.lineHeight + hoverEnlarge).style('fill-opacity', 1); }).on('mouseout', function () { select(this).transition().duration(250).attr('x', function (d) { return state.xScale(d.timeRange[0]); }).attr('width', function (d) { return max([state.minSegmentWidth, state.xScale(d.timeRange[1]) - state.xScale(d.timeRange[0])]); }).attr('y', function (d) { return state.yScale(d.group + '+&+' + d.label) - state.lineHeight / 2; }).attr('height', state.lineHeight).style('fill-opacity', .8); }).on('click', function (ev, s) { if (state.onSegmentClick) state.onSegmentClick(s); }); timelines = timelines.merge(newSegments); timelines.transition().duration(state.transDuration).attr('x', function (d) { return state.xScale(d.timeRange[0]); }).attr('width', function (d) { return max([state.minSegmentWidth, state.xScale(d.timeRange[1]) - state.xScale(d.timeRange[0])]); }).attr('y', function (d) { return state.yScale(d.group + '+&+' + d.label) - state.lineHeight / 2; }).attr('height', state.lineHeight).style('fill', function (d) { return state.zColorScale(d.val); }).style('fill-opacity', .8); } } }); export { timelines as default };