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 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); } }