export const byDate = events.reduce((acc, event) => { const skey = dateScale(getTime(event.startdate)) const ekey = dateScale(getTime(event.enddate)) range(skey, ekey).map(key => acc[key].push(event)) return acc }, range(0, 101).map(() => []))
tape('kde generates samples', function(t) { var kde = stats.randomKDE(d3.range(0, 1000) .map(gaussian.sample)); check(t, 0, 1, samples(kde, 1000)); kde = stats.randomKDE(d3.range(0, 1000) .map(function() { return 5 * gaussian.sample(); })); check(t, 0, 5, samples(kde, 1000)); t.end(); });
function generateData() { const stack = d3Stack() .keys(range(NUMBER_OF_LAYERS)) .offset(stackOffsetWiggle); const transposed = transpose( range(NUMBER_OF_LAYERS).map(() => bumps(SAMPLES_PER_LAYER, BUMPS_PER_LAYER)) ); return stack(transposed).map(series => series.map((row, x) => ({x, y0: row[0], y: row[1]})) ); }
tape('kde approximates the cdf', function(t) { var data = d3.range(0, 1000).map(gaussian.sample), kde = stats.randomKDE(data), domain = d3.range(-5, 5.1, 0.5), error = domain.map(function(x) { return Math.abs(kde.cdf(x) - gaussian.cdf(x)); }); t.ok((d3.sum(error) / domain.length) < 0.01); t.end(); });
const sevenDaysDataMixedMinimum = (type = 'cbg') => sinon.stub().returns( _.map(range(0, (devices.dexcom.cgmInDay / 4) * extentSize), () => ({ id: chance.hash({ length: 6 }), deviceId: devices.dexcom.id, msPer24: chance.integer({ min: 0, max: 864e5 }), type, value: chance.pickone([lowestBg, 525]), })).concat(_.map(range(0, (devices.libre.cgmInDay / 4) * extentSize), () => ({ id: chance.hash({ length: 6 }), deviceId: devices.libre.id, msPer24: chance.integer({ min: 0, max: 864e5 }), type, value: chance.pickone([lowestBg, 525]), }))) );
// Inspired by Bostock's version of Lee Byron’s test data generator. function bumps(samplesPerLayer, bumpsPerLayer) { const dataOutline = new Array(samplesPerLayer).fill(0); return range(bumpsPerLayer).reduce( res => bump(res, samplesPerLayer), dataOutline ); }
function populateScene(opts) { var sceneSize; var occupiedSpots; if (opts) { sceneSize = opts.sceneSize; occupiedSpots = opts.occupiedSpots; } var sceneMap = range(sceneSize[0]).map(makeColumn); markSpots(sceneMap, occupiedSpots, 'x'); const sceneType = sceneTable.roll(); // console.log(sceneType); populatorForScene[sceneType]({ probable: probable, sceneMap: sceneMap }); // addFlames return sceneMap; function makeColumn() { return range(sceneSize[1]).map(() => '.'); } }
grouped.offsetScaleForDatum = (data, d, i) => { const width = bandwidth(d, i); const offset = alignOffset(align, width); const halfWidth = width / 2; return offsetScale .domain(range(0, data.length)) .range([-halfWidth + offset, halfWidth + offset]); };
const sevenDaysDataMmol = (device = devices.dexcom, type = 'cbg') => sinon.stub().returns( _.map(range(0, device.cgmInDay * extentSize), () => ({ id: chance.hash({ length: 6 }), deviceId: device.id, msPer24: chance.integer({ min: 0, max: 864e5 }), type, value: chance.pickone([lowestBgMmol, 28.4]), })) );
// partition viewport into `psize` x `psize` regions function partitionViewport(psize, projection) { var dimensions = projection.clipExtent()[1]; psize = psize || 16; var cols = d3_range(0, dimensions[0], psize); var rows = d3_range(0, dimensions[1], psize); var partitions = []; rows.forEach(function(y) { cols.forEach(function(x) { var min = [x, y + psize]; var max = [x + psize, y]; partitions.push( geoExtent(projection.invert(min), projection.invert(max))); }); }); return partitions; }
tape("quadtree.find(x, y) returns the closest point to the given [x, y]", function(test) { var dx = 17, dy = 17, q = d3_quadtree.quadtree(); d3_array.range(dx * dy).forEach(function(i) { q.add([i % dx, i / dx | 0]); }); test.deepEqual(q.find( 0.1, 0.1), [ 0, 0]); test.deepEqual(q.find( 7.1, 7.1), [ 7, 7]); test.deepEqual(q.find( 0.1, 15.9), [ 0, 16]); test.deepEqual(q.find(15.9, 15.9), [16, 16]); test.end(); });
const generateData = () => { const dataCount = document.getElementById('label-count').value; data = range(0, document.getElementById('label-count').value) .map((_, i) => { return { x: Math.random() * width, y: Math.random() * height, data: 'node-' + i }; }); };
test('axis-utils #generateFit', t => { t.deepEqual(generateFit({x: 0, y: 0}, {x: 1, y: 1}), {left: 0, offset: 0, right: 1, slope: 1}); t.deepEqual(generateFit({x: 0, y: 0}, {x: 0, y: 1}), {left: 0, offset: 0, right: 1, slope: 0}); t.deepEqual(generateFit({x: 0, y: 0}, {x: 0, y: -1}), {left: 0, offset: 0, right: -1, slope: 0}); const result = generateFit({x: 175, y: 125}, {x: 17.33044811707665, y: 179.23546738969475}); const numberOfTicks = 5; const left = result.left; const right = result.right; const pointSlope = (right - left) / numberOfTicks; const lengthOfGeneratedPoints = range(left, right + pointSlope, pointSlope).length; t.equal(lengthOfGeneratedPoints, 7, 'should be 7, incorrect length of generated points'); t.end(); });
function rescale() { var n = domain().length, reverse = range[1] < range[0], start = range[reverse - 0], stop = range[1 - reverse]; step = (stop - start) / Math.max(1, n - paddingInner + paddingOuter * 2); if (round) step = Math.floor(step); start += (stop - start - step * (n - paddingInner)) * align; bandwidth = step * (1 - paddingInner); if (round) start = Math.round(start), bandwidth = Math.round(bandwidth); var values = sequence(n).map(function(i) { return start + step * i; }); return ordinalRange(reverse ? values.reverse() : values); }
function calculateAreaOfPoints(data) { var xyData = data.map((point) => [x(point), y(point)]); var pointAreas = range(1, xyData.length - 1).map((i) => { var lastPoint = xyData[i - 1]; var thisPoint = xyData[i]; var nextPoint = xyData[i + 1]; return 0.5 * Math.abs((lastPoint[0] - nextPoint[0]) * (thisPoint[1] - lastPoint[1]) - (lastPoint[0] - thisPoint[0]) * (nextPoint[1] - lastPoint[1])); }); return pointAreas; }
generator(type, absolute, yformat, axis, scale, height, tickCount, min, max) { let axisGenerator; if (type === "linear" || type === "power") { if (tickCount > 0) { const stepSize = (max - min) / (tickCount - 1); axisGenerator = axis(scale) .tickValues(range(min, max + max / 10000, stepSize)) .tickFormat(d => { if (absolute) { return yformat(Math.abs(d)); } return yformat(d); }) .tickSizeOuter(0); } else { if (height <= 200) { axisGenerator = axis(scale) .ticks(4) .tickFormat(d => { if (absolute) { return yformat(Math.abs(d)); } return yformat(d); }) .tickSizeOuter(0); } else { axisGenerator = axis(scale) .tickFormat(d => { if (absolute) { return yformat(Math.abs(d)); } return yformat(d); }) .tickSizeOuter(0); } } } else if (type === "log") { if (min === 0) { throw Error("In a log scale, minimum value can't be 0"); } axisGenerator = axis(scale) .ticks(10, ".2s") .tickSizeOuter(0); } return axisGenerator; }
export function generatePoints({axisStart, axisEnd, numberOfTicks, axisDomain}) { const {left, right, slope, offset} = generateFit(axisStart, axisEnd); // construct a linear band of points, then map them const pointSlope = (right - left) / (numberOfTicks); const axisScale = scaleLinear().domain([left, right]).range(axisDomain); const slopeVertical = axisStart.x === axisEnd.x; return { slope: slopeVertical ? Infinity : slope, points: range(left, right + pointSlope, pointSlope) .map(val => { if (slopeVertical) { return {y: val, x: slope * val + offset, text: axisScale(val)}; } return {x: val, y: slope * val + offset, text: axisScale(val)}; }) }; }
// Applies to ordinal scales (invert not supported in d3) function invertOrdinal(val, cmpFunc) { cmpFunc = cmpFunc || function (a, b) { return (a >= b); }; const scDomain = this.domain(); let scRange = this.range(); if (scRange.length === 2 && scDomain.length !== 2) { // Special case, interpolate range vals scRange = d3Range(scRange[0], scRange[1], (scRange[1] - scRange[0]) / scDomain.length); } const bias = scRange[0]; for (let 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]; }
[covered, uncovered].forEach(function(selection) { var range = (selection === covered ? d3_range(-10,0) : d3_range(0,11)); var layergroup = selection .selectAll('g.layergroup') .data(range); layergroup = layergroup.enter() .append('g') .attr('class', function(d) { return 'layergroup layer' + String(d); }) .merge(layergroup); layergroup .selectAll('g.linegroup') .data(['shadow', 'casing', 'stroke', 'shadow-highlighted', 'casing-highlighted', 'stroke-highlighted']) .enter() .append('g') .attr('class', function(d) { return 'linegroup line-' + d; }); layergroup.selectAll('g.line-shadow') .call(drawLineGroup, 'shadow', false); layergroup.selectAll('g.line-casing') .call(drawLineGroup, 'casing', false); layergroup.selectAll('g.line-stroke') .call(drawLineGroup, 'stroke', false); layergroup.selectAll('g.line-shadow-highlighted') .call(drawLineGroup, 'shadow', true); layergroup.selectAll('g.line-casing-highlighted') .call(drawLineGroup, 'casing', true); layergroup.selectAll('g.line-stroke-highlighted') .call(drawLineGroup, 'stroke', true); var onewaygroup = layergroup .selectAll('g.onewaygroup') .data(['oneway']); onewaygroup = onewaygroup.enter() .append('g') .attr('class', 'onewaygroup') .merge(onewaygroup); var oneways = onewaygroup .selectAll('path') .filter(filter) .data( function data() { return onewaydata[this.parentNode.__data__] || []; }, function key(d) { return [d.id, d.index]; } ); oneways.exit() .remove(); oneways = oneways.enter() .append('path') .attr('class', 'oneway') .attr('marker-mid', 'url(#oneway-marker)') .merge(oneways) .attr('d', function(d) { return d.d; }); if (detected.ie) { oneways.each(function() { this.parentNode.insertBefore(this, this); }); } });
import { randomNormal as d3RandomNormal } from "d3-random"; import { range as d3Range } from "d3-array"; import { Scatter } from "@hpcc-js/chart"; const randomX = d3RandomNormal(200, 80); const randomY = d3RandomNormal(200, 80); const points = d3Range(200).map(function () { return [randomX(), randomY()]; }); new Scatter() .target("target") .columns(["X", "Y"]) .data(points) .xAxisType("linear") .yAxisType("linear") .render() ;
return function module() { let margin = { top: 70, right: 30, bottom: 60, left: 70 }, width = 960, height = 500, xScale, xAxis, xMonthAxis, yScale, yAxis, aspectRatio = null, monthAxisPadding = 30, verticalTicks = 5, yTickTextYOffset = -8, yTickTextXOffset = -20, tickPadding = 5, colorSchema = colorHelper.colorSchemas.britechartsColorSchema, areaOpacity = 0.64, categoryColorMap, order, forceAxisSettings = null, forcedXTicks = null, forcedXFormat = null, locale, baseLine, layers, layersInitial, area, // Area Animation maxAreaNumber = 8, areaAnimationDelayStep = 20, areaAnimationDelays = d3Array.range(areaAnimationDelayStep, maxAreaNumber* areaAnimationDelayStep, areaAnimationDelayStep), overlay, verticalMarkerContainer, verticalMarker, epsilon, dataPoints = {}, pointsSize = 1.5, pointsColor = '#c0c6cc', pointsBorderColor = '#ffffff', isAnimated = false, ease = d3Ease.easeQuadInOut, areaAnimationDuration = 1000, svg, chartWidth, chartHeight, data, dataByDate, dataByDateFormatted, dataByDateZeroed, verticalGridLines, horizontalGridLines, grid = null, tooltipThreshold = 480, xAxisPadding = { top: 0, left: 15, bottom: 0, right: 0 }, dateLabel = 'date', valueLabel = 'value', keyLabel = 'name', // getters getName = ({name}) => name, getDate = ({date}) => date, // events dispatcher = d3Dispatch.dispatch('customMouseOver', 'customMouseOut', 'customMouseMove'); /** * This function creates the graph using the selection and data provided * @param {D3Selection} _selection A d3 selection that represents * the container(s) where the chart(s) will be rendered * @param {areaChartData} _data The data to attach and generate the chart */ function exports(_selection) { _selection.each(function(_data) { chartWidth = width - margin.left - margin.right; chartHeight = height - margin.top - margin.bottom; data = cleanData(_data); dataByDate = getDataByDate(data); buildLayers(); buildScales(); buildSVG(this); buildAxis(); drawAxis(); drawStackedAreas(); if(shouldShowTooltip()) { drawHoverOverlay(); drawVerticalMarker(); addMouseEvents(); } }); } /** * Adds events to the container group if the environment is not mobile * Adding: mouseover, mouseout and mousemove */ function addMouseEvents() { svg .on('mouseover', handleMouseOver) .on('mouseout', handleMouseOut) .on('mousemove', handleMouseMove); } /** * Formats the value depending on its characteristics * @param {Number} value Value to format * @return {Number} Formatted value */ function getFormattedValue(value) { let format; if (isInteger(value)) { format = formatIntegerValue; } else { format = formatDecimalValue; } return format(value); } /** * Creates the d3 x and y axis, setting orientations * @private */ function buildAxis() { let dataSpan = yScale.domain()[1] - yScale.domain()[0]; let yTickNumber = dataSpan < verticalTicks - 1 ? dataSpan : verticalTicks; let minor, major; if (forceAxisSettings === 'custom' && typeof forcedXFormat === 'string') { minor = { tick: forcedXTicks, format: d3TimeFormat.timeFormat(forcedXFormat) }; major = null; } else { ({minor, major} = getXAxisSettings(dataByDate, width, forceAxisSettings, locale)); xMonthAxis = d3Axis.axisBottom(xScale) .ticks(major.tick) .tickSize(0, 0) .tickFormat(major.format); } xAxis = d3Axis.axisBottom(xScale) .ticks(minor.tick) .tickSize(10, 0) .tickPadding(tickPadding) .tickFormat(minor.format); yAxis = d3Axis.axisRight(yScale) .ticks(yTickNumber) .tickSize([0]) .tickPadding(tickPadding) .tickFormat(getFormattedValue); drawGridLines(minor.tick, yTickNumber); } /** * Builds containers for the chart, the axis and a wrapper for all of them * NOTE: The order of drawing of this group elements is really important, * as everything else will be drawn on top of them * @private */ function buildContainerGroups() { let container = svg .append('g') .classed('container-group', true) .attr('transform', `translate(${margin.left},${margin.top})`); container .append('g').classed('x-axis-group', true) .append('g').classed('x axis', true); container.selectAll('.x-axis-group') .append('g').classed('month-axis', true); container .append('g').classed('y-axis-group axis', true); container .append('g').classed('grid-lines-group', true); container .append('g').classed('chart-group', true); container .append('g').classed('metadata-group', true); } /** * Builds the stacked layers layout * @return {D3Layout} Layout for drawing the chart * @private */ function buildLayers() { dataByDateFormatted = dataByDate .map(d => assign({}, d, d.values)) .map(d => { Object.keys(d).forEach(k => { const entry = d[k]; if (entry && entry.name) { d[entry.name] = entry.value; } }); return assign({}, d, { date: new Date(d['key']) }); }); dataByDateZeroed = dataByDate .map(d => assign({}, d, d.values)) .map(d => { Object.keys(d).forEach(k => { const entry = d[k]; if (entry && entry.name) { d[entry.name] = 0; } }); return assign({}, d, { date: new Date(d['key']) }); }); let initialTotalsObject = uniq(data.map(({name}) => name)) .reduce((memo, key) => ( assign({}, memo, {[key]: 0}) ), {}); let totals = data.reduce((memo, item) => ( assign({}, memo, {[item.name]: memo[item.name] += item.value}) ), initialTotalsObject); order = formatOrder(totals); let stack3 = d3Shape.stack() .keys(order) .order(d3Shape.stackOrderNone) .offset(d3Shape.stackOffsetNone); layersInitial = stack3(dataByDateZeroed); layers = stack3(dataByDateFormatted); } /** * Takes an object with all topics as keys and their aggregate totals as values, * sorts them into a list by descending total value and * moves "Other" to the end * @param {Object} totals Keys of all the topics and their corresponding totals * @return {Array} List of topic names in aggregate order */ function formatOrder(totals) { let order = Object.keys(totals) .sort((a, b) => { if (totals[a] > totals[b]) return -1; if (totals[a] === totals[b]) return 0; return 1; }); let otherIndex = order.indexOf('Other'); if (otherIndex >= 0) { let other = order.splice(otherIndex, 1); order = order.concat(other); } return order; } /** * Creates the x, y and color scales of the chart * @private */ function buildScales() { xScale = d3Scale.scaleTime() .domain(d3Array.extent(dataByDate, ({date}) => date)) .rangeRound([0, chartWidth]); yScale = d3Scale.scaleLinear() .domain([0, getMaxValueByDate()]) .rangeRound([chartHeight, 0]) .nice(); categoryColorMap = order.reduce((memo, topic, index) => ( assign({}, memo, {[topic]: colorSchema[index]}) ), {}); } /** * @param {HTMLElement} container DOM element that will work as the container of the graph * @private */ function buildSVG(container) { if (!svg) { svg = d3Selection.select(container) .append('svg') .classed('britechart stacked-area', true); buildContainerGroups(); } svg .attr('width', width) .attr('height', height); } /** * Parses dates and values into JS Date objects and numbers * @param {obj} data Raw data from JSON file * @return {obj} Parsed data with values and dates */ function cleanData(data) { return data.map((d) => { d.date = new Date(d[dateLabel]), d.value = +d[valueLabel] return d; }); } /** * Draws the x and y axis on the svg object within their * respective groups * @private */ function drawAxis() { svg.select('.x-axis-group .axis.x') .attr('transform', `translate( 0, ${chartHeight} )`) .call(xAxis); if (forceAxisSettings !== 'custom') { svg.select('.x-axis-group .month-axis') .attr('transform', `translate(0, ${(chartHeight + monthAxisPadding)})`) .call(xMonthAxis); } svg.select('.y-axis-group.axis') .attr('transform', `translate( ${-xAxisPadding.left}, 0)`) .call(yAxis) .call(adjustYTickLabels); // Moving the YAxis tick labels to the right side // d3Selection.selectAll('.y-axis-group .tick text') // .attr('transform', `translate( ${-chartWidth - yTickTextXOffset}, ${yTickTextYOffset})` ); } /** * Adjusts the position of the y axis' ticks * @param {D3Selection} selection Y axis group * @return void */ function adjustYTickLabels(selection) { selection.selectAll('.tick text') .attr('transform', `translate(${yTickTextXOffset}, ${yTickTextYOffset})`); } /** * Creates SVG dot elements for each data entry and draws them * TODO: Plug */ function drawDataReferencePoints() { // Creates Dots on Data points var points = svg.select('.chart-group').selectAll('.dots') .data(layers) .enter().append('g') .attr('class', 'dots') .attr('d', ({values}) => area(values)) .attr('clip-path', 'url(#clip)'); // Processes the points // TODO: Optimize this code points.selectAll('.dot') .data(({values}, index) => values.map((point) => ({index, point}))) .enter() .append('circle') .attr('class','dot') .attr('r', () => pointsSize) .attr('fill', () => pointsColor) .attr('stroke-width', '0') .attr('stroke', pointsBorderColor) .attr('transform', function(d) { let {point} = d; let key = xScale(point.date); dataPoints[key] = dataPoints[key] || []; dataPoints[key].push(d); let {date, y, y0} = point; return `translate( ${xScale(date)}, ${yScale(y + y0)} )`; }); } /** * Draws grid lines on the background of the chart * @return void */ function drawGridLines(xTicks, yTicks) { if (grid === 'horizontal' || grid === 'full') { horizontalGridLines = svg.select('.grid-lines-group') .selectAll('line.horizontal-grid-line') .data(yScale.ticks(yTicks)) .enter() .append('line') .attr('class', 'horizontal-grid-line') .attr('x1', (-xAxisPadding.left - 30)) .attr('x2', chartWidth) .attr('y1', (d) => yScale(d)) .attr('y2', (d) => yScale(d)); } if (grid === 'vertical' || grid === 'full') { verticalGridLines = svg.select('.grid-lines-group') .selectAll('line.vertical-grid-line') .data(xScale.ticks(xTicks)) .enter() .append('line') .attr('class', 'vertical-grid-line') .attr('y1', 0) .attr('y2', chartHeight) .attr('x1', (d) => xScale(d)) .attr('x2', (d) => xScale(d)); } //draw a horizontal line to extend x-axis till the edges baseLine = svg.select('.grid-lines-group') .selectAll('line.extended-x-line') .data([0]) .enter() .append('line') .attr('class', 'extended-x-line') .attr('x1', (-xAxisPadding.left - 30)) .attr('x2', chartWidth) .attr('y1', height - margin.bottom - margin.top) .attr('y2', height - margin.bottom - margin.top); } /** * Draws an overlay element over the graph * @private */ function drawHoverOverlay() { overlay = svg.select('.metadata-group') .append('rect') .attr('class', 'overlay') .attr('y1', 0) .attr('y2', chartHeight) .attr('height', chartHeight) .attr('width', chartWidth) .attr('fill', 'rgba(0,0,0,0)') .style('display', 'none'); } /** * Draws the different areas into the chart-group element * @private */ function drawStackedAreas() { let series; area = d3Shape.area() .curve(d3Shape.curveMonotoneX) .x( ({data}) => xScale(data.date) ) .y0( (d) => yScale(d[0]) ) .y1( (d) => yScale(d[1]) ); if (isAnimated) { series = svg.select('.chart-group').selectAll('.layer') .data(layersInitial) .enter() .append('g') .classed('layer-container', true); series .append('path') .attr('class', 'layer') .attr('d', area) .style('fill', ({key}) => categoryColorMap[key]); // Update svg.select('.chart-group').selectAll('.layer') .data(layers) .transition() .delay( (_, i) => areaAnimationDelays[i]) .duration(areaAnimationDuration) .ease(ease) .attr('d', area) .style('opacity', areaOpacity) .style('fill', ({key}) => categoryColorMap[key]); } else { series = svg.select('.chart-group').selectAll('.layer') .data(layers) .enter() .append('g') .classed('layer-container', true); series .append('path') .attr('class', 'layer') .attr('d', area) .style('fill', ({key}) => categoryColorMap[key]); // Update series .attr('d', area) .style('opacity', areaOpacity) .style('fill', ({key}) => categoryColorMap[key]); } // Exit series.exit() .transition() .style('opacity', 0) .remove(); } /** * Creates the vertical marker * @return void */ function drawVerticalMarker() { verticalMarkerContainer = svg.select('.metadata-group') .append('g') .attr('class', 'vertical-marker-container') .attr('transform', 'translate(9999, 0)'); verticalMarker = verticalMarkerContainer.selectAll('path') .data([{ x1: 0, y1: 0, x2: 0, y2: 0 }]) .enter() .append('line') .classed('vertical-marker', true) .attr('x1', 0) .attr('y1', chartHeight) .attr('x2', 0) .attr('y2', 0); } /** * Removes all the datapoints highlighter circles added to the marker container * @return void */ function eraseDataPointHighlights() { verticalMarkerContainer.selectAll('.circle-container').remove(); } /** * Orders the data by date for consumption on the chart tooltip * @param {areaChartData} data Chart data * @return {Object[]} Chart data ordered by date * @private */ function getDataByDate(data) { return d3Collection.nest() .key(getDate) .entries( data.sort((a, b) => a.date - b.date) ) .map(d => { return assign({}, d, { date: new Date(d.key) }); }); // let b = d3Collection.nest() // .key(getDate).sortKeys(d3Array.ascending) // .entries(data); } /** * Computes the maximum sum of values for any date * * @return {Number} Max value */ function getMaxValueByDate() { let keys = uniq(data.map(o => o.name)); let maxValueByDate = d3Array.max(dataByDateFormatted, function(d){ let vals = keys.map((key) => d[key]); return d3Array.sum(vals); }); return maxValueByDate; } /** * Extract X position on the chart from a given mouse event * @param {obj} event D3 mouse event * @return {Number} Position on the x axis of the mouse * @private */ function getMouseXPosition(event) { return d3Selection.mouse(event)[0]; } /** * Finds out the data entry that is closer to the given position on pixels * @param {Number} mouseX X position of the mouse * @return {obj} Data entry that is closer to that x axis position */ function getNearestDataPoint(mouseX) { let points = dataByDate.filter(({date}) => Math.abs(xScale(date) - mouseX) <= epsilon); if (points.length) { return points[0]; } } /** * Epsilon is the value given to the number representing half of the distance in * pixels between two date data points * @return {Number} half distance between any two points */ function setEpsilon() { let dates = dataByDate.map(({date}) => date); epsilon = (xScale(dates[1]) - xScale(dates[0])) / 2; } /** * MouseMove handler, calculates the nearest dataPoint to the cursor * and updates metadata related to it * @private */ function handleMouseMove() { epsilon || setEpsilon(); let dataPoint = getNearestDataPoint(getMouseXPosition(this) - margin.left), dataPointXPosition; if(dataPoint) { dataPointXPosition = xScale(new Date( dataPoint.key )); // Move verticalMarker to that datapoint moveVerticalMarker(dataPointXPosition); // Add data points highlighting highlightDataPoints(dataPoint); // Emit event with xPosition for tooltip or similar feature dispatcher.call('customMouseMove', this, dataPoint, categoryColorMap, dataPointXPosition); } } /** * MouseOut handler, hides overlay and removes active class on verticalMarkerLine * It also resets the container of the vertical marker * @private */ function handleMouseOut(data) { overlay.style('display', 'none'); verticalMarker.classed('bc-is-active', false); verticalMarkerContainer.attr('transform', 'translate(9999, 0)'); dispatcher.call('customMouseOut', this, data); } /** * Mouseover handler, shows overlay and adds active class to verticalMarkerLine * @private */ function handleMouseOver(data) { overlay.style('display', 'block'); verticalMarker.classed('bc-is-active', true); dispatcher.call('customMouseOver', this, data); } /** * Creates coloured circles marking where the exact data y value is for a given data point * @param {obj} dataPoint Data point to extract info from * @private */ function highlightDataPoints({values}) { let accumulator = 0; eraseDataPointHighlights(); // ensure order stays constant values = values .filter(v => !!v) .sort((a,b) => order.indexOf(a.name) > order.indexOf(b.name)) values.forEach(({name}, index) => { let marker = verticalMarkerContainer .append('g') .classed('circle-container', true), circleSize = 12; accumulator = accumulator + values[index][valueLabel]; marker.append('circle') .classed('data-point-highlighter', true) .attr('cx', circleSize) .attr('cy', 0) .attr('r', 5) .style('stroke-width', 2) .style('stroke', categoryColorMap[name]); marker.attr('transform', `translate( ${(- circleSize)}, ${(yScale(accumulator))} )` ); }); } /** * Helper method to update the x position of the vertical marker * @param {obj} dataPoint Data entry to extract info * @return void */ function moveVerticalMarker(verticalMarkerXPosition) { verticalMarkerContainer.attr('transform', `translate(${verticalMarkerXPosition},0)`); } /** * Determines if we should add the tooltip related logic depending on the * size of the chart and the tooltipThreshold variable value * @return {boolean} Should we build the tooltip? * @private */ function shouldShowTooltip() { return width > tooltipThreshold; } // Accessors /** * Gets or Sets the opacity of the stacked areas in the chart (all of them will have the same opacity) * @param {Object} _x Opacity to get/set * @return { opacity | module} Current opacity or Area Chart module to chain calls * @public */ exports.areaOpacity = function(_x) { if (!arguments.length) { return areaOpacity; } areaOpacity = _x; return this; }; /** * Gets or Sets the aspect ratio of the chart * @param {Number} _x Desired aspect ratio for the graph * @return { (Number | Module) } Current aspect ratio or Area Chart module to chain calls * @public */ exports.aspectRatio = function(_x) { if (!arguments.length) { return aspectRatio; } aspectRatio = _x; return this; }; /** * Gets or Sets the colorSchema of the chart * @param {String[]} _x Desired colorSchema for the graph * @return { colorSchema | module} Current colorSchema or Chart module to chain calls * @public */ exports.colorSchema = function(_x) { if (!arguments.length) { return colorSchema; } colorSchema = _x; return this; }; /** * Gets or Sets the dateLabel of the chart * @param {Number} _x Desired dateLabel for the graph * @return { dateLabel | module} Current dateLabel or Chart module to chain calls * @public */ exports.dateLabel = function(_x) { if (!arguments.length) { return dateLabel; } dateLabel = _x; return this; }; /** * Exposes the ability to force the chart to show a certain x axis grouping * @param {String} _x Desired format * @return { (String|Module) } Current format or module to chain calls * @example * area.forceAxisFormat(area.axisTimeCombinations.HOUR_DAY) */ exports.forceAxisFormat = function(_x) { if (!arguments.length) { return forceAxisSettings; } forceAxisSettings = _x; return this; }; /** * Exposes the ability to force the chart to show a certain x format * It requires a `forceAxisFormat` of 'custom' in order to work. * NOTE: localization not supported * @param {String} _x Desired format for x axis * @return { (String|Module) } Current format or module to chain calls */ exports.forcedXFormat = function(_x) { if (!arguments.length) { return forcedXFormat; } forcedXFormat = _x; return this; }; /** * Exposes the ability to force the chart to show a certain x ticks. It requires a `forceAxisFormat` of 'custom' in order to work. * NOTE: This value needs to be a multiple of 2, 5 or 10. They won't always work as expected, as D3 decides at the end * how many and where the ticks will appear. * * @param {Number} _x Desired number of x axis ticks (multiple of 2, 5 or 10) * @return { (Number|Module) } Current number or ticks or module to chain calls */ exports.forcedXTicks = function(_x) { if (!arguments.length) { return forcedXTicks; } forcedXTicks = _x; return this; }; /** * Gets or Sets the grid mode. * * @param {String} _x Desired mode for the grid ('vertical'|'horizontal'|'full') * @return { String | module} Current mode of the grid or Area Chart module to chain calls * @public */ exports.grid = function(_x) { if (!arguments.length) { return grid; } grid = _x; return this; }; /** * Gets or Sets the height of the chart * @param {Number} _x Desired width for the graph * @return { height | module} Current height or Area Chart module to chain calls * @public */ exports.height = function(_x) { if (!arguments.length) { return height; } if (aspectRatio) { width = Math.ceil(_x / aspectRatio); } height = _x; return this; }; /** * Gets or Sets the isAnimated property of the chart, making it to animate when render. * By default this is 'false' * * @param {Boolean} _x Desired animation flag * @return { isAnimated | module} Current isAnimated flag or Chart module * @public */ exports.isAnimated = function(_x) { if (!arguments.length) { return isAnimated; } isAnimated = _x; return this; }; /** * Gets or Sets the keyLabel of the chart * @param {Number} _x Desired keyLabel for the graph * @return { keyLabel | module} Current keyLabel or Chart module to chain calls * @public */ exports.keyLabel = function(_x) { if (!arguments.length) { return keyLabel; } keyLabel = _x; return this; }; /** * Gets or Sets the margin of the chart * @param {Object} _x Margin object to get/set * @return { margin | module} Current margin or Area Chart module to chain calls * @public */ exports.margin = function(_x) { if (!arguments.length) { return margin; } margin = _x; return this; }; /** * Gets or Sets the minimum width of the graph in order to show the tooltip * NOTE: This could also depend on the aspect ratio * * @param {Object} _x Margin object to get/set * @return { tooltipThreshold | module} Current tooltipThreshold or Area Chart module to chain calls * @public */ exports.tooltipThreshold = function(_x) { if (!arguments.length) { return tooltipThreshold; } tooltipThreshold = _x; return this; }; /** * Gets or Sets the valueLabel of the chart * @param {Number} _x Desired valueLabel for the graph * @return { valueLabel | module} Current valueLabel or Chart module to chain calls * @public */ exports.valueLabel = function(_x) { if (!arguments.length) { return valueLabel; } valueLabel = _x; return this; }; /** * Gets or Sets the number of verticalTicks of the yAxis on the chart * @param {Number} _x Desired verticalTicks * @return { verticalTicks | module} Current verticalTicks or Chart module to chain calls * @public */ exports.verticalTicks = function(_x) { if (!arguments.length) { return verticalTicks; } verticalTicks = _x; return this; }; /** * Gets or Sets the width of the chart * @param {Number} _x Desired width for the graph * @return { width | module} Current width or Area Chart module to chain calls * @public */ exports.width = function(_x) { if (!arguments.length) { return width; } if (aspectRatio) { height = Math.ceil(_x * aspectRatio); } width = _x; return this; }; /** * Pass language tag for the tooltip to localize the date. * Feature uses Intl.DateTimeFormat, for compatability and support, refer to * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat * @param {String} _x must be a language tag (BCP 47) like 'en-US' or 'fr-FR' * @return { (String|Module) } Current locale or module to chain calls */ exports.locale = function(_x) { if (!arguments.length) { return locale; } locale = _x; return this; }; /** * Chart exported to png and a download action is fired * @public */ exports.exportChart = function(filename, title) { exportChart.call(exports, svg, filename, title); }; /** * Exposes an 'on' method that acts as a bridge with the event dispatcher * We are going to expose this events: * customMouseOver, customMouseMove and customMouseOut * * @return {module} Bar Chart * @public */ exports.on = function() { let value = dispatcher.on.apply(dispatcher, arguments); return value === dispatcher ? exports : value; }; /** * Exposes the constants to be used to force the x axis to respect a certain granularity * current options: MINUTE_HOUR, HOUR_DAY, DAY_MONTH, MONTH_YEAR * @example * area.forceAxisFormat(area.axisTimeCombinations.HOUR_DAY) */ exports.axisTimeCombinations = axisTimeCombinations; return exports; };
renderLegend() { this.doc.fontSize(9); const lineHeight = this.doc.currentLineHeight(); this.doc.fillColor('black').fillOpacity(1) .text(t('Legend'), this.margins.left, this.bottomEdge - lineHeight * 8); const legendHeight = lineHeight * 4; const legendTop = this.bottomEdge - lineHeight * 6; this.doc.lineWidth(1) .rect(this.margins.left, legendTop, this.width, legendHeight) .stroke('black'); this.doc.fontSize(this.smallFontSize); const legendVerticalMiddle = legendTop + lineHeight * 2; const legendTextMiddle = legendVerticalMiddle - this.doc.currentLineHeight() / 2; const legendItemLeftOffset = 9; const legendItemLabelOffset = 6; let cursor = this.margins.left + legendItemLeftOffset; // rendering the items in the legend // cbg const vertOffsetAdjustments = [ 2.25, 1, 0.25, 0, 0, -0.25, -1, -2.25, ]; _.each(_.map(range(0, 16, 2), (d) => ([d, d - 7])), (pair) => { const [horizOffset, vertOffset] = pair; const adjustedVertOffset = vertOffset + vertOffsetAdjustments[horizOffset / 2]; let fill; if (horizOffset < 4) { fill = 'high'; } else if (horizOffset < 12) { fill = 'target'; } else { fill = 'low'; } this.doc .circle(cursor + horizOffset, legendVerticalMiddle + adjustedVertOffset, this.cbgRadius) .fill(this.colors[fill]); }); cursor += 16 + legendItemLabelOffset; this.doc.fillColor('black').text(t('CGM'), cursor, legendTextMiddle); cursor += this.doc.widthOfString(t('CGM')) + legendItemLeftOffset * 2; // smbg const smbgPositions = { target: [cursor, legendVerticalMiddle], high: [cursor + this.smbgRadius * 2, legendVerticalMiddle - this.smbgRadius * 2], low: [cursor + this.smbgRadius * 2, legendVerticalMiddle + this.smbgRadius * 2], }; this.doc.circle(cursor, legendVerticalMiddle, this.smbgRadius) .fill(this.colors.target); this.doc.circle(smbgPositions.high[0], smbgPositions.high[1], this.smbgRadius) .fill(this.colors.high); this.doc.circle(smbgPositions.low[0], smbgPositions.low[1], this.smbgRadius) .fill(this.colors.low); cursor += this.smbgRadius * 3 + legendItemLabelOffset; this.doc.fillColor('black').text(t('BGM'), cursor, legendTextMiddle); cursor += this.doc.widthOfString(t('BGM')) + legendItemLeftOffset * 2; /* boluses */ const bolusOpts = { bolusWidth: this.bolusWidth, extendedLineThickness: this.extendedLineThickness, interruptedLineThickness: this.interruptedLineThickness, triangleHeight: this.triangleHeight, }; const legendBolusYScale = scaleLinear() .domain([0, 10]) .range([legendTop + legendHeight - legendHeight / 4, legendTop + legendHeight / 4]); // (normal) bolus const normalBolusXScale = scaleLinear() .domain([0, 10]) .range([cursor, cursor + 10]); const normalPaths = getBolusPaths( { normal: 10, utc: 0 }, normalBolusXScale, legendBolusYScale, bolusOpts ); _.each(normalPaths, (path) => { this.renderEventPath(path); }); cursor += this.bolusWidth + legendItemLabelOffset; this.doc.fillColor('black').text(t('Bolus'), cursor, legendTextMiddle); cursor += this.doc.widthOfString(t('Bolus')) + legendItemLeftOffset * 2; // underride & override boluses const rideBolusXScale = scaleLinear() .domain([0, 10]) .range([cursor, cursor + 10]); const overridePaths = getBolusPaths( { type: 'wizard', recommended: { net: 8, carb: 8, correction: 0, }, bolus: { normal: 10, utc: 0, }, }, rideBolusXScale, legendBolusYScale, bolusOpts ); _.each(overridePaths, (path) => { this.renderEventPath(path); }); const underridePaths = getBolusPaths( { type: 'wizard', recommended: { net: 10, carb: 8, correction: 2, }, bolus: { normal: 5, utc: 5, }, }, rideBolusXScale, legendBolusYScale, bolusOpts ); _.each(underridePaths, (path) => { this.renderEventPath(path); }); cursor += this.bolusWidth * 3 + legendItemLabelOffset; this.doc.fillColor('black').text(t('Override up & down'), cursor, legendTextMiddle); cursor += this.doc.widthOfString(t('Override up & down')) + legendItemLeftOffset * 2; // interrupted bolus const interruptedBolusXScale = scaleLinear() .domain([0, 10]) .range([cursor, cursor + 10]); const interruptedPaths = getBolusPaths( { normal: 6, expectedNormal: 10, utc: 0, }, interruptedBolusXScale, legendBolusYScale, bolusOpts ); _.each(interruptedPaths, (path) => { this.renderEventPath(path); }); cursor += this.bolusWidth + legendItemLabelOffset; this.doc.fillColor('black').text(t('Interrupted'), cursor, legendTextMiddle); cursor += this.doc.widthOfString(t('Interrupted')) + legendItemLeftOffset * 2; // extended bolus const extendedBolusXScale = scaleLinear() .domain([0, 10]) .range([cursor, cursor + 10]); const extendedPaths = getBolusPaths( { normal: 5, extended: 5, duration: 10, utc: 0, }, extendedBolusXScale, legendBolusYScale, bolusOpts ); _.each(extendedPaths, (path) => { this.renderEventPath(path); }); cursor += this.bolusWidth / 2 + 10 + legendItemLabelOffset; this.doc .fillColor('black') .text(t('Combo /'), cursor, legendTextMiddle - this.doc.currentLineHeight() / 2) .text(t('Extended')); cursor += this.doc.widthOfString(t('Extended')) + legendItemLeftOffset * 2; // carbohydrates this.doc.circle(cursor, legendVerticalMiddle, this.carbRadius) .fill(this.colors.carbs); this.doc.fillColor('black') .fontSize(this.carbsFontSize) .text( '25', cursor - this.carbRadius, legendVerticalMiddle - this.carbRadius / 2, { align: 'center', width: this.carbRadius * 2 } ); this.doc.fontSize(this.smallFontSize); cursor += this.carbRadius + legendItemLabelOffset; this.doc.fillColor('black').text(t('Carbs'), cursor, legendTextMiddle); cursor += this.doc.widthOfString(t('Carbs')) + legendItemLeftOffset * 2; /* basals */ const legendBasalYScale = scaleLinear() .domain([0, 2.5]) .range([legendTop + legendHeight - legendHeight / 4, legendTop + legendHeight / 4.5]); const legendBasalXScale = scaleLinear() .domain([0, 10]) .range([cursor, cursor + 50]); const dynamicBasalType = this.isAutomatedBasalDevice ? 'automated' : 'scheduled'; const scheduled1 = { subType: dynamicBasalType, rate: 1.5, duration: 2, utc: 0, }; const negTemp = { subType: 'temp', rate: 0.5, duration: 2.5, utc: 2, suppressed: { rate: 1.5, }, }; const scheduled2 = { subType: 'scheduled', rate: 1.75, duration: 1.5, utc: 4.5, }; const posTemp = { subType: 'temp', rate: 2, duration: 2, utc: 6, suppressed: { rate: 1.75, }, }; const suspend = { subType: 'suspend', rate: 0, duration: 2, utc: 8, suppressed: { subType: dynamicBasalType, rate: dynamicBasalType === 'automated' ? 0 : 1.75, }, }; const data = { basal: [ scheduled1, negTemp, scheduled2, posTemp, suspend, ], basalSequences: [ [scheduled1], [negTemp], [scheduled2], [posTemp], [suspend], ], }; this.renderBasalPaths({ basalScale: legendBasalYScale, data, xScale: legendBasalXScale, }); cursor += 50 + legendItemLabelOffset; this.doc.fillColor('black').text(t('Basals'), cursor, legendTextMiddle); return this; }
var d3 = require('d3-array'); // generate random graph object var nodeTotal = 11; var edgeTotal = nodeTotal * 1.5; var graph = { nodes: [], edges: [], adjacent: [] }; graph.nodes = d3.range(nodeTotal).map(function(i) { var node = { id : i }; return node; }); graph.edges = d3.range(edgeTotal).map(function(i) { var source = i < nodeTotal ? i : Math.floor(Math.random() * nodeTotal); var target = Math.floor(Math.random() * nodeTotal); while (source == target) { target = Math.floor(Math.random() * nodeTotal); } var edge = { source: source, target: target }; if (graph.adjacent[source] === undefined) {
function drawLines(selection, graph, entities, filter) { function waystack(a, b) { var selected = context.selectedIDs(), scoreA = selected.indexOf(a.id) !== -1 ? 20 : 0, scoreB = selected.indexOf(b.id) !== -1 ? 20 : 0; if (a.tags.highway) { scoreA -= highway_stack[a.tags.highway]; } if (b.tags.highway) { scoreB -= highway_stack[b.tags.highway]; } return scoreA - scoreB; } function drawLineGroup(selection, klass, isSelected) { var lines = selection .selectAll('path') .filter(filter) .data(getPathData(isSelected), osmEntity.key); lines.exit() .remove(); // Optimization: call simple TagClasses only on enter selection. This // works because osmEntity.key is defined to include the entity v attribute. lines.enter() .append('path') .attr('class', function(d) { return 'way line ' + klass + ' ' + d.id + (isSelected ? ' selected' : '') + (oldMultiPolygonOuters[d.id] ? ' old-multipolygon' : ''); }) .call(svgTagClasses()) .merge(lines) .sort(waystack) .attr('d', getPath) .call(svgTagClasses().tags(svgRelationMemberTags(graph))); return selection; } function getPathData(isSelected) { return function() { var layer = this.parentNode.__data__; var data = pathdata[layer] || []; return data.filter(function(d) { if (isSelected) return context.selectedIDs().indexOf(d.id) !== -1; else return context.selectedIDs().indexOf(d.id) === -1; }); }; } var getPath = svgPath(projection, graph), ways = [], pathdata = {}, onewaydata = {}, oldMultiPolygonOuters = {}; for (var i = 0; i < entities.length; i++) { var entity = entities[i], outer = osmSimpleMultipolygonOuterMember(entity, graph); if (outer) { ways.push(entity.mergeTags(outer.tags)); oldMultiPolygonOuters[outer.id] = true; } else if (entity.geometry(graph) === 'line') { ways.push(entity); } } ways = ways.filter(getPath); pathdata = _groupBy(ways, function(way) { return way.layer(); }); _forOwn(pathdata, function(v, k) { var arr = _filter(v, function(d) { return d.isOneWay(); }); onewaydata[k] = _flatten(_map(arr, svgOneWaySegments(projection, graph, 35))); }); var layer = selection.selectAll('.layer-lines'); var layergroup = layer .selectAll('g.layergroup') .data(d3_range(-10, 11)); layergroup = layergroup.enter() .append('g') .attr('class', function(d) { return 'layergroup layer' + String(d); }) .merge(layergroup); layergroup .selectAll('g.linegroup') .data(['shadow', 'casing', 'stroke', 'shadow-highlighted', 'casing-highlighted', 'stroke-highlighted']) .enter() .append('g') .attr('class', function(d) { return 'linegroup line-' + d; }); layergroup.selectAll('g.line-shadow') .call(drawLineGroup, 'shadow', false); layergroup.selectAll('g.line-casing') .call(drawLineGroup, 'casing', false); layergroup.selectAll('g.line-stroke') .call(drawLineGroup, 'stroke', false); layergroup.selectAll('g.line-shadow-highlighted') .call(drawLineGroup, 'shadow', true); layergroup.selectAll('g.line-casing-highlighted') .call(drawLineGroup, 'casing', true); layergroup.selectAll('g.line-stroke-highlighted') .call(drawLineGroup, 'stroke', true); var onewaygroup = layergroup .selectAll('g.onewaygroup') .data(['oneway']); onewaygroup = onewaygroup.enter() .append('g') .attr('class', 'onewaygroup') .merge(onewaygroup); var oneways = onewaygroup .selectAll('path') .filter(filter) .data( function() { return onewaydata[this.parentNode.__data__] || []; }, function(d) { return [d.id, d.index]; } ); oneways.exit() .remove(); oneways = oneways.enter() .append('path') .attr('class', 'oneway') .attr('marker-mid', 'url(#oneway-marker)') .merge(oneways) .attr('d', function(d) { return d.d; }); if (detected.ie) { oneways.each(function() { this.parentNode.insertBefore(this, this); }); } }
describe('calculateCbgStatsForBin', () => { const bin = 900000; const binKey = bin.toString(); const binSize = 1000 * 60 * 30; const min = 0; const max = 100; const data = shuffle(range(min, max + 1)); const res = utils.calculateCbgStatsForBin(binKey, binSize, data); it('should be a function', () => { assert.isFunction(utils.calculateCbgStatsForBin); }); it('should produce result full of `undefined`s on empty values array', () => { const emptyValsRes = utils.calculateCbgStatsForBin(binKey, binSize, []); assert.isObject(emptyValsRes); expect(emptyValsRes).to.deep.equal({ id: binKey, min: undefined, tenthQuantile: undefined, firstQuartile: undefined, median: undefined, thirdQuartile: undefined, ninetiethQuantile: undefined, max: undefined, msX: bin, msFrom: 0, msTo: bin * 2, }); }); it('should add the `binKey` as the `id` on the resulting object', () => { assert.isString(res.id); expect(res.id).to.equal(binKey); }); it('should add the minimum as the `min` on the resulting object', () => { expect(res.min).to.equal(min); }); it('should add the 10th quantile as the `tenthQuantile` on the resulting object', () => { expect(res.tenthQuantile).to.equal(10); }); it('should add the first quartile as the `firstQuartile` on the resulting object', () => { expect(res.firstQuartile).to.equal(25); }); it('should add the median as `median` on the resulting object', () => { expect(res.median).to.equal(50); }); it('should add the third quartile as the `thirdQuartile` on the resulting object', () => { expect(res.thirdQuartile).to.equal(75); }); it('should add the 90th quantile as the `ninetiethQuantile` on the resulting object', () => { expect(res.ninetiethQuantile).to.equal(90); }); it('should add the maximum as the `max` on the resulting object', () => { expect(res.max).to.equal(max); }); it('should add the bin as `msX` on the resulting object', () => { expect(res.msX).to.equal(bin); }); it('should add a `msFrom` to the resulting object half a bin earlier', () => { expect(res.msFrom).to.equal(0); }); it('should add a `msTo` to the resulting object half a bin later', () => { expect(res.msTo).to.equal(1800000); }); describe('when an array of out-of-range annotations is provided', () => { const outOfRange = [{ value: 'low', threshold: 25, }, { value: 'low', threshold: 40, }, { value: 'high', threshold: 500, }, { value: 'high', threshold: 400, }]; const resWithOutOfRange = utils.calculateCbgStatsForBin(binKey, binSize, data, outOfRange); it('should add `outOfRangeThresholds` to the resulting object', () => { expect(resWithOutOfRange.outOfRangeThresholds).to.deep.equal({ low: 40, high: 400, }); }); }); });
) const byStartHour = ([start, end]) => d => { const hour = getHours(d.startdate) return hour >= start && hour < end } export const filterByStartHour = memoizeWith(identity, (start, end) => events.filter(byStartHour([start, end])), ) import { scaleQuantize } from 'd3-scale' import { extent, range } from 'd3-array' export const dateScale = scaleQuantize() .range(range(0, 101)) .domain(extent(events, event => getTime(event.startdate))) export const byDate = events.reduce((acc, event) => { const skey = dateScale(getTime(event.startdate)) const ekey = dateScale(getTime(event.enddate)) range(skey, ekey).map(key => acc[key].push(event)) return acc }, range(0, 101).map(() => [])) const seasons = [ 'winter', 'winter', 'spring', 'spring', 'spring',
describe('calculateSmbgStatsForBin', () => { const bin = 5400000; const binKey = bin.toString(); const binSize = 1000 * 60 * 60 * 3; const min = 0; const max = 100; const data = shuffle(range(min, max + 1)); const res = utils.calculateSmbgStatsForBin(binKey, binSize, data); it('should be a function', () => { assert.isFunction(utils.calculateSmbgStatsForBin); }); it('should produce result full of `undefined`s on empty values array', () => { const emptyValsRes = utils.calculateSmbgStatsForBin(binKey, binSize, []); assert.isObject(emptyValsRes); expect(emptyValsRes).to.deep.equal({ id: binKey, min: undefined, mean: undefined, max: undefined, msX: bin, msFrom: 0, msTo: bin * 2, }); }); it('should add the `binKey` as the `id` on the resulting object', () => { assert.isString(res.id); expect(res.id).to.equal(binKey); }); it('should add the minimum as the `min` on the resulting object', () => { expect(res.min).to.equal(min); }); it('should add the mean as the `mean` on the resulting object', () => { expect(res.mean).to.equal(50); }); it('should add the maximum as the `max` on the resulting object', () => { expect(res.max).to.equal(max); }); it('should add the bin as `msX` on the resulting object', () => { expect(res.msX).to.equal(bin); }); describe('when an array of out-of-range annotations is provided', () => { const outOfRange = [{ value: 'low', threshold: 25, }, { value: 'low', threshold: 40, }, { value: 'high', threshold: 500, }, { value: 'high', threshold: 400, }]; const resWithOutOfRange = utils.calculateSmbgStatsForBin(binKey, binSize, data, outOfRange); it('should add `outOfRangeThresholds` to the resulting object', () => { expect(resWithOutOfRange.outOfRangeThresholds).to.deep.equal({ low: 40, high: 400, }); }); }); });
return ( <g id="background"> <rect className={styles.background} x={margins.left} y={margins.top} width={width} height={height} /> {lines} </g> ); }; Background.defaultProps = { data: _.map(range(1, 8), (i) => (i * datetime.THREE_HRS)), linesAtThreeHrs: false, }; Background.propTypes = { data: PropTypes.arrayOf(PropTypes.number.isRequired).isRequired, linesAtThreeHrs: PropTypes.bool.isRequired, margins: PropTypes.shape({ top: PropTypes.number.isRequired, right: PropTypes.number.isRequired, bottom: PropTypes.number.isRequired, left: PropTypes.number.isRequired, }).isRequired, svgDimensions: PropTypes.shape({ width: PropTypes.number.isRequired, height: PropTypes.number.isRequired,
function chord(matrix) { var n = matrix.length, groupSums = [], groupIndex = range(n), subgroupIndex = [], chords = [], groups = chords.groups = new Array(n), subgroups = new Array(n * n), k, x, x0, dx, i, j; // Compute the sum. k = 0, i = -1; while (++i < n) { x = 0, j = -1; while (++j < n) { x += matrix[i][j]; } groupSums.push(x); subgroupIndex.push(range(n)); k += x; } // Sort groups… if (sortGroups) groupIndex.sort(function(a, b) { return sortGroups(groupSums[a], groupSums[b]); }); // Sort subgroups… if (sortSubgroups) subgroupIndex.forEach(function(d, i) { d.sort(function(a, b) { return sortSubgroups(matrix[i][a], matrix[i][b]); }); }); // Convert the sum to scaling factor for [0, 2pi]. // TODO Allow start and end angle to be specified? // TODO Allow padding to be specified as percentage? k = max(0, tau - padAngle * n) / k; dx = k ? padAngle : tau / n; // Compute the start and end angle for each group and subgroup. // Note: Opera has a bug reordering object literal properties! x = 0, i = -1; while (++i < n) { x0 = x, j = -1; while (++j < n) { var di = groupIndex[i], dj = subgroupIndex[di][j], v = matrix[di][dj], a0 = x, a1 = x += v * k; subgroups[dj * n + di] = { index: di, subindex: dj, startAngle: a0, endAngle: a1, value: v }; } groups[di] = { index: di, startAngle: x0, endAngle: x, value: groupSums[di] }; x += dx; } // Generate chords for each (non-empty) subgroup-subgroup link. i = -1; while (++i < n) { j = i - 1; while (++j < n) { var source = subgroups[j * n + i], target = subgroups[i * n + j]; if (source.value || target.value) { chords.push(source.value < target.value ? {source: target, target: source} : {source: source, target: target}); } } } return sortChords ? chords.sort(sortChords) : chords; }
/** Extends the draw behavior of the abstract Viz class. @private */ _draw(callback) { if (!this._filteredData.length) return this; const stackGroup = (d, i) => this._stacked ? `${this._groupBy.length > 1 ? this._ids(d, i).slice(0, -1).join("_") : "group"}` : `${this._ids(d, i).join("_")}`; let data = this._filteredData.map((d, i) => ({ __d3plus__: true, data: d, group: stackGroup(d, i), i, hci: this._confidence && this._confidence[1] && this._confidence[1](d, i), id: this._ids(d, i).slice(0, this._drawDepth + 1).join("_"), lci: this._confidence && this._confidence[0] && this._confidence[0](d, i), shape: this._shape(d, i), x: this._x(d, i), x2: this._x2(d, i), y: this._y(d, i), y2: this._y2(d, i) })); this._formattedData = data; if (this._size) { const rExtent = extent(data, d => this._size(d.data)); this._sizeScaleD3 = () => this._sizeMin; this._sizeScaleD3 = scales[`scale${this._sizeScale.charAt(0).toUpperCase()}${this._sizeScale.slice(1)}`]() .domain(rExtent) .range([rExtent[0] === rExtent[1] ? this._sizeMax : min([this._sizeMax / 2, this._sizeMin]), this._sizeMax]); } else { this._sizeScaleD3 = () => this._sizeMin; } const x2Exists = data.some(d => d.x2 !== undefined), y2Exists = data.some(d => d.y2 !== undefined); const height = this._height - this._margin.top - this._margin.bottom, opp = this._discrete ? this._discrete === "x" ? "y" : "x" : undefined, opp2 = this._discrete ? this._discrete === "x" ? "y2" : "x2" : undefined, opps = [opp, opp2], parent = this._select, transition = this._transition, width = this._width - this._margin.left - this._margin.right; const x2Time = this._time && data[0].x2 === this._time(data[0].data, data[0].i), xTime = this._time && data[0].x === this._time(data[0].data, data[0].i), y2Time = this._time && data[0].y2 === this._time(data[0].data, data[0].i), yTime = this._time && data[0].y === this._time(data[0].data, data[0].i); for (let i = 0; i < data.length; i++) { const d = data[i]; if (xTime) d.x = date(d.x); if (x2Time) d.x2 = date(d.x2); if (yTime) d.y = date(d.y); if (y2Time) d.y2 = date(d.y2); d.discrete = d.shape === "Bar" ? `${d[this._discrete]}_${d.group}` : `${d[this._discrete]}`; } let discreteKeys, domains, stackData, stackKeys; if (this._stacked) { const groupValues = nest() .key(d => d.group) .entries(data) .reduce((obj, d) => { if (!obj[d.key]) obj[d.key] = 0; obj[d.key] += sum(d.values, dd => dd[opp]); return obj; }, {}); data = data.sort((a, b) => { if (this[`_${this._discrete}Sort`]) return this[`_${this._discrete}Sort`](a.data, b.data); const a1 = a[this._discrete], b1 = b[this._discrete]; if (a1 - b1 !== 0) return a1 - b1; if (a.group !== b.group) return groupValues[b.group] - groupValues[a.group]; return b[opp] - a[opp]; }); discreteKeys = Array.from(new Set(data.map(d => d.discrete))); stackKeys = Array.from(new Set(data.map(d => d.id))); stackData = nest() .key(d => d.discrete) .entries(data) .map(d => d.values); stackData.forEach(g => { const ids = Array.from(new Set(g.map(d => d.id))); if (ids.length < stackKeys.length) { stackKeys.forEach(k => { if (!ids.includes(k)) { const d = data.filter(d => d.id === k)[0]; if (d.shape === "Area") { const group = stackGroup(d.data, d.i); const fillerPoint = { __d3plus__: true, data: d.data, discrete: d.shape === "Bar" ? `${g[0][this._discrete]}_${group}` : `${g[0][this._discrete]}`, group, id: k, shape: d.shape, [this._discrete]: g[0][this._discrete], [opp]: 0 }; data.push(fillerPoint); } } }); } }); if (this[`_${this._discrete}Sort`]) { data.sort((a, b) => this[`_${this._discrete}Sort`](a.data, b.data)); } else { data.sort((a, b) => a[this._discrete] - b[this._discrete]); } const order = this._stackOrder; if (order instanceof Array) stackKeys.sort((a, b) => order.indexOf(a) - order.indexOf(b)); else if (order === d3Shape.stackOrderNone) stackKeys.sort((a, b) => a.localeCompare(b)); stackData = d3Shape.stack() .keys(stackKeys) .offset(this._stackOffset) .order(order instanceof Array ? d3Shape.stackOrderNone : order) .value((group, key) => { const d = group.filter(g => g.id === key); return d.length ? d[0][opp] : 0; })(stackData); domains = { [this._discrete]: extent(data, d => d[this._discrete]), [opp]: [min(stackData.map(g => min(g.map(p => p[0])))), max(stackData.map(g => max(g.map(p => p[1]))))] }; } else { const xData = this._discrete === "x" ? data.map(d => d.x) : data.map(d => d.x) .concat(this._confidence && this._confidence[0] ? data.map(d => d.lci) : []) .concat(this._confidence && this._confidence[1] ? data.map(d => d.hci) : []); const x2Data = this._discrete === "x" ? data.map(d => d.x2) : data.map(d => d.x2) .concat(this._confidence && this._confidence[0] ? data.map(d => d.lci) : []) .concat(this._confidence && this._confidence[1] ? data.map(d => d.hci) : []); const yData = this._discrete === "y" ? data.map(d => d.y) : data.map(d => d.y) .concat(this._confidence && this._confidence[0] ? data.map(d => d.lci) : []) .concat(this._confidence && this._confidence[1] ? data.map(d => d.hci) : []); const y2Data = this._discrete === "y" ? data.map(d => d.y2) : data.map(d => d.y2) .concat(this._confidence && this._confidence[0] ? data.map(d => d.lci) : []) .concat(this._confidence && this._confidence[1] ? data.map(d => d.hci) : []); if (this[`_${this._discrete}Sort`]) { data.sort((a, b) => this[`_${this._discrete}Sort`](a.data, b.data)); } else { data.sort((a, b) => a[this._discrete] - b[this._discrete]); } domains = { x: this._xSort ? Array.from(new Set(data.filter(d => d.x).sort((a, b) => this._xSort(a.data, b.data)).map(d => d.x))) : extent(xData, d => d), x2: this._x2Sort ? Array.from(new Set(data.filter(d => d.x2).sort((a, b) => this._x2Sort(a.data, b.data)).map(d => d.x2))) : extent(x2Data, d => d), y: this._ySort ? Array.from(new Set(data.filter(d => d.y).sort((a, b) => this._ySort(a.data, b.data)).map(d => d.y))) : extent(yData, d => d), y2: this._y2Sort ? Array.from(new Set(data.filter(d => d.y2).sort((a, b) => this._y2Sort(a.data, b.data)).map(d => d.y2))) : extent(y2Data, d => d) }; } let xDomain = this._xDomain ? this._xDomain.slice() : domains.x, xScale = this._xSort ? "Ordinal" : "Linear"; if (xDomain[0] === void 0) xDomain[0] = domains.x[0]; if (xDomain[1] === void 0) xDomain[1] = domains.x[1]; if (xTime) { xDomain = xDomain.map(date); xScale = "Time"; } else if (this._discrete === "x") { xDomain = Array.from(new Set(data.filter(d => d.x).sort((a, b) => this._xSort ? this._xSort(a.data, b.data) : a.x - b.x).map(d => d.x))); xScale = "Ordinal"; } let x2Domain = this._x2Domain ? this._x2Domain.slice() : domains.x2, x2Scale = this._x2Sort ? "Ordinal" : "Linear"; if (x2Domain && x2Domain[0] === void 0) x2Domain[0] = domains.x2[0]; if (x2Domain && x2Domain[1] === void 0) x2Domain[1] = domains.x2[1]; if (x2Time) { x2Domain = x2Domain.map(date); x2Scale = "Time"; } else if (this._discrete === "x") { x2Domain = Array.from(new Set(data.filter(d => d.x2).sort((a, b) => this._x2Sort ? this._x2Sort(a.data, b.data) : a.x2 - b.x2).map(d => d.x2))); x2Scale = "Ordinal"; } let yDomain = this._yDomain ? this._yDomain.slice() : domains.y, yScale = this._ySort ? "Ordinal" : "Linear"; if (yDomain[0] === void 0) yDomain[0] = domains.y[0]; if (yDomain[1] === void 0) yDomain[1] = domains.y[1]; let y2Domain = this._y2Domain ? this._y2Domain.slice() : domains.y2, y2Scale = this._y2Sort ? "Ordinal" : "Linear"; if (y2Domain && y2Domain[0] === void 0) y2Domain[0] = domains.y2[0]; if (y2Domain && y2Domain[1] === void 0) y2Domain[1] = domains.y2[1]; if (yTime) { yDomain = yDomain.map(date); yScale = "Time"; } else if (this._discrete === "y") { yDomain = Array.from(new Set(data.sort((a, b) => this._ySort ? this._ySort(a.data, b.data) : a.y - b.y).map(d => d.y))); yScale = "Ordinal"; y2Domain = Array.from(new Set(data.sort((a, b) => this._y2Sort ? this._y2Sort(a.data, b.data) : a.y2 - b.y2).map(d => d.y2))); y2Scale = "Ordinal"; } if (y2Time) { y2Domain = y2Domain.map(date); y2Scale = "Time"; } domains = {x: xDomain, x2: x2Domain || xDomain, y: yDomain, y2: y2Domain || yDomain}; opps.forEach(opp => { if (opp && this._baseline !== void 0) { const b = this._baseline; if (domains[opp] && domains[opp][0] > b) domains[opp][0] = b; else if (domains[opp] && domains[opp][1] < b) domains[opp][1] = b; } }); let x = scales[`scale${xScale}`]().domain(domains.x).range(range(0, width + 1, width / (domains.x.length - 1))), x2 = scales[`scale${x2Scale}`]().domain(domains.x2).range(range(0, width + 1, width / (domains.x2.length - 1))), y = scales[`scale${yScale}`]().domain(domains.y.reverse()).range(range(0, height + 1, height / (domains.y.length - 1))), y2 = scales[`scale${y2Scale}`]().domain(domains.y2.reverse()).range(range(0, height + 1, height / (domains.y2.length - 1))); const shapeData = nest() .key(d => d.shape) .entries(data) .sort((a, b) => this._shapeSort(a.key, b.key)); const oppScale = this._discrete === "x" ? yScale : xScale; if (this._xConfig.scale !== "log" && this._yConfig.scale !== "log" && oppScale !== "Ordinal") { shapeData.forEach(d => { if (this._buffer[d.key]) { const res = this._buffer[d.key].bind(this)({data: d.values, x, y, config: this._shapeConfig[d.key]}); if (this._xConfig.scale !== "log") x = res[0]; if (this._yConfig.scale !== "log") y = res[1]; const res2 = this._buffer[d.key].bind(this)({data: d.values, x: x2, y: y2, x2: true, y2: true, config: this._shapeConfig[d.key]}); if (this._x2Config.scale !== "log") x2 = res2[0]; if (this._y2Config.scale !== "log") y2 = res2[1]; } }); } xDomain = x.domain(); x2Domain = x2.domain(); yDomain = y.domain(); y2Domain = y2.domain(); const testGroup = elem("g.d3plus-plot-test", {enter: {opacity: 0}, parent: this._select}), x2Ticks = this._discrete === "x" && !x2Time ? domains.x2 : undefined, xTicks = this._discrete === "x" && !xTime ? domains.x : undefined, y2Ticks = this._discrete === "y" && !y2Time ? domains.y2 : undefined, yTicks = this._discrete === "y" && !yTime ? domains.y : undefined; const yC = { gridConfig: {stroke: !this._discrete || this._discrete === "x" ? this._yTest.gridConfig().stroke : "transparent"} }; const defaultConfig = { barConfig: {"stroke-width": 0}, gridSize: 0, labels: [], title: false, tickSize: 0 }; const defaultX2Config = x2Exists ? {} : defaultConfig; const defaultY2Config = y2Exists ? {} : defaultConfig; this._yTest .domain(yDomain) .height(height) .maxSize(width / 2) .range([undefined, undefined]) .scale(yScale.toLowerCase()) .select(testGroup.node()) .ticks(yTicks) .width(width) .config(yC) .config(this._yConfig) .render(); let yBounds = this._yTest.outerBounds(); let yWidth = yBounds.width ? yBounds.width + this._yTest.padding() : undefined; if (y2Exists) { this._y2Test .domain(y2Domain) .height(height) .range([undefined, undefined]) .scale(y2Scale.toLowerCase()) .select(testGroup.node()) .ticks(y2Ticks) .width(width) .config(yC) .config(defaultY2Config) .config(this._y2Config) .render(); } let y2Bounds = this._y2Test.outerBounds(); let y2Width = y2Bounds.width ? y2Bounds.width + this._y2Test.padding() : undefined; const xC = { gridConfig: {stroke: !this._discrete || this._discrete === "y" ? this._xTest.gridConfig().stroke : "transparent"} }; this._xTest .domain(xDomain) .height(height) .maxSize(height / 2) .range([undefined, undefined]) .scale(xScale.toLowerCase()) .select(testGroup.node()) .ticks(xTicks) .width(width) .config(xC) .config(this._xConfig) .render(); if (x2Exists) { this._x2Test .domain(x2Domain) .height(height) .range([undefined, undefined]) .scale(x2Scale.toLowerCase()) .select(testGroup.node()) .ticks(x2Ticks) .width(width) .config(xC) .tickSize(0) .config(defaultX2Config) .config(this._x2Config) .render(); } const xTestRange = this._xTest._getRange(); const x2TestRange = this._x2Test._getRange(); const x2Bounds = this._x2Test.outerBounds(); const x2Height = x2Bounds.height + this._x2Test.padding(); let xOffsetLeft = max([yWidth, xTestRange[0], x2TestRange[0]]); this._xTest .range([xOffsetLeft, undefined]) .render(); const topOffset = this._yTest.shapeConfig().labelConfig.fontSize() / 2; let xOffsetRight = max([y2Width, width - xTestRange[1], width - x2TestRange[1]]); const xBounds = this._xTest.outerBounds(); const xHeight = xBounds.height + this._xTest.padding(); this._padding.left += xOffsetLeft; this._padding.right += xOffsetRight; this._padding.bottom += xHeight; this._padding.top += x2Height + topOffset; super._draw(callback); const horizontalMargin = this._margin.left + this._margin.right; const verticalMargin = this._margin.top + this._margin.bottom; this._yTest .domain(yDomain) .height(height) .maxSize(width / 2) .range([x2Height, height - (xHeight + topOffset + verticalMargin)]) .scale(yScale.toLowerCase()) .select(testGroup.node()) .ticks(yTicks) .width(width) .config(yC) .config(this._yConfig) .render(); yBounds = this._yTest.outerBounds(); yWidth = yBounds.width ? yBounds.width + this._yTest.padding() : undefined; xOffsetLeft = max([yWidth, xTestRange[0], x2TestRange[0]]); if (y2Exists) { this._y2Test .config(yC) .domain(y2Domain) .gridSize(0) .height(height) .range([x2Height, height - (xHeight + topOffset + verticalMargin)]) .scale(y2Scale.toLowerCase()) .select(testGroup.node()) .width(width - max([0, xOffsetRight - y2Width])) .title(false) .config(this._y2Config) .config(defaultY2Config) .render(); } y2Bounds = this._y2Test.outerBounds(); y2Width = y2Bounds.width ? y2Bounds.width + this._y2Test.padding() : undefined; xOffsetRight = max([y2Width, width - xTestRange[1], width - x2TestRange[1]]); const transform = `translate(${this._margin.left}, ${this._margin.top + x2Height + topOffset})`; const x2Transform = `translate(${this._margin.left}, ${this._margin.top + topOffset})`; const xGroup = elem("g.d3plus-plot-x-axis", {parent, transition, enter: {transform}, update: {transform}}); const x2Group = elem("g.d3plus-plot-x2-axis", {parent, transition, enter: {transform: x2Transform}, update: {transform: x2Transform}}); const xTrans = xOffsetLeft > yWidth ? xOffsetLeft - yWidth : 0; const yTransform = `translate(${this._margin.left + xTrans}, ${this._margin.top + topOffset})`; const yGroup = elem("g.d3plus-plot-y-axis", {parent, transition, enter: {transform: yTransform}, update: {transform: yTransform}}); const y2Transform = `translate(-${this._margin.right}, ${this._margin.top + topOffset})`; const y2Group = elem("g.d3plus-plot-y2-axis", {parent, transition, enter: {transform: y2Transform}, update: {transform: y2Transform}}); this._xAxis .domain(xDomain) .height(height - (x2Height + topOffset + verticalMargin)) .maxSize(height / 2) .range([xOffsetLeft, width - (xOffsetRight + horizontalMargin)]) .scale(xScale.toLowerCase()) .select(xGroup.node()) .ticks(xTicks) .width(width) .config(xC) .config(this._xConfig) .render(); if (x2Exists) { this._x2Axis .domain(x2Domain) .height(height - (xHeight + topOffset + verticalMargin)) .range([xOffsetLeft, width - (xOffsetRight + horizontalMargin)]) .scale(x2Scale.toLowerCase()) .select(x2Group.node()) .ticks(x2Ticks) .width(width) .config(xC) .config(defaultX2Config) .config(this._x2Config) .render(); } x = (d, x) => { if (x === "x2") { if (this._x2Config.scale === "log" && d === 0) d = x2Domain[0] < 0 ? -1 : 1; return this._x2Axis._getPosition.bind(this._x2Axis)(d); } else { if (this._xConfig.scale === "log" && d === 0) d = xDomain[0] < 0 ? -1 : 1; return this._xAxis._getPosition.bind(this._xAxis)(d); } }; const xRange = this._xAxis._getRange(); this._yAxis .domain(yDomain) .height(height) .maxSize(width / 2) .range([this._xAxis.outerBounds().y + x2Height, height - (xHeight + topOffset + verticalMargin)]) .scale(yScale.toLowerCase()) .select(yGroup.node()) .ticks(yTicks) .width(xRange[xRange.length - 1]) .config(yC) .config(this._yConfig) .render(); if (y2Exists) { this._y2Axis .config(yC) .domain(y2Exists ? y2Domain : yDomain) .gridSize(0) .height(height) .range([this._xAxis.outerBounds().y + x2Height, height - (xHeight + topOffset + verticalMargin)]) .scale(y2Exists ? y2Scale.toLowerCase() : yScale.toLowerCase()) .select(y2Group.node()) .width(width - max([0, xOffsetRight - y2Width])) .title(false) .config(this._y2Config) .config(defaultY2Config) .render(); } y = (d, y) => { if (y === "y2") { if (this._y2Config.scale === "log" && d === 0) d = y2Domain[0] < 0 ? -1 : 1; return this._y2Axis._getPosition.bind(this._y2Axis)(d) - x2Height; } else { if (this._yConfig.scale === "log" && d === 0) d = yDomain[0] < 0 ? -1 : 1; return this._yAxis._getPosition.bind(this._yAxis)(d) - x2Height; } }; const yRange = this._yAxis._getRange(); const annotationGroup = elem("g.d3plus-plot-annotations", {parent, transition, enter: {transform}, update: {transform}}).node(); this._annotations.forEach(annotation => { new shapes[annotation.shape]() .config(annotation) .config({ x: d => d.x2 ? x(d.x2, "x2") : x(d.x), x0: this._discrete === "x" ? d => d.x2 ? x(d.x2, "x2") : x(d.x) : x(0), x1: this._discrete === "x" ? null : d => d.x2 ? x(d.x2, "x2") : x(d.x), y: d => d.y2 ? y(d.y2, "y2") : y(d.y), y0: this._discrete === "y" ? d => d.y2 ? y(d.y2, "y2") : y(d.y) : y(0) - yOffset, y1: this._discrete === "y" ? null : d => d.y2 ? y(d.y2, "y2") : y(d.y) - yOffset }) .select(annotationGroup) .render(); }); let yOffset = this._xAxis.barConfig()["stroke-width"]; if (yOffset) yOffset /= 2; const shapeConfig = { duration: this._duration, label: d => this._drawLabel(d.data, d.i), select: elem("g.d3plus-plot-shapes", {parent, transition, enter: {transform}, update: {transform}}).node(), x: d => d.x2 ? x(d.x2, "x2") : x(d.x), x0: this._discrete === "x" ? d => d.x2 ? x(d.x2, "x2") : x(d.x) : x(0), x1: this._discrete === "x" ? null : d => d.x2 ? x(d.x2, "x2") : x(d.x), y: d => d.y2 ? y(d.y2, "y2") : y(d.y), y0: this._discrete === "y" ? d => d.y2 ? y(d.y2, "y2") : y(d.y) : y(0) - yOffset, y1: this._discrete === "y" ? null : d => d.y2 ? y(d.y2, "y2") : y(d.y) - yOffset }; if (this._stacked) { const scale = opp === "x" ? x : y; shapeConfig[`${opp}`] = shapeConfig[`${opp}0`] = d => { const dataIndex = stackKeys.indexOf(d.id), discreteIndex = discreteKeys.indexOf(d.discrete); return dataIndex >= 0 ? scale(stackData[dataIndex][discreteIndex][0]) : scale(0); }; shapeConfig[`${opp}1`] = d => { const dataIndex = stackKeys.indexOf(d.id), discreteIndex = discreteKeys.indexOf(d.discrete); return dataIndex >= 0 ? scale(stackData[dataIndex][discreteIndex][1]) : scale(0); }; } const events = Object.keys(this._on); shapeData.forEach(d => { const s = new shapes[d.key]().config(shapeConfig).data(d.values); if (d.key === "Bar") { let space; const scale = this._discrete === "x" ? x : y; const vals = (this._discrete === "x" ? xDomain : yDomain).filter(d => typeof d !== "string" || d.indexOf("d3plus-buffer-") < 0); const range = this._discrete === "x" ? xRange : yRange; if (vals.length > 1) space = scale(vals[1]) - scale(vals[0]); else space = range[range.length - 1] - range[0]; space -= this._groupPadding; let barSize = space; const groups = nest() .key(d => d[this._discrete]) .key(d => d.group) .entries(d.values); const ids = merge(groups.map(d => d.values.map(v => v.key))); const uniqueIds = Array.from(new Set(ids)); if (max(groups.map(d => d.values.length)) === 1) { s[this._discrete]((d, i) => shapeConfig[this._discrete](d, i)); } else { barSize = (barSize - this._barPadding * uniqueIds.length - 1) / uniqueIds.length; const offset = space / 2 - barSize / 2; const xMod = scales.scaleLinear() .domain([0, uniqueIds.length - 1]) .range([-offset, offset]); s[this._discrete]((d, i) => shapeConfig[this._discrete](d, i) + xMod(uniqueIds.indexOf(d.group))); } s.width(barSize); s.height(barSize); } else if (d.key === "Line" && this._confidence) { const areaConfig = Object.assign({}, shapeConfig); const key = this._discrete === "x" ? "y" : "x"; const scaleFunction = this._discrete === "x" ? y : x; areaConfig[`${key}0`] = d => scaleFunction(this._confidence[0] ? d.lci : d[key]); areaConfig[`${key}1`] = d => scaleFunction(this._confidence[1] ? d.hci : d[key]); const area = new shapes.Area().config(areaConfig).data(d.values); const confidenceConfig = Object.assign(this._shapeConfig, this._confidenceConfig); area.config(configPrep.bind(this)(confidenceConfig, "shape", "Area")).render(); this._shapes.push(area); } const classEvents = events.filter(e => e.includes(`.${d.key}`)), globalEvents = events.filter(e => !e.includes(".")), shapeEvents = events.filter(e => e.includes(".shape")); for (let e = 0; e < globalEvents.length; e++) s.on(globalEvents[e], d => this._on[globalEvents[e]](d.data, d.i)); for (let e = 0; e < shapeEvents.length; e++) s.on(shapeEvents[e], d => this._on[shapeEvents[e]](d.data, d.i)); for (let e = 0; e < classEvents.length; e++) s.on(classEvents[e], d => this._on[classEvents[e]](d.data, d.i)); s.config(configPrep.bind(this)(this._shapeConfig, "shape", d.key)).render(); this._shapes.push(s); }); return this; }