function init({ chartData }) { const $el = $('.ml-explorer-chart'); // Clear any existing elements from the visualization, // then build the svg elements for the chart. const chartElement = d3.select(element).select('.content-wrapper'); chartElement.select('svg').remove(); const svgWidth = $el.width(); const svgHeight = chartHeight + margin.top + margin.bottom; const svg = chartElement.append('svg') .classed('ml-explorer-chart-svg', true) .attr('width', svgWidth) .attr('height', svgHeight); const categoryLimit = 30; const scaleCategories = d3.nest() .key(d => d.entity) .entries(chartData) .sort((a, b) => { return b.values.length - a.values.length; }) .filter((d, i) => { // only filter for rare charts if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { return (i < categoryLimit || d.key === highlight); } return true; }) .map(d => d.key); chartData = chartData.filter((d) => { return (scaleCategories.includes(d.entity)); }); if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { const focusData = chartData.filter((d) => { return d.entity === highlight; }).map(d => d.value); const focusExtent = d3.extent(focusData); // now again filter chartData to include only the data points within the domain chartData = chartData.filter((d) => { return (d.value <= focusExtent[1]); }); lineChartYScale = d3.scale.linear() .range([chartHeight, 0]) .domain([0, focusExtent[1]]) .nice(); } else if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { // avoid overflowing the border of the highlighted area const rowMargin = 5; lineChartYScale = d3.scale.ordinal() .rangePoints([rowMargin, chartHeight - rowMargin]) .domain(scaleCategories); } else { throw `chartType '${chartType}' not supported`; } const yAxis = d3.svg.axis().scale(lineChartYScale) .orient('left') .innerTickSize(0) .outerTickSize(0) .tickPadding(10); let maxYAxisLabelWidth = 0; const tempLabelText = svg.append('g') .attr('class', 'temp-axis-label tick'); const tempLabelTextData = (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) ? lineChartYScale.ticks() : scaleCategories; tempLabelText.selectAll('text.temp.axis').data(tempLabelTextData) .enter() .append('text') .text((d) => { if (fieldFormat !== undefined) { return fieldFormat.convert(d, 'text'); } else { if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { return lineChartYScale.tickFormat()(d); } return d; } }) // Don't use an arrow function since we need access to `this`. .each(function () { maxYAxisLabelWidth = Math.max(this.getBBox().width + yAxis.tickPadding(), maxYAxisLabelWidth); }) .remove(); d3.select('.temp-axis-label').remove(); // Set the size of the left margin according to the width of the largest y axis tick label // if the chart is either a population chart or a rare chart below the cardinality threshold. if ( chartType === CHART_TYPE.POPULATION_DISTRIBUTION || ( chartType === CHART_TYPE.EVENT_DISTRIBUTION && scaleCategories.length <= Y_AXIS_LABEL_THRESHOLD ) ) { margin.left = (Math.max(maxYAxisLabelWidth, 40)); } vizWidth = svgWidth - margin.left - margin.right; // Set the x axis domain to match the request plot range. // This ensures ranges on different charts will match, even when there aren't // data points across the full range, and the selected anomalous region is centred. lineChartXScale = d3.time.scale() .range([0, vizWidth]) .domain([config.plotEarliest, config.plotLatest]); lineChartValuesLine = d3.svg.line() .x(d => lineChartXScale(d.date)) .y(d => lineChartYScale(d[CHART_Y_ATTRIBUTE])) .defined(d => d.value !== null); lineChartGroup = svg.append('g') .attr('class', 'line-chart') .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); return chartData; }
function genplot(plotsvg,cf,cellid){ var all = cf.groupAll() var dimensions = {} var vars = ['ts','vmt','road_class'] vars.forEach(function(k){ dimensions[k] = cf.dimension(function(d){ return d[k] }) return null }) // and another one too dimensions['ts2'] = cf.dimension(function(d){ return d['ts'] }) var dates = dimensions.ts.group(d3.time.day) var group_road_class = dimensions.road_class.group() //var filter_roadclass = dimensions.road_class.filter(function(d){ // // return d !== 'totals' // return d !== 'detector_based' // }) var vmtsums = dimensions.vmt .groupAll() .reduceSum(function(d) { return d.vmt; }); var tsvmt_group = dimensions.ts2 .group(function(d){ return d}) .reduce( function (p, v) { return p + +(v.vmt) } ,function (p, v) { return p - +v.vmt } ,function (){ return 0 } ) var mycheck = cb.checkBox() .dimension(dimensions.road_class) .group(group_road_class) .renderAll(renderAll(plotsvg,all)) // svg.selectAll(".limit") // .data([mycheck]) // .enter() // .append('div').classed('limit',true) // .each(function(limit) { // d3.select(this).call(limit) // }); var mychart = xyChart() .xdimension(dimensions.ts2) .xdim('ts') .ydim('vmt') .ylabel('VMT grid '+cellid) .xlabel('timestamp (hour)') .group(tsvmt_group) .sumgroup(vmtsums) .x(d3.time.scale() .rangeRound([0, 10 * 85])); var timescale = barChart() .dimension(dimensions.ts) .group(dates) .round(d3.time.day.round) //.ylabel('Count') //.xlabel('timestamp (day)') .x(d3.time.scale() .rangeRound([0, 10 * 85])) barcharts.push(timescale) var g = plotsvg.selectAll("g.gridplot") .data([mychart,timescale]) var ch = plotsvg.selectAll("g.checkboxes") .data([mycheck]) // if g is there?? // g.remove() ?? g.enter().append('g').attr("class", 'gridplot') .each(function(chart) { if(chart.on !== undefined ){ chart .on("brush", renderAll(plotsvg,all)) .on("brushend", renderAll(plotsvg,all)); } }) .each(render); ch.enter().append('g').attr("class", 'checkboxes') .each(render); return null }
NetworkGraph.prototype.make = function() { var me = this; var graphDom = me.element, data = me.lower.data, rxData = me.upper.data, txData = me.lower.data; var getX = Utils.get("x"); var getY = Utils.get("y"); var yAxisWidth = 60, margin = { top: 10, right: 20, bottom: 5, left: yAxisWidth }, width = this.width - margin.left - margin.right, height = this.height - margin.top - margin.bottom; var yMax = d3.max([d3.max(rxData, getY) , d3.max(txData, getY) , 1.2 * 1024]) || 0; var yMeanUpper = d3.max([d3.mean(rxData, getY) , d3.mean(rxData, getY)]) || 0; var yMeanLower = d3.max([d3.mean(txData, getY) , d3.mean(txData, getY)]) || 0; var xMax = d3.max(data, getX); var xMin = d3.min(data, getX); var x = d3.time.scale() .range([0, width]) .domain(d3.extent(data, getX)); var y = d3.scale.linear() .range([height, 0]) .domain([-1.2 * yMax, 1.2 * yMax]); var line = d3.svg.line() .x(function(d) { return x(d.x); }) .y(function(d) { return y(d.y); }); var area = d3.svg.area() .x(function(d) { return x(d.x); }) .y0(height / 2) .y1(function(d) { return y(d.y); }); var lineReflect = d3.svg.line() .x(function(d) { return x(d.x); }) .y(function(d) { return -y(d.y); }); var areaReflect = d3.svg.area() .x(function(d) { return x(d.x); }) .y0(height / 2) .y1(function(d) { return -y(d.y) + height; }); var svg = d3.select(graphDom).append("svg") .attr("width", me.width) .attr("height", me.height) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); var delta = 0.2; var showMeanUpper = yMeanUpper < yMax - delta * yMax && yMeanUpper > delta * yMax; var showMeanLower = yMeanLower < yMax - delta * yMax && yMeanLower > delta * yMax; if (showMeanUpper && showMeanLower) { svg.append("path") .datum([ { x: xMin, y: yMeanUpper }, { x: xMax, y: yMeanUpper } ]) .style("stroke-dasharray", ("3, 3")) .attr("class", "metrics mean line") .attr("d", line); svg.append("path") .datum([ { x: xMin, y: -yMeanLower }, { x: xMax, y: -yMeanLower } ]) .style("stroke-dasharray", ("3, 3")) .attr("class", "metrics mean line") .attr("d", line); } svg.append("path") .datum(rxData) .attr("class", "metrics rx area") .attr("d", area); svg.append("path") .datum(txData) .attr("class", "metrics tx area") .attr("d", areaReflect); svg.append("path") .datum(txData) .attr("class", "metrics tx line") .attr("d", lineReflect) .attr("transform", "translate(0," + height + ")"); svg.append("path") .datum(rxData) .attr("class", "metrics rx line") .attr("d", line); var yTick = Math.max(1024, yMax); var yAxis = d3.svg.axis() .tickFormat(function(d) { return Utils.bytesToString(Math.abs(d)); }) .tickValues([-yTick, yTick, 0 /*yMeanUpper, -yMeanLower*/ ]) .scale(y) .orient("left"); svg.append("g") .attr("class", "metrics y axis") .call(yAxis); svg.append("text") .attr("class", "metrics x axis") .attr("style", "text-anchor:end") .attr("x", width) .attr("y", 0) .attr("dy", ".32em") .text("data in"); svg.append("text") .attr("class", "metrics x axis") .attr("style", "text-anchor:end") .attr("x", width) .attr("y", height) .attr("dy", ".32em") .text("data out"); };
import d3 from 'd3'; export const dateFormat = d3.time.format('%b %d, %Y'); export const timeFormat = d3.time.format('%H:%M%p');
export default function() { var xScale = d3.time.scale(), yScale = d3.scale.linear(), upperValue = 70, lowerValue = 30, multiSeries = _multi(), decorate = noop; var annotations = annotationLine(); var rsiLine = seriesLine() .xValue(function(d, i) { return d.date; }) .yValue(function(d, i) { return d.rsi; }); var rsi = function(selection) { multiSeries.xScale(xScale) .yScale(yScale) .series([annotations, rsiLine]) .mapping(function(series) { if (series === annotations) { return [ upperValue, 50, lowerValue ]; } return this; }) .decorate(function(g, data, index) { g.enter() .attr('class', function(d, i) { return 'multi rsi ' + ['annotations', 'indicator'][i]; }); decorate(g, data, index); }); selection.call(multiSeries); }; rsi.xScale = function(x) { if (!arguments.length) { return xScale; } xScale = x; return rsi; }; rsi.yScale = function(x) { if (!arguments.length) { return yScale; } yScale = x; return rsi; }; rsi.upperValue = function(x) { if (!arguments.length) { return upperValue; } upperValue = x; return rsi; }; rsi.lowerValue = function(x) { if (!arguments.length) { return lowerValue; } lowerValue = x; return rsi; }; rsi.decorate = function(x) { if (!arguments.length) { return decorate; } decorate = x; return rsi; }; d3.rebind(rsi, rsiLine, 'yValue', 'xValue'); return rsi; }
function draw(measurements, history) { if (!history) return var w = measurements.w var h = measurements.h var padding = measurements.padding var totalH = h + padding.top + padding.bottom // var totalW = w + padding.left + padding.right var x = d3.time.scale() .range([padding.left + 5, w + padding.left - 5]) .domain(d3.extent(history, function(d) { return d.ratingDate })) var y = d3.scale.linear() .range([h + padding.top - 5, padding.top + 5]) .domain(d3.extent(history, function(d) { return d.newRating })) function yAxis(ticks) { return d3.svg.axis() .scale(y) .ticks(ticks || 10) .orient('left') } function xAxis(ticks) { return d3.svg.axis() .scale(x) .ticks(ticks || 10) .orient('bottom') } var line = d3.svg.line() //.interpolate('cardinal') .x(function(d) { return x(d.ratingDate) }) .y(function(d) { return y(d.newRating) }) var svg = d3.select('.history-graph').append('svg') .attr('width', w + padding.left + padding.right) .attr('height', h + padding.top + padding.bottom) svg.append('rect') .attr('x', padding.left) .attr('y', padding.top) .attr('width', w) .attr('height', h) svg.append('g') .attr('class', 'x axis') .attr('transform', 'translate(0,' + (h + padding.top) +')') .call(xAxis().tickFormat(function(d) { var m = moment(d) if (m.format('MM') == '01') return m.format('YYYY') else return m.format('MMM').toUpperCase() })) svg.selectAll('g.x.axis .tick text') .attr('font-weight', function(d) { return moment(d).format('MM') == '01' ? 'bold' : 'normal' }) .attr('fill', function(d) { return moment(d).format('MM') == '01' ? 'black' : '#a3a3ae' }) .attr('font-size', function(d) { return 11 }) svg.append('g') .attr('class', 'y axis') .attr('transform', 'translate(' + (padding.left - 25) + ')') .call(yAxis().tickFormat(function(d) { return parseInt(d) + '' }) ) svg.append('g') .attr('class', 'grid x') .attr('transform', 'translate(0, ' + (h + padding.top) + ')') .call( xAxis(/*Math.round(w / 35)*/).tickSize(-h, 0, 0) .tickFormat('') ) svg.append('g') .attr('class', 'grid y') .attr('transform', 'translate(' + padding.left + ',0)') .call( yAxis(/*Math.round(h / 30)*/).tickSize(-w, 0, 0) .tickFormat('') ) svg.append('path') .datum(history) .attr('class', 'line') .attr('d', line) // FIXME !!! svg.append('g') .selectAll('line') .data($scope.colors) .enter() .append('line') .attr('x1', padding.left - 18) .attr('x2', padding.left - 18) .attr('y1', function(d) { return processRatingStripePoint(y(d.start)) }) .attr('y2', function(d) { return processRatingStripePoint(y(d.end)) }) .attr('stroke', function(d) { return d.color }) .attr('stroke-width', 3) function processRatingStripePoint(y) { if (y < padding.top || isNaN(y)) { return padding.top } else if (y > totalH - padding.bottom) { return totalH - padding.bottom } else { return y } } /* render react tooltip component */ ReactDOM.unmountComponentAtNode(document.getElementById('chart-tooltip')) ReactDOM.render(<Tooltip popMethod='click'> <div className='tooltip-target'></div> <div className='tooltip-body'> <div className='tooltip-rating'></div> <div className='tooltip-challenge'> <div className='challenge-name'></div> <div className='challenge-date'></div> </div> </div> </Tooltip> , document.getElementById('chart-tooltip')) svg.selectAll('circle') .data(history) .enter() .append('circle') .attr('cx', function(d) { return x(d.ratingDate) }) .attr('cy', function(d) { return y(d.newRating) }) .attr('fill', function(d) { return ratingToColor($scope.colors, d.newRating) }) .on('mouseover', function(d) { $scope.historyRating = d.newRating $scope.historyDate = moment(d.ratingDate).format('YYYY-MM-DD') $scope.historyChallenge = d.challengeName $('#chart-tooltip .tooltip-container').on('click', function(){ if($state.params && ($state.params.subTrack === 'SRM' || $state.params.subTrack === 'MARATHON_MATCH')) location.href = $filter('challengeLinks')({'rounds': [{id: d.challengeId, forumId: null}], 'track': $state.params.track, 'subTrack': $state.params.subTrack}, 'detail') else location.href = $filter('challengeLinks')({id: d.challengeId, 'track': $state.params.track, 'subTrack': $state.params.subTrack}, 'detail') }) /* update tooltip location on mouseover, feature currently not inbuilt in react tooltip component */ d3.select('#chart-tooltip') .style('left', (d3.event.pageX-5) + 'px') .style('top', (d3.event.pageY-5) + 'px') d3.select('#chart-tooltip .tooltip-container') .style('left', '20px !important') .style('top', '-20px !important') d3.select('#chart-tooltip .tooltip-container .tooltip-pointer') .style('left', '-5.5px !important') .style('bottom', '25px !important') d3.select('#chart-tooltip .challenge-name').text($scope.historyChallenge) d3.select('#chart-tooltip .challenge-date').text(moment(d.ratingDate).format('MMM DD, YYYY')) d3.select('#chart-tooltip .tooltip-rating').text($scope.historyRating) d3.select('#chart-tooltip .tooltip-rating').style('background', ratingToColor($scope.colors, $scope.historyRating)) $('#chart-tooltip').removeClass('distribution') $scope.$digest() }) .on('mouseout', function(d) { $scope.historyRating = undefined $scope.$digest() }) /* hide tooltip when clicked anywhere outside */ d3.select('body').on('click', function(){ if((d3.event.target.classList[0] != 'tooltip-target') && !$('#chart-tooltip .tooltip-container').hasClass('tooltip-hide') && (d3.event.target.tagName.toLowerCase()!='circle') && !(d3.event.target.tagName.toLowerCase()=='rect' && d3.event.target.classList[0] == 'hover')) { $('#chart-tooltip .tooltip-target').trigger('click') $('#chart-tooltip .tooltip-container').off('click') } }) }
"use strict"; import React from "react"; import ReactDOM from "react-dom"; import d3 from "d3"; import ReStock from "react-stockcharts"; var parseDate = d3.time.format("%Y-%m-%d").parse var parseDateTime = d3.time.format("%Y-%m-%d %H:%M:%S").parse import "stylesheets/re-stock"; import Nav from "lib/navbar"; import Sidebar from "lib/sidebar"; import MainContainer from "lib/main-container"; import MenuGroup from "lib/menu-group"; import MenuItem from "lib/MenuItem"; var DOCUMENTATION = { head: "Documentation", pages: [ // require("lib/page/GettingStartedPage").default, // require("lib/page/QuickStartExamplesPage").default, require("lib/page/OverviewPage").default, require("lib/page/SvgVsCanvasPage").default, require("lib/page/LotsOfDataPage").default, require("lib/page/ChangeLogPage").default, require("lib/page/ComingSoonPage").default, ]
function drawSwimlane(swlGroup, swlWidth, swlHeight) { const data = scope.swimlaneData; // Calculate the x axis domain. // Elasticsearch aggregation returns points at start of bucket, so set the // x-axis min to the start of the aggregation interval. // Need to use the min(earliest) and max(earliest) of the context chart // aggregation to align the axes of the chart and swimlane elements. const xAxisDomain = calculateContextXAxisDomain(); const x = d3.time.scale().range([0, swlWidth]) .domain(xAxisDomain); const y = d3.scale.linear().range([swlHeight, 0]) .domain([0, swlHeight]); const xAxis = d3.svg.axis() .scale(x) .orient('bottom') .innerTickSize(-swlHeight) .outerTickSize(0); const yAxis = d3.svg.axis() .scale(y) .orient('left') .tickValues(y.domain()) .innerTickSize(-swlWidth) .outerTickSize(0); const axes = swlGroup.append('g'); axes.append('g') .attr('class', 'x axis') .attr('transform', 'translate(0,' + (swlHeight) + ')') .call(xAxis); axes.append('g') .attr('class', 'y axis') .call(yAxis); const earliest = xAxisDomain[0].getTime(); const latest = xAxisDomain[1].getTime(); const swimlaneAggMs = scope.contextAggregationInterval.asMilliseconds(); let cellWidth = swlWidth / ((latest - earliest) / swimlaneAggMs); if (cellWidth < 1) { cellWidth = 1; } const cells = swlGroup.append('g') .attr('class', 'swimlane-cells') .selectAll('cells') .data(data); cells.enter().append('rect') .attr('x', (d) => { return x(d.date); }) .attr('y', 0) .attr('rx', 0) .attr('ry', 0) .attr('class', (d) => { return d.score > 0 ? 'swimlane-cell' : 'swimlane-cell-hidden';}) .attr('width', cellWidth) .attr('height', swlHeight) .style('fill', (d) => { return anomalyColorScale(d.score);}); }
var tickFormatter = function (val) { return d3.time.format(tickFormat)(val); }
function link(scope, element) { // Key dimensions for the viz and constituent charts. let svgWidth = angular.element('.results-container').width(); const focusZoomPanelHeight = 25; const focusChartHeight = 310; const focusHeight = focusZoomPanelHeight + focusChartHeight; const contextChartHeight = 60; const contextChartLineTopMargin = 3; const chartSpacing = 25; const swimlaneHeight = 30; const margin = { top: 20, right: 10, bottom: 15, left: 40 }; const svgHeight = focusHeight + contextChartHeight + swimlaneHeight + chartSpacing + margin.top + margin.bottom; let vizWidth = svgWidth - margin.left - margin.right; const ZOOM_INTERVAL_OPTIONS = [ { duration: moment.duration(1, 'h'), label: '1h' }, { duration: moment.duration(12, 'h'), label: '12h' }, { duration: moment.duration(1, 'd'), label: '1d' }, { duration: moment.duration(1, 'w'), label: '1w' }, { duration: moment.duration(2, 'w'), label: '2w' }, { duration: moment.duration(1, 'M'), label: '1M' }]; // Set up the color scale to use for indicating score. const anomalyColorScale = d3.scale.threshold() .domain([3, 25, 50, 75, 100]) .range(['#d2e9f7', '#8bc8fb', '#ffdd00', '#ff7e00', '#fe5050']); // Create a gray-toned version of the color scale to use under the context chart mask. const anomalyGrayScale = d3.scale.threshold() .domain([3, 25, 50, 75, 100]) .range(['#dce7ed', '#b0c5d6', '#b1a34e', '#b17f4e', '#c88686']); const focusXScale = d3.time.scale().range([0, vizWidth]); const focusYScale = d3.scale.linear().range([focusHeight, focusZoomPanelHeight]); const focusXAxis = d3.svg.axis().scale(focusXScale).orient('bottom') .innerTickSize(-focusChartHeight).outerTickSize(0).tickPadding(10); const focusYAxis = d3.svg.axis().scale(focusYScale).orient('left') .innerTickSize(-vizWidth).outerTickSize(0).tickPadding(10); const focusValuesLine = d3.svg.line() .x(function (d) { return focusXScale(d.date); }) .y(function (d) { return focusYScale(d.value); }) .defined(d => d.value !== null); const focusBoundedArea = d3.svg.area() .x (function (d) { return focusXScale(d.date) || 1; }) .y0(function (d) { return focusYScale(d.upper); }) .y1(function (d) { return focusYScale(d.lower); }) .defined(d => (d.lower !== null && d.upper !== null)); let contextXScale = d3.time.scale().range([0, vizWidth]); let contextYScale = d3.scale.linear().range([contextChartHeight, contextChartLineTopMargin]); let fieldFormat = undefined; const brush = d3.svg.brush(); let mask; scope.$on('render', () => { fieldFormat = mlFieldFormatService.getFieldFormat(scope.selectedJob.job_id, scope.detectorIndex); render(); drawContextChartSelection(); }); scope.$watchCollection('focusForecastData', renderFocusChart); scope.$watchCollection('focusChartData', renderFocusChart); scope.$watchGroup(['showModelBounds', 'showForecast'], renderFocusChart); // Redraw the charts when the container is resize. const resizeChecker = new ResizeChecker(angular.element('.ml-timeseries-chart')); resizeChecker.on('resize', () => { scope.$evalAsync(() => { // Wait a digest cycle before rendering to prevent // the underlying ResizeObserver going into an infinite loop. render(); drawContextChartSelection(); renderFocusChart(); }); }); // Listeners for mouseenter/leave events for rows in the table // to highlight the corresponding anomaly mark in the focus chart. const tableRecordMousenterListener = function (record) { highlightFocusChartAnomaly(record); }; const tableRecordMouseleaveListener = function (record) { unhighlightFocusChartAnomaly(record); }; mlAnomaliesTableService.anomalyRecordMouseenter.watch(tableRecordMousenterListener); mlAnomaliesTableService.anomalyRecordMouseleave.watch(tableRecordMouseleaveListener); element.on('$destroy', () => { mlAnomaliesTableService.anomalyRecordMouseenter.unwatch(tableRecordMousenterListener); mlAnomaliesTableService.anomalyRecordMouseleave.unwatch(tableRecordMouseleaveListener); resizeChecker.destroy(); scope.$destroy(); }); function render() { // Clear any existing elements from the visualization, // then build the svg elements for the bubble chart. const chartElement = d3.select(element.get(0)); chartElement.selectAll('*').remove(); if (scope.contextChartData === undefined) { return; } // Set the size of the components according to the width of the parent container at render time. svgWidth = Math.max(angular.element('.results-container').width(), 0); const svg = chartElement.append('svg') .attr('width', svgWidth) .attr('height', svgHeight); let contextDataMin; let contextDataMax; if (scope.modelPlotEnabled === true || (scope.contextForecastData !== undefined && scope.contextForecastData.length > 0)) { const combinedData = scope.contextForecastData === undefined ? scope.contextChartData : scope.contextChartData.concat(scope.contextForecastData); contextDataMin = d3.min(combinedData, d => Math.min(d.value, d.lower)); contextDataMax = d3.max(combinedData, d => Math.max(d.value, d.upper)); } else { contextDataMin = d3.min(scope.contextChartData, d => d.value); contextDataMax = d3.max(scope.contextChartData, d => d.value); } // Set the size of the left margin according to the width of the largest y axis tick label. // The min / max of the aggregated context chart data may be less than the min / max of the // data which is displayed in the focus chart which is likely to be plotted at a lower // aggregation interval. Therefore ceil the min / max with the higher absolute value to allow // for extra space for chart labels which may have higher values than the context data // e.g. aggregated max may be 9500, whereas focus plot max may be 11234. const ceiledMax = contextDataMax > 0 ? Math.pow(10, Math.ceil(Math.log10(Math.abs(contextDataMax)))) : contextDataMax; const flooredMin = contextDataMin >= 0 ? contextDataMin : -1 * Math.pow(10, Math.ceil(Math.log10(Math.abs(contextDataMin)))); // Temporarily set the domain of the focus y axis to the min / max of the full context chart // data range so that we can measure the maximum tick label width on temporary text elements. focusYScale.domain([flooredMin, ceiledMax]); let maxYAxisLabelWidth = 0; const tempLabelText = svg.append('g') .attr('class', 'temp-axis-label tick'); tempLabelText.selectAll('text.temp.axis').data(focusYScale.ticks()) .enter() .append('text') .text((d) => { if (fieldFormat !== undefined) { return fieldFormat.convert(d, 'text'); } else { return focusYScale.tickFormat()(d); } }) .each(function () { maxYAxisLabelWidth = Math.max(this.getBBox().width + focusYAxis.tickPadding(), maxYAxisLabelWidth); }) .remove(); d3.select('.temp-axis-label').remove(); margin.left = (Math.max(maxYAxisLabelWidth, 40)); vizWidth = Math.max(svgWidth - margin.left - margin.right, 0); focusXScale.range([0, vizWidth]); focusYAxis.innerTickSize(-vizWidth); const focus = svg.append('g') .attr('class', 'focus-chart') .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); const context = svg.append('g') .attr('class', 'context-chart') .attr('transform', 'translate(' + margin.left + ',' + (focusHeight + margin.top + chartSpacing) + ')'); // Draw each of the component elements. createFocusChart(focus, vizWidth, focusHeight); drawContextElements(context, vizWidth, contextChartHeight, swimlaneHeight); } function drawContextChartSelection() { if (scope.contextChartData === undefined) { return; } // Make appropriate selection in the context chart to trigger loading of the focus chart. let focusLoadFrom; let focusLoadTo; const contextXMin = contextXScale.domain()[0].getTime(); const contextXMax = contextXScale.domain()[1].getTime(); let combinedData = scope.contextChartData; if (scope.contextForecastData !== undefined) { combinedData = combinedData.concat(scope.contextForecastData); } if (scope.zoomFrom) { focusLoadFrom = scope.zoomFrom.getTime(); } else { focusLoadFrom = _.reduce(combinedData, (memo, point) => Math.min(memo, point.date.getTime()), new Date(2099, 12, 31).getTime()); } focusLoadFrom = Math.max(focusLoadFrom, contextXMin); if (scope.zoomTo) { focusLoadTo = scope.zoomTo.getTime(); } else { focusLoadTo = _.reduce(combinedData, (memo, point) => Math.max(memo, point.date.getTime()), 0); } focusLoadTo = Math.min(focusLoadTo, contextXMax); if ((focusLoadFrom !== contextXMin) || (focusLoadTo !== contextXMax)) { setContextBrushExtent(new Date(focusLoadFrom), new Date(focusLoadTo), true); } else { // Don't set the brush if the selection is the full context chart domain. setBrushVisibility(false); const selectedBounds = contextXScale.domain(); scope.selectedBounds = { min: moment(new Date(selectedBounds[0])), max: moment(selectedBounds[1]) }; scope.$root.$broadcast('contextChartSelected', { from: selectedBounds[0], to: selectedBounds[1] }); } } function createFocusChart(fcsGroup, fcsWidth, fcsHeight) { // Split out creation of the focus chart from the rendering, // as we want to re-render the paths and points when the zoom area changes. // Add a group at the top to display info on the chart aggregation interval // and links to set the brush span to 1h, 1d, 1w etc. const zoomGroup = fcsGroup.append('g') .attr('class', 'focus-zoom'); zoomGroup.append('rect') .attr('x', 0) .attr('y', 0) .attr('width', fcsWidth) .attr('height', focusZoomPanelHeight) .attr('class', 'chart-border'); createZoomInfoElements(zoomGroup, fcsWidth); // Add border round plot area. fcsGroup.append('rect') .attr('x', 0) .attr('y', focusZoomPanelHeight) .attr('width', fcsWidth) .attr('height', focusChartHeight) .attr('class', 'chart-border'); // Add background for x axis. const xAxisBg = fcsGroup.append('g') .attr('class', 'x-axis-background'); xAxisBg.append('rect') .attr('x', 0) .attr('y', fcsHeight) .attr('width', fcsWidth) .attr('height', chartSpacing); xAxisBg.append('line') .attr('x1', 0) .attr('y1', fcsHeight) .attr('x2', 0) .attr('y2', fcsHeight + chartSpacing); xAxisBg.append('line') .attr('x1', fcsWidth) .attr('y1', fcsHeight) .attr('x2', fcsWidth) .attr('y2', fcsHeight + chartSpacing); xAxisBg.append('line') .attr('x1', 0) .attr('y1', fcsHeight + chartSpacing) .attr('x2', fcsWidth) .attr('y2', fcsHeight + chartSpacing); const axes = fcsGroup.append('g'); axes.append('g') .attr('class', 'x axis') .attr('transform', 'translate(0,' + fcsHeight + ')'); axes.append('g') .attr('class', 'y axis'); // Create the elements for the metric value line and model bounds area. fcsGroup.append('path') .attr('class', 'area bounds'); fcsGroup.append('path') .attr('class', 'values-line'); fcsGroup.append('g') .attr('class', 'focus-chart-markers'); // Create the path elements for the forecast value line and bounds area. if (scope.contextForecastData) { fcsGroup.append('path') .attr('class', 'area forecast'); fcsGroup.append('path') .attr('class', 'values-line forecast'); fcsGroup.append('g') .attr('class', 'focus-chart-markers forecast'); } fcsGroup.append('rect') .attr('x', 0) .attr('y', 0) .attr('width', fcsWidth) .attr('height', fcsHeight + 24) .attr('class', 'chart-border chart-border-highlight'); } function renderFocusChart() { if (scope.focusChartData === undefined) { return; } const data = scope.focusChartData; const focusChart = d3.select('.focus-chart'); // Update the plot interval labels. const focusAggInt = scope.focusAggregationInterval.expression; const bucketSpan = scope.selectedJob.analysis_config.bucket_span; angular.element('.zoom-aggregation-interval').text( `(aggregation interval: ${focusAggInt}, bucket span: ${bucketSpan})`); // Render the axes. // Calculate the x axis domain. // Elasticsearch aggregation returns points at start of bucket, // so set the x-axis min to the start of the first aggregation interval, // and the x-axis max to the end of the last aggregation interval. const bounds = scope.selectedBounds; const aggMs = scope.focusAggregationInterval.asMilliseconds(); const earliest = moment(Math.floor((bounds.min.valueOf()) / aggMs) * aggMs); const latest = moment(Math.ceil((bounds.max.valueOf()) / aggMs) * aggMs); focusXScale.domain([earliest.toDate(), latest.toDate()]); // Calculate the y-axis domain. if (scope.focusChartData.length > 0 || (scope.focusForecastData !== undefined && scope.focusForecastData.length > 0)) { if (fieldFormat !== undefined) { focusYAxis.tickFormat(d => fieldFormat.convert(d, 'text')); } else { // Use default tick formatter. focusYAxis.tickFormat(null); } // Calculate the min/max of the metric data and the forecast data. let yMin = 0; let yMax = 0; let combinedData = data; if (scope.focusForecastData !== undefined && scope.focusForecastData.length > 0) { combinedData = data.concat(scope.focusForecastData); } yMin = d3.min(combinedData, (d) => { return d.lower !== undefined ? Math.min(d.value, d.lower) : d.value; }); yMax = d3.max(combinedData, (d) => { return d.upper !== undefined ? Math.max(d.value, d.upper) : d.value; }); if (yMax === yMin) { if ( contextYScale.domain()[0] !== contextYScale.domain()[1] && yMin >= contextYScale.domain()[0] && yMax <= contextYScale.domain()[1] ) { // Set the focus chart limits to be the same as the context chart. yMin = contextYScale.domain()[0]; yMax = contextYScale.domain()[1]; } else { yMin -= (yMin * 0.05); yMax += (yMax * 0.05); } } focusYScale.domain([yMin, yMax]); } else { // Display 10 unlabelled ticks. focusYScale.domain([0, 10]); focusYAxis.tickFormat(''); } // Get the scaled date format to use for x axis tick labels. const timeBuckets = new TimeBuckets(); timeBuckets.setInterval('auto'); timeBuckets.setBounds(bounds); const xAxisTickFormat = timeBuckets.getScaledDateFormat(); focusChart.select('.x.axis') .call(focusXAxis.ticks(numTicksForDateFormat(vizWidth), xAxisTickFormat) .tickFormat((d) => { return moment(d).format(xAxisTickFormat); })); focusChart.select('.y.axis') .call(focusYAxis); filterAxisLabels(focusChart.select('.x.axis'), vizWidth); // Render the bounds area and values line. if (scope.modelPlotEnabled === true) { focusChart.select('.area.bounds') .attr('d', focusBoundedArea(data)) .classed('hidden', !scope.showModelBounds); } focusChart.select('.values-line') .attr('d', focusValuesLine(data)); drawLineChartDots(data, focusChart, focusValuesLine); // Render circle markers for the points. // These are used for displaying tooltips on mouseover. // Don't render dots where value=null (data gaps) or for multi-bucket anomalies. const dots = d3.select('.focus-chart-markers').selectAll('.metric-value') .data(data.filter(d => (d.value !== null && !showMultiBucketAnomalyMarker(d)))); // Remove dots that are no longer needed i.e. if number of chart points has decreased. dots.exit().remove(); // Create any new dots that are needed i.e. if number of chart points has increased. dots.enter().append('circle') .attr('r', LINE_CHART_ANOMALY_RADIUS) .on('mouseover', function (d) { showFocusChartTooltip(d, this); }) .on('mouseout', () => mlChartTooltipService.hide()); // Update all dots to new positions. dots.attr('cx', (d) => { return focusXScale(d.date); }) .attr('cy', (d) => { return focusYScale(d.value); }) .attr('class', (d) => { let markerClass = 'metric-value'; if (_.has(d, 'anomalyScore')) { markerClass += ` anomaly-marker ${getSeverityWithLow(d.anomalyScore)}`; } return markerClass; }); // Render cross symbols for any multi-bucket anomalies. const multiBucketMarkers = d3.select('.focus-chart-markers').selectAll('.multi-bucket') .data(data.filter(d => (d.anomalyScore !== null && showMultiBucketAnomalyMarker(d) === true))); // Remove multi-bucket markers that are no longer needed. multiBucketMarkers.exit().remove(); // Add any new markers that are needed i.e. if number of multi-bucket points has increased. multiBucketMarkers.enter().append('path') .attr('d', d3.svg.symbol().size(MULTI_BUCKET_SYMBOL_SIZE).type('cross')) .on('mouseover', function (d) { showFocusChartTooltip(d, this); }) .on('mouseout', () => mlChartTooltipService.hide()); // Update all markers to new positions. multiBucketMarkers.attr('transform', d => `translate(${focusXScale(d.date)}, ${focusYScale(d.value)})`) .attr('class', d => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore)}`); // Add rectangular markers for any scheduled events. const scheduledEventMarkers = d3.select('.focus-chart-markers').selectAll('.scheduled-event-marker') .data(data.filter(d => d.scheduledEvents !== undefined)); // Remove markers that are no longer needed i.e. if number of chart points has decreased. scheduledEventMarkers.exit().remove(); // Create any new markers that are needed i.e. if number of chart points has increased. scheduledEventMarkers.enter().append('rect') .attr('width', LINE_CHART_ANOMALY_RADIUS * 2) .attr('height', SCHEDULED_EVENT_SYMBOL_HEIGHT) .attr('class', 'scheduled-event-marker') .attr('rx', 1) .attr('ry', 1); // Update all markers to new positions. scheduledEventMarkers.attr('x', (d) => focusXScale(d.date) - LINE_CHART_ANOMALY_RADIUS) .attr('y', (d) => focusYScale(d.value) - 3); // Plot any forecast data in scope. if (scope.focusForecastData !== undefined) { focusChart.select('.area.forecast') .attr('d', focusBoundedArea(scope.focusForecastData)) .classed('hidden', !scope.showForecast); focusChart.select('.values-line.forecast') .attr('d', focusValuesLine(scope.focusForecastData)) .classed('hidden', !scope.showForecast); const forecastDots = d3.select('.focus-chart-markers.forecast').selectAll('.metric-value') .data(scope.focusForecastData); // Remove dots that are no longer needed i.e. if number of forecast points has decreased. forecastDots.exit().remove(); // Create any new dots that are needed i.e. if number of forecast points has increased. forecastDots.enter().append('circle') .attr('r', LINE_CHART_ANOMALY_RADIUS) .on('mouseover', function (d) { showFocusChartTooltip(d, this); }) .on('mouseout', () => mlChartTooltipService.hide()); // Update all dots to new positions. forecastDots.attr('cx', (d) => { return focusXScale(d.date); }) .attr('cy', (d) => { return focusYScale(d.value); }) .attr('class', 'metric-value') .classed('hidden', !scope.showForecast); } } function createZoomInfoElements(zoomGroup, fcsWidth) { // Create zoom duration links applicable for the current time span. // Don't add links for any durations which would give a brush extent less than 10px. const bounds = timefilter.getActiveBounds(); const boundsSecs = bounds.max.unix() - bounds.min.unix(); const minSecs = (10 / vizWidth) * boundsSecs; let xPos = 10; const zoomLabel = zoomGroup.append('text') .attr('x', xPos) .attr('y', 17) .attr('class', 'zoom-info-text') .text('Zoom:'); const zoomOptions = [{ durationMs: scope.autoZoomDuration, label: 'auto' }]; _.each(ZOOM_INTERVAL_OPTIONS, (option) => { if (option.duration.asSeconds() > minSecs && option.duration.asSeconds() < boundsSecs) { zoomOptions.push({ durationMs: option.duration.asMilliseconds(), label: option.label }); } }); xPos += (zoomLabel.node().getBBox().width + 4); _.each(zoomOptions, (option) => { const text = zoomGroup.append('a') .attr('data-ms', option.durationMs) .attr('href', '') .append('text') .attr('x', xPos) .attr('y', 17) .attr('class', 'zoom-info-text') .text(option.label); xPos += (text.node().getBBox().width + 4); }); zoomGroup.append('text') .attr('x', (xPos + 6)) .attr('y', 17) .attr('class', 'zoom-info-text zoom-aggregation-interval') .text('(aggregation interval: , bucket span: )'); if (scope.modelPlotEnabled === false) { const modelPlotLabel = zoomGroup.append('text') .attr('x', 300) .attr('y', 17) .attr('class', 'zoom-info-text') .text('Model bounds are not available'); modelPlotLabel.attr('x', (fcsWidth - (modelPlotLabel.node().getBBox().width + 10))); } $('.focus-zoom a').click(function (e) { e.preventDefault(); setZoomInterval($(this).attr('data-ms')); }); } function drawContextElements(cxtGroup, cxtWidth, cxtChartHeight, swlHeight) { const data = scope.contextChartData; contextXScale = d3.time.scale().range([0, cxtWidth]) .domain(calculateContextXAxisDomain()); const combinedData = scope.contextForecastData === undefined ? data : data.concat(scope.contextForecastData); const valuesRange = { min: Number.MAX_VALUE, max: Number.MIN_VALUE }; _.each(combinedData, (item) => { valuesRange.min = Math.min(item.value, valuesRange.min); valuesRange.max = Math.max(item.value, valuesRange.max); }); let dataMin = valuesRange.min; let dataMax = valuesRange.max; const chartLimits = { min: dataMin, max: dataMax }; if (scope.modelPlotEnabled === true || (scope.contextForecastData !== undefined && scope.contextForecastData.length > 0)) { const boundsRange = { min: Number.MAX_VALUE, max: Number.MIN_VALUE }; _.each(combinedData, (item) => { boundsRange.min = Math.min(item.lower, boundsRange.min); boundsRange.max = Math.max(item.upper, boundsRange.max); }); dataMin = Math.min(dataMin, boundsRange.min); dataMax = Math.max(dataMax, boundsRange.max); // Set the y axis domain so that the range of actual values takes up at least 50% of the full range. if ((valuesRange.max - valuesRange.min) < 0.5 * (dataMax - dataMin)) { if (valuesRange.min > dataMin) { chartLimits.min = valuesRange.min - (0.5 * (valuesRange.max - valuesRange.min)); } if (valuesRange.max < dataMax) { chartLimits.max = valuesRange.max + (0.5 * (valuesRange.max - valuesRange.min)); } } } contextYScale = d3.scale.linear().range([cxtChartHeight, contextChartLineTopMargin]) .domain([chartLimits.min, chartLimits.max]); const borders = cxtGroup.append('g') .attr('class', 'axis'); // Add borders left and right. borders.append('line') .attr('x1', 0) .attr('y1', 0) .attr('x2', 0) .attr('y2', cxtChartHeight + swlHeight); borders.append('line') .attr('x1', cxtWidth) .attr('y1', 0) .attr('x2', cxtWidth) .attr('y2', cxtChartHeight + swlHeight); // Add x axis. const bounds = timefilter.getActiveBounds(); const timeBuckets = new TimeBuckets(); timeBuckets.setInterval('auto'); timeBuckets.setBounds(bounds); const xAxisTickFormat = timeBuckets.getScaledDateFormat(); const xAxis = d3.svg.axis().scale(contextXScale) .orient('top') .innerTickSize(-cxtChartHeight) .outerTickSize(0) .tickPadding(0) .ticks(numTicksForDateFormat(cxtWidth, xAxisTickFormat)) .tickFormat((d) => { return moment(d).format(xAxisTickFormat); }); cxtGroup.datum(data); const contextBoundsArea = d3.svg.area() .x((d) => { return contextXScale(d.date); }) .y0((d) => { return contextYScale(Math.min(chartLimits.max, Math.max(d.lower, chartLimits.min))); }) .y1((d) => { return contextYScale(Math.max(chartLimits.min, Math.min(d.upper, chartLimits.max))); }) .defined(d => (d.lower !== null && d.upper !== null)); if (scope.modelPlotEnabled === true) { cxtGroup.append('path') .datum(data) .attr('class', 'area context') .attr('d', contextBoundsArea); } const contextValuesLine = d3.svg.line() .x((d) => { return contextXScale(d.date); }) .y((d) => { return contextYScale(d.value); }) .defined(d => d.value !== null); cxtGroup.append('path') .datum(data) .attr('class', 'values-line') .attr('d', contextValuesLine); drawLineChartDots(data, cxtGroup, contextValuesLine, 1); // Create the path elements for the forecast value line and bounds area. if (scope.contextForecastData !== undefined) { cxtGroup.append('path') .datum(scope.contextForecastData) .attr('class', 'area forecast') .attr('d', contextBoundsArea); cxtGroup.append('path') .datum(scope.contextForecastData) .attr('class', 'values-line forecast') .attr('d', contextValuesLine); } // Create and draw the anomaly swimlane. const swimlane = cxtGroup.append('g') .attr('class', 'swimlane') .attr('transform', 'translate(0,' + cxtChartHeight + ')'); drawSwimlane(swimlane, cxtWidth, swlHeight); // Draw a mask over the sections of the context chart and swimlane // which fall outside of the zoom brush selection area. mask = new ContextChartMask(cxtGroup, scope.contextChartData, scope.modelPlotEnabled, swlHeight) .x(contextXScale) .y(contextYScale); // Draw the x axis on top of the mask so that the labels are visible. cxtGroup.append('g') .attr('class', 'x axis context-chart-axis') .call(xAxis); // Move the x axis labels up so that they are inside the contact chart area. cxtGroup.selectAll('.x.context-chart-axis text') .attr('dy', (cxtChartHeight - 5)); filterAxisLabels(cxtGroup.selectAll('.x.context-chart-axis'), cxtWidth); drawContextBrush(cxtGroup); } function drawContextBrush(contextGroup) { // Create the brush for zooming in to the focus area of interest. brush.x(contextXScale) .on('brush', brushing) .on('brushend', brushed); contextGroup.append('g') .attr('class', 'x brush') .call(brush) .selectAll('rect') .attr('y', -1) .attr('height', contextChartHeight + swimlaneHeight + 1); // move the left and right resize areas over to // be under the handles contextGroup.selectAll('.w rect') .attr('x', -10) .attr('width', 10); contextGroup.selectAll('.e rect') .attr('x', 0) .attr('width', 10); const topBorder = contextGroup.append('rect') .attr('class', 'top-border') .attr('y', -2) .attr('height', contextChartLineTopMargin); // Draw the brush handles using SVG foreignObject elements. // Note these are not supported on IE11 and below, so will not appear in IE. const leftHandle = contextGroup.append('foreignObject') .attr('width', 10) .attr('height', 90) .attr('class', 'brush-handle') .html('<div class="brush-handle-inner brush-handle-inner-left"><i class="fa fa-caret-left"></i></div>'); const rightHandle = contextGroup.append('foreignObject') .attr('width', 10) .attr('height', 90) .attr('class', 'brush-handle') .html('<div class="brush-handle-inner brush-handle-inner-right"><i class="fa fa-caret-right"></i></div>'); setBrushVisibility(!brush.empty()); function showBrush(show) { if (show === true) { const brushExtent = brush.extent(); mask.reveal(brushExtent); leftHandle.attr('x', contextXScale(brushExtent[0]) - 10); rightHandle.attr('x', contextXScale(brushExtent[1]) + 0); topBorder.attr('x', contextXScale(brushExtent[0]) + 1); topBorder.attr('width', contextXScale(brushExtent[1]) - contextXScale(brushExtent[0]) - 2); } setBrushVisibility(show); } function brushing() { const isEmpty = brush.empty(); showBrush(!isEmpty); } function brushed() { const isEmpty = brush.empty(); showBrush(!isEmpty); const selectedBounds = isEmpty ? contextXScale.domain() : brush.extent(); const selectionMin = selectedBounds[0].getTime(); const selectionMax = selectedBounds[1].getTime(); // Set the color of the swimlane cells according to whether they are inside the selection. contextGroup.selectAll('.swimlane-cell') .style('fill', (d) => { const cellMs = d.date.getTime(); if (cellMs < selectionMin || cellMs > selectionMax) { return anomalyGrayScale(d.score); } else { return anomalyColorScale(d.score); } }); scope.selectedBounds = { min: moment(selectionMin), max: moment(selectionMax) }; scope.$root.$broadcast('contextChartSelected', { from: selectedBounds[0], to: selectedBounds[1] }); } } function setBrushVisibility(show) { if (mask !== undefined) { const visibility = show ? 'visible' : 'hidden'; mask.style('visibility', visibility); d3.selectAll('.brush').style('visibility', visibility); const brushHandles = d3.selectAll('.brush-handle-inner'); brushHandles.style('visibility', visibility); const topBorder = d3.selectAll('.top-border'); topBorder.style('visibility', visibility); const border = d3.selectAll('.chart-border-highlight'); border.style('visibility', visibility); } } function drawSwimlane(swlGroup, swlWidth, swlHeight) { const data = scope.swimlaneData; // Calculate the x axis domain. // Elasticsearch aggregation returns points at start of bucket, so set the // x-axis min to the start of the aggregation interval. // Need to use the min(earliest) and max(earliest) of the context chart // aggregation to align the axes of the chart and swimlane elements. const xAxisDomain = calculateContextXAxisDomain(); const x = d3.time.scale().range([0, swlWidth]) .domain(xAxisDomain); const y = d3.scale.linear().range([swlHeight, 0]) .domain([0, swlHeight]); const xAxis = d3.svg.axis() .scale(x) .orient('bottom') .innerTickSize(-swlHeight) .outerTickSize(0); const yAxis = d3.svg.axis() .scale(y) .orient('left') .tickValues(y.domain()) .innerTickSize(-swlWidth) .outerTickSize(0); const axes = swlGroup.append('g'); axes.append('g') .attr('class', 'x axis') .attr('transform', 'translate(0,' + (swlHeight) + ')') .call(xAxis); axes.append('g') .attr('class', 'y axis') .call(yAxis); const earliest = xAxisDomain[0].getTime(); const latest = xAxisDomain[1].getTime(); const swimlaneAggMs = scope.contextAggregationInterval.asMilliseconds(); let cellWidth = swlWidth / ((latest - earliest) / swimlaneAggMs); if (cellWidth < 1) { cellWidth = 1; } const cells = swlGroup.append('g') .attr('class', 'swimlane-cells') .selectAll('cells') .data(data); cells.enter().append('rect') .attr('x', (d) => { return x(d.date); }) .attr('y', 0) .attr('rx', 0) .attr('ry', 0) .attr('class', (d) => { return d.score > 0 ? 'swimlane-cell' : 'swimlane-cell-hidden';}) .attr('width', cellWidth) .attr('height', swlHeight) .style('fill', (d) => { return anomalyColorScale(d.score);}); } function calculateContextXAxisDomain() { // Calculates the x axis domain for the context elements. // Elasticsearch aggregation returns points at start of bucket, // so set the x-axis min to the start of the first aggregation interval, // and the x-axis max to the end of the last aggregation interval. // Context chart and swimlane use the same aggregation interval. const bounds = timefilter.getActiveBounds(); let earliest = bounds.min.valueOf(); if (scope.swimlaneData !== undefined && scope.swimlaneData.length > 0) { // Adjust the earliest back to the time of the first swimlane point // if this is before the time filter minimum. earliest = Math.min(_.first(scope.swimlaneData).date.getTime(), bounds.min.valueOf()); } const contextAggMs = scope.contextAggregationInterval.asMilliseconds(); const earliestMs = Math.floor(earliest / contextAggMs) * contextAggMs; const latestMs = Math.ceil((bounds.max.valueOf()) / contextAggMs) * contextAggMs; return [new Date(earliestMs), new Date(latestMs)]; } // Sets the extent of the brush on the context chart to the // supplied from and to Date objects. function setContextBrushExtent(from, to, fireEvent) { brush.extent([from, to]); brush(d3.select('.brush')); if (fireEvent) { brush.event(d3.select('.brush')); } } function setZoomInterval(ms) { const bounds = timefilter.getActiveBounds(); const minBoundsMs = bounds.min.valueOf(); const maxBoundsMs = bounds.max.valueOf(); // Attempt to retain the same zoom end time. // If not, go back to the bounds start and add on the required millis. const millis = +ms; let to = scope.zoomTo.getTime(); let from = to - millis; if (from < minBoundsMs) { from = minBoundsMs; to = Math.min(minBoundsMs + millis, maxBoundsMs); } setContextBrushExtent(new Date(from), new Date(to), true); } function showFocusChartTooltip(marker, circle) { // Show the time and metric values in the tooltip. // Uses date, value, upper, lower and anomalyScore (optional) marker properties. const formattedDate = moment(marker.date).format('MMMM Do YYYY, HH:mm'); let contents = formattedDate + '<br/><hr/>'; if (_.has(marker, 'anomalyScore')) { const score = parseInt(marker.anomalyScore); const displayScore = (score > 0 ? score : '< 1'); contents += `anomaly score: ${displayScore}<br/>`; if (showMultiBucketAnomalyTooltip(marker) === true) { contents += `multi-bucket impact: ${getMultiBucketImpactLabel(marker.multiBucketImpact)}<br/>`; } if (scope.modelPlotEnabled === false) { // Show actual/typical when available except for rare detectors. // Rare detectors always have 1 as actual and the probability as typical. // Exposing those values in the tooltip with actual/typical labels might irritate users. if (_.has(marker, 'actual') && marker.function !== 'rare') { // Display the record actual in preference to the chart value, which may be // different depending on the aggregation interval of the chart. contents += `actual: ${formatValue(marker.actual, marker.function, fieldFormat)}`; contents += `<br/>typical: ${formatValue(marker.typical, marker.function, fieldFormat)}`; } else { contents += `value: ${formatValue(marker.value, marker.function, fieldFormat)}`; if (_.has(marker, 'byFieldName') && _.has(marker, 'numberOfCauses')) { const numberOfCauses = marker.numberOfCauses; const byFieldName = mlEscape(marker.byFieldName); if (numberOfCauses < 10) { // If numberOfCauses === 1, won't go into this block as actual/typical copied to top level fields. contents += `<br/> ${numberOfCauses} unusual ${byFieldName} values`; } else { // Maximum of 10 causes are stored in the record, so '10' may mean more than 10. contents += `<br/> ${numberOfCauses}+ unusual ${byFieldName} values`; } } } } else { contents += `value: ${formatValue(marker.value, marker.function, fieldFormat)}`; contents += `<br/>upper bounds: ${formatValue(marker.upper, marker.function, fieldFormat)}`; contents += `<br/>lower bounds: ${formatValue(marker.lower, marker.function, fieldFormat)}`; } } else { // TODO - need better formatting for small decimals. if (_.get(marker, 'isForecast', false) === true) { contents += `prediction: ${formatValue(marker.value, marker.function, fieldFormat)}`; } else { contents += `value: ${formatValue(marker.value, marker.function, fieldFormat)}`; } if (scope.modelPlotEnabled === true) { contents += `<br/>upper bounds: ${formatValue(marker.upper, marker.function, fieldFormat)}`; contents += `<br/>lower bounds: ${formatValue(marker.lower, marker.function, fieldFormat)}`; } } if (_.has(marker, 'scheduledEvents')) { contents += `<br/><hr/>Scheduled events:<br/>${marker.scheduledEvents.map(mlEscape).join('<br/>')}`; } mlChartTooltipService.show(contents, circle, { x: LINE_CHART_ANOMALY_RADIUS * 2, y: 0 }); } function highlightFocusChartAnomaly(record) { // Highlights the anomaly marker in the focus chart corresponding to the specified record. // Find the anomaly marker which corresponds to the time of the anomaly record. // Depending on the way the chart is aggregated, there may not be // a point at exactly the same time as the record being highlighted. const anomalyTime = record.source.timestamp; const markerToSelect = findChartPointForAnomalyTime(scope.focusChartData, anomalyTime); // Render an additional highlighted anomaly marker on the focus chart. // TODO - plot anomaly markers for cases where there is an anomaly due // to the absence of data and model plot is enabled. if (markerToSelect !== undefined) { const selectedMarker = d3.select('.focus-chart-markers') .selectAll('.focus-chart-highlighted-marker') .data([markerToSelect]); if (showMultiBucketAnomalyMarker(markerToSelect) === true) { selectedMarker.enter().append('path') .attr('d', d3.svg.symbol().size(MULTI_BUCKET_SYMBOL_SIZE).type('cross')) .attr('transform', d => `translate(${focusXScale(d.date)}, ${focusYScale(d.value)})`) .attr('class', d => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore)} highlighted`); } else { selectedMarker.enter().append('circle') .attr('r', LINE_CHART_ANOMALY_RADIUS) .attr('cx', d => focusXScale(d.date)) .attr('cy', d => focusYScale(d.value)) .attr('class', d => `anomaly-marker metric-value ${getSeverityWithLow(d.anomalyScore)} highlighted`); } // Display the chart tooltip for this marker. // Note the values of the record and marker may differ depending on the levels of aggregation. const anomalyMarker = $('.focus-chart-markers .anomaly-marker.highlighted'); if (anomalyMarker.length) { showFocusChartTooltip(markerToSelect, anomalyMarker[0]); } } } function unhighlightFocusChartAnomaly() { d3.select('.focus-chart-markers').selectAll('.anomaly-marker.highlighted').remove(); mlChartTooltipService.hide(); } }
function drawContextElements(cxtGroup, cxtWidth, cxtChartHeight, swlHeight) { const data = scope.contextChartData; contextXScale = d3.time.scale().range([0, cxtWidth]) .domain(calculateContextXAxisDomain()); const combinedData = scope.contextForecastData === undefined ? data : data.concat(scope.contextForecastData); const valuesRange = { min: Number.MAX_VALUE, max: Number.MIN_VALUE }; _.each(combinedData, (item) => { valuesRange.min = Math.min(item.value, valuesRange.min); valuesRange.max = Math.max(item.value, valuesRange.max); }); let dataMin = valuesRange.min; let dataMax = valuesRange.max; const chartLimits = { min: dataMin, max: dataMax }; if (scope.modelPlotEnabled === true || (scope.contextForecastData !== undefined && scope.contextForecastData.length > 0)) { const boundsRange = { min: Number.MAX_VALUE, max: Number.MIN_VALUE }; _.each(combinedData, (item) => { boundsRange.min = Math.min(item.lower, boundsRange.min); boundsRange.max = Math.max(item.upper, boundsRange.max); }); dataMin = Math.min(dataMin, boundsRange.min); dataMax = Math.max(dataMax, boundsRange.max); // Set the y axis domain so that the range of actual values takes up at least 50% of the full range. if ((valuesRange.max - valuesRange.min) < 0.5 * (dataMax - dataMin)) { if (valuesRange.min > dataMin) { chartLimits.min = valuesRange.min - (0.5 * (valuesRange.max - valuesRange.min)); } if (valuesRange.max < dataMax) { chartLimits.max = valuesRange.max + (0.5 * (valuesRange.max - valuesRange.min)); } } } contextYScale = d3.scale.linear().range([cxtChartHeight, contextChartLineTopMargin]) .domain([chartLimits.min, chartLimits.max]); const borders = cxtGroup.append('g') .attr('class', 'axis'); // Add borders left and right. borders.append('line') .attr('x1', 0) .attr('y1', 0) .attr('x2', 0) .attr('y2', cxtChartHeight + swlHeight); borders.append('line') .attr('x1', cxtWidth) .attr('y1', 0) .attr('x2', cxtWidth) .attr('y2', cxtChartHeight + swlHeight); // Add x axis. const bounds = timefilter.getActiveBounds(); const timeBuckets = new TimeBuckets(); timeBuckets.setInterval('auto'); timeBuckets.setBounds(bounds); const xAxisTickFormat = timeBuckets.getScaledDateFormat(); const xAxis = d3.svg.axis().scale(contextXScale) .orient('top') .innerTickSize(-cxtChartHeight) .outerTickSize(0) .tickPadding(0) .ticks(numTicksForDateFormat(cxtWidth, xAxisTickFormat)) .tickFormat((d) => { return moment(d).format(xAxisTickFormat); }); cxtGroup.datum(data); const contextBoundsArea = d3.svg.area() .x((d) => { return contextXScale(d.date); }) .y0((d) => { return contextYScale(Math.min(chartLimits.max, Math.max(d.lower, chartLimits.min))); }) .y1((d) => { return contextYScale(Math.max(chartLimits.min, Math.min(d.upper, chartLimits.max))); }) .defined(d => (d.lower !== null && d.upper !== null)); if (scope.modelPlotEnabled === true) { cxtGroup.append('path') .datum(data) .attr('class', 'area context') .attr('d', contextBoundsArea); } const contextValuesLine = d3.svg.line() .x((d) => { return contextXScale(d.date); }) .y((d) => { return contextYScale(d.value); }) .defined(d => d.value !== null); cxtGroup.append('path') .datum(data) .attr('class', 'values-line') .attr('d', contextValuesLine); drawLineChartDots(data, cxtGroup, contextValuesLine, 1); // Create the path elements for the forecast value line and bounds area. if (scope.contextForecastData !== undefined) { cxtGroup.append('path') .datum(scope.contextForecastData) .attr('class', 'area forecast') .attr('d', contextBoundsArea); cxtGroup.append('path') .datum(scope.contextForecastData) .attr('class', 'values-line forecast') .attr('d', contextValuesLine); } // Create and draw the anomaly swimlane. const swimlane = cxtGroup.append('g') .attr('class', 'swimlane') .attr('transform', 'translate(0,' + cxtChartHeight + ')'); drawSwimlane(swimlane, cxtWidth, swlHeight); // Draw a mask over the sections of the context chart and swimlane // which fall outside of the zoom brush selection area. mask = new ContextChartMask(cxtGroup, scope.contextChartData, scope.modelPlotEnabled, swlHeight) .x(contextXScale) .y(contextYScale); // Draw the x axis on top of the mask so that the labels are visible. cxtGroup.append('g') .attr('class', 'x axis context-chart-axis') .call(xAxis); // Move the x axis labels up so that they are inside the contact chart area. cxtGroup.selectAll('.x.context-chart-axis text') .attr('dy', (cxtChartHeight - 5)); filterAxisLabels(cxtGroup.selectAll('.x.context-chart-axis'), cxtWidth); drawContextBrush(cxtGroup); }
'use strict'; /**/ var React = require('react'); var d3 = require('d3'); require('styles/react-stockcharts'); require('stylesheets/re-stock'); var ReadME = require('md/MAIN.md'); document.getElementById("content").innerHTML = ReadME; var parseDate = d3.time.format("%Y-%m-%d").parse d3.tsv("data/MSFT.tsv", function(err, data) { data.forEach((d, i) => { d.date = new Date(parseDate(d.date).getTime()); d.open = +d.open; d.high = +d.high; d.low = +d.low; d.close = +d.close; d.volume = +d.volume; // console.log(d); }); var CandleStickChartWithEdge = require('lib/charts/CandleStickChartWithEdge'); React.render(<CandleStickChartWithEdge data={data} />, document.getElementById("chart")); }); //require('./examples/freezer-example');
function link(scope, el, attr){ var renderTimeout; var m = 50; // DATA PREP var parseDate = d3.time.format("%x").parse; var xScale = d3.time.scale() // .range([0, _o.width]); var yScale = d3.scale.linear() // .range([_o.height, 0]); // APPEND SVG TO CONTAINER ELEMENT var svg = d3.select(el[0]) .append('svg'); // GENERATORS var xAxis = d3.svg.axis() .scale(xScale) .orient("bottom"); var yAxis = d3.svg.axis() .scale(yScale) .orient("left"); var lineGen = d3.svg.line() .x(function (d) { return xScale(d.plotDay); }) .y(function (d) { return yScale(d.plotCnt); }) .interpolate("linear"); var xAxisG = svg.append('g').attr('class', 'x-axis'); var yAxisG = svg.append('g').attr('class', 'y-axis'); // var lines = svg.append('g').attr('class', 'lines').selectAll('g.line'); // UPDATE function update(){ // if (!dateRange) return; if (renderTimeout) { _o.$timeout.cancel(renderTimeout); } renderTimeout = _o.$timeout( function () { var parseDate = d3.time.format("%x").parse; scope.data.forEach(function (d) { d.plotDay = parseDate(d.day); d.plotCnt = +d.cnt; }); xScale.domain(d3.extent(scope.data, function (d) { return d.plotDay; })); yScale.domain(d3.extent(scope.data, function (d) { return d.plotCnt; })); // RENDER // svg.selectAll('*').remove(); var lines = svg .append("path") .datum(scope.data) .attr("class", "line") .attr("d", lineGen); lines .exit() .remove(); xAxisG.call(xAxis); yAxisG.call(yAxis) // .append("text") // .attr("transform", "rotate(-90)") // .attr("y", 6) // .attr("dy", ".71em") // .style("text-anchor", "end") // .text("#"); }, 200 ); } // REACTIVITY // scope.$watch('dateRange', function(newVals, oldVals) { // return update(); // }, false); // scope.$watch('dateRange', function(newVals) { // update(); // }, false); scope.$watch(function() { return angular.element(_o.$window)[0].innerWidth; }, resize ); // responsive _o.$window.onresize = function() { scope.$apply(); }; function resize(){ svg.attr({ width: _o.width, height: _o.height }); xScale.range([ _o.margin.left, _o.width - _o.margin.left ]); yScale.range([ _o.height - _o.margin.left , _o.margin.left ]); xAxis.tickSize( -_o.height + 2 * _o.margin.left ); yAxis.tickSize( -_o.width + 2 * _o.margin.left); xAxisG.attr('transform', 'translate(' + [0, yScale.range()[0] + 0.5] + ')'); yAxisG.attr('transform', 'translate(' + [xScale.range()[0], 0] + ')'); update(); } }
// If the y-domain is positive and could reasonably be displayed to include zero, expand it to do so. // (Eg. the range is 5 to 50, do it; if it is 5 to 8, do not. Set the boundary arbitrarily at 5 to 12.5, ie. 1:2.5.) if ((thisYDomain[0] > 0) && (thisYDomain[0] / thisYDomain[1] < 0.4)) { thisYDomain[0] = 0; } // If the y-domain is negative and could reasonably be displayed to include zero, expand it to do so. if ((thisYDomain[1] < 0) && (thisYDomain[0] / thisYDomain[1] < 0.4)) { thisYDomain[1] = 0; } } } } let xScale; if (domain.x[0] instanceof Date) { xScale = d3.time.scale(); } else { xScale = d3.scale.linear(); } xScale.range([0, size.width]).domain(domain.x); // The x-axis takes up plot space, if it is at the bottom of the plot (ie. if the y-domain is entirely positive), // but not if it is in the middle of the plot (ie. if the y-domain includes zero). // // For now, we assume that the x-axis will be displayed aligned with the first data series' y-scale. const mainYDomain = domain.y[allUnits[0]]; const yContainsZero = (mainYDomain[0] < 0 && mainYDomain[1] > 0); if (yContainsZero) {
render: function() { var self = this; var title = this.props.title; var values = _.without(this.props.values,'NULL'); var period = this.props.period; var line, lastValue, gradingValue; var labelColor = 'default'; try { gradingValue = Math.round(values[values.length - 1].Value) || 0; // PI value } catch (err) { gradingValue = 0; debug(err); } var tooltipString = 'Performance Indicator'; // lastValue = values[values.length - 1].Value || 0; lastValue = Math.round(values[values.length - 1].Value) || 0; tooltipString = 'KPI'; // Color grading of PI label if(gradingValue >= 0 && gradingValue <= 2) { labelColor = 'danger'; } else if(gradingValue >= 2 && gradingValue <= 3) { labelColor = 'warning'; } else if(gradingValue >= 3) { labelColor = 'success'; } else { labelColor = 'default'; } // D3 configuration var margin = {top: 20, right: 20, bottom: 60, left: 50}, width = 500 - margin.left - margin.right, height = 200 - margin.top - margin.bottom; var parseDate = d3.time.format("%m/%d/%Y").parse; if(period) { var startYear = moment('2014').subtract(period, 'years'); } else { var startYear = moment('2014').subtract(5, 'years'); } var x = d3.time.scale() .range([0, width]) .domain([startYear.toDate(), moment('12-30-2014').toDate()]).clamp(true); var y = d3.scale.linear() .range([height, 1]); var xAxis = d3.svg.axis() .scale(x) .tickPadding(12) .orient("bottom"); var yAxis = d3.svg.axis() .scale(y) .orient("left"); line = d3.svg.line() .defined(function(d) { return d.Value != 'NULL'; }) .x(function(d) { return x(parseDate(d.Date)); }) .y(function(d) { return y(Number(d.Value)); }); d3.select('#'+title.replace(/ /g, '-')).select('svg').remove(); // This should not be necessary, break up d3 into smaller components? var svg = d3.select('#'+title).append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); // y.domain(d3.extent(values, function(d) { return Number(d.Value); })); y.domain([1, 10]); var xaxis = svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .call(xAxis); xaxis.selectAll("text") .style("text-anchor", "end") .attr("dx", "-.8em") .attr("dy", ".15em") .attr("transform", function(d) { return "rotate(-65)"; }); svg.append("g") .attr("class", "y axis") .call(yAxis) .append("text") .text('KPI') .attr("transform", "rotate(-90)") .attr("y", 6) .attr("dy", ".71em") .style("text-anchor", "end"); var withoutNull = _.without(values, 'NULL'); svg.append('circle') .attr('class', 'sparkcircle') .attr('cx', x(parseDate(withoutNull[withoutNull.length - 1].Date))) .attr('cy', y(Number(withoutNull[withoutNull.length - 1].Value))) .attr('r', 5); var path = svg.append("path") .datum(values) .attr("class", "line") .attr("d", line); path.each(function(d) { d.totalLength = this.getTotalLength(); }) // Add total length per path, needed for animating over full length path .attr("stroke-dasharray", function(d) { return d.totalLength + " " + d.totalLength; }) .attr("stroke-dashoffset", function(d) { return d.totalLength; }) .transition() .duration(1000) .ease("linear") .attr("stroke-dashoffset", 0); var bisectDate = d3.bisector(function(d) { return parseDate(d.Date); }).left; var focus = svg.append("g") .attr("class", "focus") .style("display", "none"); focus.append("circle") .attr("r", 4.5); focus.append("text") .attr("x", 9) .attr("dy", ".35em"); svg.append("rect") .attr("class", "overlay") .attr("width", width) .attr("height", height) .on("mouseover", function() { focus.style("display", null); }) .on("mouseout", function() { focus.style("display", "none"); }) .on("mousemove", mousemove); function mousemove() { var x0 = x.invert(d3.mouse(this)[0]), i = bisectDate(values, x0, 1), d0 = values[i - 1], d1 = values[i], d = x0 - d0.Date > d1.Date - x0 ? d1 : d0; focus.attr("transform", "translate(" + x(parseDate(d.Date)) + "," + y(d.Value) + ")"); focus.select("text").text(d.Value); } var classesTitlebar = cx({ 'panel-heading': true, 'rightarrowdiv': self.props.active, 'active': self.props.active }); var classesContainer = cx({ 'panel': true, 'panel-default': true, 'active': self.props.active }); return ( <div className={classesContainer} onClick={this.handleHistoClick} ref="histoRoot"> <div style={{position:'relative'}} className={classesTitlebar}> <OverlayTrigger placement="right" overlay={<Tooltip><strong>{tooltipString}</strong></Tooltip>}> <Label onClick={this.toggleGraph} style={{float:'right', fontSize:'1.1em', cursor: 'pointer', backgroundColor:Utils.quantize(lastValue)}}>{lastValue}</Label> </OverlayTrigger> <span onClick={this.handleHistoClick} style={{cursor:'pointer', fontWeight:'bold', fontSize:'1.1em'}}>{title}</span> </div> <div className="panel-body"> <div className="histoChart" ref="chart" id={title} /> </div> </div> ); }
d3.gantt = function() { var FIT_TIME_DOMAIN_MODE = "fit"; var FIXED_TIME_DOMAIN_MODE = "fixed"; var DEFAULT_BLOCK_COLOR = "#57c17b"; var margin = { top : 10, right : 65, bottom : 40, left : 75 }; var padding = { top : 10, right : 10, bottom : 10, left : 10 }; var timeDomainStart = d3.time.day.offset(new Date(),-3); var timeDomainEnd = d3.time.hour.offset(new Date(),+3); var timeDomainMode = FIT_TIME_DOMAIN_MODE;// fixed or fit var taskTypes = []; var height = document.body.clientHeight - margin.top - margin.bottom-5; var width = document.body.clientWidth - margin.right - margin.left-5; var tooltipFields = []; var tickFormat = "%H:%M"; var ticks = 10; var keyFunction = function(d) { return d.startDate + d.taskName + d.endDate; }; var colorFunction = function (d) { // Default block color return DEFAULT_BLOCK_COLOR; }; var rectTransform = function(d) { return "translate(" + x(d.startDate) + "," + y(d.taskName) + ")"; }; var x = d3.time.scale().domain([ timeDomainStart, timeDomainEnd ]).range([ 0, width ]).clamp(true); var y = d3.scale.ordinal().domain(taskTypes).rangeRoundBands([ 0, height - margin.top - margin.bottom ], .1); var xAxis = d3.svg.axis().scale(x).orient("bottom").tickFormat(d3.time.format(tickFormat)).ticks(ticks); var yAxis = d3.svg.axis().scale(y).orient("left").tickSize(0); var initTimeDomain = function(tasks) { if (timeDomainMode === FIT_TIME_DOMAIN_MODE) { if (tasks === undefined || tasks.length < 1) { timeDomainStart = d3.time.day.offset(new Date(), -3); timeDomainEnd = d3.time.hour.offset(new Date(), +3); return; } tasks.sort(function(a, b) { return a.endDate - b.endDate; }); timeDomainEnd = tasks[tasks.length - 1].endDate; tasks.sort(function(a, b) { return a.startDate - b.startDate; }); timeDomainStart = new Date(tasks[0].startDate); timeDomainEnd = new Date(); } }; var tickFormatter = function (val) { return d3.time.format(tickFormat)(val); } var initAxis = function() { x = d3.time.scale().domain([ timeDomainStart, timeDomainEnd ]).range([ 0, width - margin.left - margin.right ]).clamp(true); y = d3.scale.ordinal().domain(taskTypes).rangeRoundBands([ 0, height - margin.top - margin.bottom ], .1); xAxis = d3.svg.axis().scale(x).orient("bottom").tickFormat(tickFormatter).ticks(ticks); yAxis = d3.svg.axis().scale(y).orient("left").tickSize(0); }; function gantt(tasks, element, timefilter) { // Set minimal and active size for the chart. width = element.width() - padding.left - padding.right; height = element.height() - padding.top - padding.bottom; width = width < 440 ? 440 : width; height = height < 150 ? 150 : height; // Init time domain and axis initTimeDomain(tasks); initAxis(); // Empty previous chart draw, the redraw function should be used somewhow instead. element.empty(); // Hide old tooltips if left after page refresh (bugfix) $('.gantt-tooltip').css("opacity", 0); var brush = d3.svg.brush() .x(x) .on("brushend", function () { var extent = brush.extent(); var from = moment(extent[0]); var to = moment(extent[1]); if (to - from === 0) return; timefilter.time.from = from; timefilter.time.to = to; timefilter.time.mode = 'absolute'; }); var tooltip = d3.select("body") .append("div") .attr("class", "gantt-tooltip") .style("opacity", 0); var svg = d3.select(element[0]) .append("svg") .attr("class", "chart") .attr("width", width) .attr("height", height) .append("g") .attr("class", "gantt-chart") .attr("transform", "translate(" + margin.left + ", " + margin.top + ")"); gBrush = svg.append("g") .attr("class", "brush") .call(brush) .selectAll("rect") .attr("height", height - margin.top - margin.bottom); svg.selectAll(".chart") .data(tasks, keyFunction).enter() .append("rect") .attr("rx", 5) .attr("ry", 5) .style("fill", function(d){ return colorFunction(d.color); }) .attr("transform", rectTransform) .attr("height", function(d) { return y.rangeBand(); }) .attr("width", function(d) { return (x(d.endDate) - x(d.startDate)); }) .on('mouseover', function (d) { tooltip.transition().duration(200).style("opacity", .93); tooltip.html(TooltipFormatter(d)) .style("left", (d3.event.pageX) + "px") .style("top", (d3.event.pageY) + "px"); }) .on('mouseout', function (d) { tooltip.transition().duration(300).style("opacity", 0); }); svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0, " + (height - margin.top - margin.bottom) + ")") .transition() .call(xAxis); svg.append("g").attr("class", "y axis").transition().call(yAxis); return gantt; }; var TooltipFormatter = function (dataElement) { var html = "<table>"; tooltipFields.forEach(function (elem) { html += "<tr><td><b>" + elem + "</b></td><td>" + dataElement[elem] + "</td></tr>"; }) return html; } gantt.margin = function(value) { if (!arguments.length) return margin; margin = value; return gantt; }; gantt.timeDomain = function(start, end) { if ((!start) || (!end)) return [ timeDomainStart, timeDomainEnd ]; timeDomainStart = start, timeDomainEnd = end; return gantt; }; /** * @param {string} * vale The value can be "fit" - the domain fits the data or * "fixed" - fixed domain. */ gantt.timeDomainMode = function(value) { if (!arguments.length) return timeDomainMode; timeDomainMode = value; return gantt; }; gantt.taskTypes = function(value) { if (!arguments.length) return taskTypes; taskTypes = value; return gantt; }; gantt.width = function(value) { if (!arguments.length) return width; width = +value; return gantt; }; gantt.height = function(value) { if (!arguments.length) return height; height = +value; return gantt; }; gantt.tickFormat = function(value) { if (!arguments.length) return tickFormat; tickFormat = value; return gantt; }; gantt.ticks = function(value) { if (!arguments.length) return ticks; ticks = value; return gantt; }; // Generates a color for the gantt bars. gantt.colorFunction = function (value) { colorFunction = value; return gantt; } // Generates a color for the gantt bars. gantt.tooltipFields = function (value) { tooltipFields = value; return gantt; } return gantt; };
define(function(require) { var Base = require('util/base'), d3 = require('d3'), _ = require('underscore'); // Interval return Base.extend({ defaults: { name: '', unitWidth: 20, unitCellWidth: 19, unitFormat: d3.time.format('%-d'), unitInterval: d3.time.day, groupInterval: d3.time.week }, getUnitBackgroundColor: function(d,i) { }, getMinGroup: function(data, accessor) { }, getMaxGroup: function(data, accessor) { }, setDomain: function(domain) { this.set('domain', domain); }, setRange: function(range) { this.set('range', range); }, getUnitSteps: function() { return this.get('unitInterval').range.apply(undefined, this.get('domain')); }, getGroupSteps: function() { return this.get('groupInterval').range.apply(undefined, this.get('domain')); }, getGroupText: function(start) { }, appendGroupCell: function(sel) { }, getUnitText: function(date) { }, getXScale: function() { var scale = d3.time.scale() .domain(this.get('domain')) .rangeRound(this.get('range')); return scale; }, getWidthScale: function() { var scale = this.getXScale(); var correct = this.correctDate; return function(start, end) { return scale(correct(end)) - scale(start); } }, // Used to add one day to the input. This is done because a // task that ends on a certain day should be shown as filling // that whole day on the Gantt chart. correctDate: function(arg) { var inner = function(date) { return d3.time.day.offset(date, 1); }; if (_.isFunction(arg)) return _.compose(inner, arg); else return inner(arg); } }); });
d3.csv("./src/stores/ndx.csv", function(error, data) { var dateFormat = d3.time.format("%m/%d/%Y"); data.forEach(function (d) { d.dd = dateFormat.parse(d.date); d.month = d3.time.month(d.dd).getMonth(); // pre-calculate month for better performance d.year = d3.time.year(d.dd).getFullYear(); d.close = +d.close; // coerce to number d.open = +d.open; }); var ndx = crossfilter(data), monthOfTheYearDimension = ndx.dimension(function(d) { return [+d.month, +d.year]; }), percentageGainByMonthOfYearGroup = monthOfTheYearDimension.group().reduce( /* callback for when data is added to the current filter results */ function (p, v) { ++p.count; p.absGain += v.close - v.open; p.fluctuation += Math.abs(v.close - v.open); p.sumIndex += (v.open + v.close) / 2; p.avgIndex = p.count ? p.sumIndex / p.count : 0; p.percentageGain = p.avgIndex ? (p.absGain / p.avgIndex) * 100 : 0; p.fluctuationPercentage = p.avgIndex ? (p.fluctuation / p.avgIndex) * 100 : 0; return p; }, /* callback for when data is removed from the current filter results */ function (p, v) { --p.count; p.absGain -= v.close - v.open; p.fluctuation -= Math.abs(v.close - v.open); p.sumIndex -= (v.open + v.close) / 2; p.avgIndex = p.count ? p.sumIndex / p.count : 0; p.percentageGain = p.avgIndex ? (p.absGain / p.avgIndex) * 100 : 0; p.fluctuationPercentage = p.avgIndex ? (p.fluctuation / p.avgIndex) * 100 : 0; return p; }, /* initialize p */ function () { return {count: 0, absGain: 0, fluctuation: 0, fluctuationPercentage: 0, sumIndex: 0, avgIndex: 0, percentageGain: 0}; } ); var heatColorMapping = function(d) { if (d < 0) { return d3.scale.linear().domain([-23,0]).range(["red", "#e5e5e5"])(d); } else { return d3.scale.linear().domain([0,23]).range(["#e5e5e5", "green"])(d); } }; heatColorMapping.domain = function() { return [-23,23]; }; heatmapChart .width(12 * 80 + 80) .height(27 * 10 + 40) .dimension(monthOfTheYearDimension) .group(percentageGainByMonthOfYearGroup) .keyAccessor(function(d) { return +d.key[0]; }) .valueAccessor(function(d) { return +d.key[1]; }) .colorAccessor(function(d) { return +d.value.percentageGain; }) .title(function(d) { return " Month: " + d.key[0] + "\n" + " Year: " + d.key[1] + "\n" + " Gain: " + d.value.percentageGain + "%";}) .colors(heatColorMapping) .calculateColorDomain(); heatmapChart.xBorderRadius(0); heatmapChart.yBorderRadius(0); heatmapChart.render(); var monthlyDimension = ndx.dimension(function (d) { return +d.month; }); var percentageGainByMonthArrayGroup = monthlyDimension.group().reduce( function(p,v) { var absGain = v.close - v.open; var percentageGain = v.open ? (absGain / v.open) * 100 : 0; return p + percentageGain; }, function(p,v) { var absGain = v.close - v.open; var percentageGain = v.open ? (absGain / v.open) * 100 : 0; return p - percentageGain; }, function() { return 0; } ); barChart .dimension(monthlyDimension) .group(percentageGainByMonthArrayGroup) .width(12 * 80 + 80) .height(480) .y(d3.scale.linear().domain([-10.0,100.0])) .x(d3.scale.linear().domain([-0.5,11.5])) .elasticY(true) .centerBar(true); barChart.render(); });
data = data.map(function(d) { var fff = d3.time.format(time_format); d[x_accessor] = fff.parse(d[x_accessor]); return d; });
This source code is available under agreement available at https://github.com/Talend/data-prep/blob/master/LICENSE You should have received a copy of the agreement along with this program; if not, write to Talend SA 9 rue Pages 92150 Suresnes, France ============================================================================*/ import d3 from 'd3'; const DATE_FORMAT = 'YYYY-MM-DD'; const D3_DATE_FORMAT = '%Y-%m-%d'; const D3_NUMBER_DECIMAL = ','; const d3DateFormatter = d3.time.format(D3_DATE_FORMAT); const d3NumberFormatter = d3.format(D3_NUMBER_DECIMAL); const numberFormatter = { format: value => d3NumberFormatter(value), }; const dateFormatter = { parse: string => d3DateFormatter.parse(string), format: timestamp => d3DateFormatter(new Date(timestamp)), }; /** * Checks max interval >= the max of data values and if the min interval < max interval */ function adaptSelection(selection) { return {
}, render() { return ( <svg className='stat-chart' ref='chart'></svg> ); }, _update() { var width = 960; var height = 500; var padding = 100; var svg = d3.select('svg') .attr('width', width) .attr('height', height) var x = d3.time.scale() .range([0, width-padding*2]); var y = d3.scale.linear() .range([height-padding*2, 0]); x.domain(d3.extent(this.props.data, (d) => d['last-updated'] * 1000 )); y.domain([0,d3.max(this.props.data, (d) => d.toughness)]); var line = d3.svg.line() .x((d) => x(d['last-updated'] * 1000)) .y((d) => y(d.damage)); var toughnessLine = d3.svg.line() .x((d) => x(d['last-updated'] * 1000)) .y((d) => y(d.toughness));
import d3 from 'd3'; const timeFormat = d3.time.format('%Y-%m-%dT%H:%M:%SZ'); let totalDistanceByMonth = {}; function parseDate(date) { return timeFormat.parse(date); } function generateTotalsData() { const data = Object.keys(totalDistanceByMonth).map(function(k) { return { month: totalDistanceByMonth[k].month, distance: totalDistanceByMonth[k].distance }; }); console.log('totalsData', data); return data.reverse(); } export default function(element) { var margin = {top: 50, right: 80, bottom: 100, left: 80}, width = (window.innerWidth * 0.9) - margin.left - margin.right, height = (window.innerHeight * 0.9) - margin.top - margin.bottom; var x = d3.time.scale() .range([0, width]); var y = d3.scale.linear() .range([height, 0]);
chart.xAxis.tickFormat(function(d) { return d3.time.format(dateFormat)(new Date(d)); });
export default function(element) { var margin = {top: 50, right: 80, bottom: 100, left: 80}, width = (window.innerWidth * 0.9) - margin.left - margin.right, height = (window.innerHeight * 0.9) - margin.top - margin.bottom; var x = d3.time.scale() .range([0, width]); var y = d3.scale.linear() .range([height, 0]); var xAxis = d3.svg.axis() .scale(x) .orient('bottom') .ticks(d3.time.months) .tickSize(16, 0) .tickFormat(d3.time.format('%B')); var yAxis = d3.svg.axis() .scale(y) .tickFormat((d) => {return (d / 1000) + ' km';}) .orient('left'); var svg = d3.select(element).append('svg') .attr('width', width + margin.left + margin.right) .attr('height', height + margin.top + margin.bottom) .append('g') .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); d3.json('activities.json', function(error, data) { if (error) throw error; data.forEach(function (d) { d.start_date = parseDate(d.start_date); d.month = d3.time.month(d.start_date); if (!totalDistanceByMonth[d.month]) { totalDistanceByMonth[d.month] = { month: d.month, distance: 0 }; } totalDistanceByMonth[d.month] = { month: d.month, distance: totalDistanceByMonth[d.month].distance + d.distance }; }); const totalsData = generateTotalsData(); const firstMonth = new Date(totalsData[0].month); const lastMonth = new Date(totalsData[totalsData.length - 1].month); const lastMonthPlusOne = new Date(lastMonth); lastMonthPlusOne.setMonth(lastMonth.getMonth() + 1); let highestMonthTotal = 0; totalsData.forEach(data => { highestMonthTotal = Math.max(highestMonthTotal, data.distance); }); x.domain([firstMonth, lastMonth]); y.domain([0, highestMonthTotal + 1000]); svg.append('g') .attr('class', 'x axis') .attr('transform', 'translate(0,' + height + ')') .call(xAxis); svg.append('g') .attr('class', 'y axis') .call(yAxis) .append('text') .attr('transform', 'rotate(-90)') .attr('y', 6) .attr('dy', '.71em') .style('text-anchor', 'end'); svg.selectAll(".bar") .data(totalsData) .enter().append("rect") .attr("class", "bar") .attr("x", function(d) { return x(d.month); }) .attr("width", width / (totalsData.length + 1)) .attr("y", function(d) { return y(d.distance); }) .attr("height", function(d) { return height - y(d.distance); }); }); }
// ,'235_50' // ,'236_50' // handy var ts_regex=/(\d*-\d*-\d*)\s(\d*:\d*)/ function parseDate(d) { var match = ts_regex.exec(d) var canonical_date = [match[1],'T',match[2],':00-0800'].join('') return new Date(canonical_date) } // Various formatters. var formatNumber = d3.format(",d"), formatChange = d3.format("+,d"), formatDate = d3.time.format("%B %d, %Y"), formatMonth = //d3.time.format("%B"), function(d){ return month_abbrev[d] }, formatDOW = //d3.time.format("%a"), function(d){ return dow[d] }, formatTime = d3.time.format("%I:%M %p"); var formatTOD = function(d){ if (d===0) return '12:00 AM' if (d<12) return d+':00 AM' if (d===12) return '12:00 PM'
/* global updateTime, forecast */ var d3 = require('d3'); d3.sankey = require('./sankey/d3plugin.js'); var debounce = require('lodash/function/debounce'); var parties = require('uk-political-parties'); var sankeyData = require('./sankey/sankey-data.js'); var logo = require('./sankey/logo.js'); var nodeWidth = 30; var updateString = ''; var timeFormat = d3.time.format("%B %e, %Y %I:%M %p"); updateString = timeFormat(updateTime); var data = sankeyData(forecast); var partyColour = parties.converter('colour','electionForecast'); var partyShortName = parties.converter('short','electionForecast'); var resize = debounce(function(e) { drawSankey(data); }, 200); window.addEventListener("resize", resize, false); drawSankey(data); d3.select('#svg-source').text( 'Source: electionforecast.co.uk, ' + updateString ); d3.selectAll('.chart-preset').on('click',function(){ clearSelections();
.key(function(d) { return d3.time.day(d.date); });
Utils.cloneMe = function(obj) { if(obj == null || typeof(obj) !== 'object') return obj; if (obj instanceof Date) { return new Date(obj.getTime()); } var temp = {};//obj.constructor(); // changed for(var key in obj) { if(obj.hasOwnProperty(key)) { temp[key] = Utils.cloneMe(obj[key]); } } return temp; } Utils.displayDateFormat = d3.time.format("%Y-%m-%d"); Utils.displayNumberFormat = function(x) { return Utils.numberWithCommas(x.toFixed(2)); }; Utils.numberWithCommas = function(x) { return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); }; Utils.isNumeric = function(n) { return !isNaN(parseFloat(n)) && isFinite(n); }; Utils.mousePosition = function(e) { var container = e.currentTarget, rect = container.getBoundingClientRect(), x = e.clientX - rect.left - container.clientLeft, y = e.clientY - rect.top - container.clientTop, xy = [ Math.round(x), Math.round(y) ];
setData: function (data) { var startDate, endDate, date, dayFilter, parse; // Does the loading indicator needs to be displayed? if (data.loading) { if (!this._data.loading) { this._data = data; this.emitChangeData(); } // Do nothing when loading is in progress return; } // Store errors for later usage if (data.error) { requestErrors.push(data); } else { parse = d3.time.format("%Y-%m-%d %H:%M").parse; // Date formatting parser startDate = DateUtils.parseDate(this.getRestFilter(START_DATE_FILTER)); endDate = DateUtils.parseDate(this.getRestFilter(END_DATE_FILTER)); date = DateUtils.parseDate(this.getRestFilter(CURRENT_DATE_FILTER)); if (date.getFullYear() == startDate.getFullYear() && date.getMonth() == startDate.getMonth()) { dayFilter = startDate.getDate(); data.data = data.data.filter(function (row) { return DateUtils.parseDate(row.date, true).getDate() >= dayFilter }); } if (date.getFullYear() == endDate.getFullYear() && date.getMonth() == endDate.getMonth()) { dayFilter = endDate.getDate(); data.data = data.data.filter(function (row) { return DateUtils.parseDate(row.date, true).getDate() <= dayFilter }); } // Parse dates and numbers. data.data.forEach(function (d) { d.date = parse(d.date); d.flows = +d.flows; }); // Sort the data by date ASC data.data.sort(function (a, b) { return a.date - b.date; }); this._data.data.push(data.data); } this._data.loading = requestQueue.length > 0; if (!this._data.loading) { if (this._data.data.length==0) { // Broadcast first found error this._data = requestErrors[0]; } this.emitChangeData(); } else { setTimeout(this.dequeue.bind(this), 1); } }
function timeScale(model, options, orientation) { var range = getRange(model, orientation); return d3.time.scale() .domain(model.independentDomain) .range(range); }