diff --git a/web/angular.json b/web/angular.json index cc98948..2962eb5 100644 --- a/web/angular.json +++ b/web/angular.json @@ -135,4 +135,4 @@ "cli": { "analytics": "bf853fd1-38d6-4df6-a388-35a46c9a7ff1" } -} +} \ No newline at end of file diff --git a/web/src/app/app.module.ts b/web/src/app/app.module.ts index f4ecc02..8c8675d 100644 --- a/web/src/app/app.module.ts +++ b/web/src/app/app.module.ts @@ -1,19 +1,3 @@ -/** - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - import {HttpClientModule} from '@angular/common/http'; import {NgModule} from '@angular/core'; import {FormsModule} from '@angular/forms'; @@ -27,8 +11,8 @@ import {BarChartComponent} from './components/bar-chart/bar-chart.component'; import {ButtonRowComponent} from './components/button-row/button-row.component'; import {ButtonComponent} from './components/button/button.component'; import {CDFComponent} from './components/cdf/cdf.component'; -import {PageNameComponent} from './components/page-name/page-name.component'; import {TimelineComponent} from './components/timeline/timeline.component'; +import {PageNameComponent} from './components/page-name/page-name.component'; import {AllPushesComponent} from './pages/all-pushes/all-pushes.component'; import {MyPushesComponent} from './pages/my-pushes/my-pushes.component'; import {OnePushComponent} from './pages/one-push/one-push.component'; diff --git a/web/src/app/components/cdf/cdf.component.ts b/web/src/app/components/cdf/cdf.component.ts index e58b11e..4c7c84d 100644 --- a/web/src/app/components/cdf/cdf.component.ts +++ b/web/src/app/components/cdf/cdf.component.ts @@ -29,7 +29,24 @@ import {COMPLETED_BLUE, d3SVG, Item, STROKE_COLOR} from './cdf.utils'; styleUrls: ['./cdf.component.scss'] }) -export class CDFComponent implements AfterViewChecked, AfterViewInit { +export class CDFComponent implements AfterViewInit { + private static readonly NANO_TO_MINUTES: number = (10 ** 9) * 60; + private static readonly STATE_TO_COLOR: {[index: number]: string} = { + 1: '#eee', + 3: '#ba68c8', + 4: '#ff6e40', + 5: '#00bfa5', + 6: '#ff6e40', + 7: '#ba68c8', + 8: '#ba68c8', + 9: '#ff6e40', + 10: '#ba68c8', + 12: '#ff6e40', + 13: '#ba68c8', + 15: '#ba68c8', + 16: '#ff6e40' + }; + @ViewChild('cdf') private CDFContainer!: ElementRef; @Input() pushInfos!: step189_2020.IPushInfo[]|null; @Input() currentPush!: step189_2020.IPushInfo|null; @@ -40,20 +57,6 @@ export class CDFComponent implements AfterViewChecked, AfterViewInit { private durationUnit = ''; private showDotsBoolean = false; - ngAfterViewChecked(): void { - if (this.showDotsBoolean === this.showDots) { - return; - } - if (!this.svg) { - return; - } - this.showDotsBoolean = this.showDots; - if (!this.showDotsBoolean) { - this.svg.select('#cdf-chart').selectAll('.dots').attr('opacity', 0); - } else { - this.svg.select('#cdf-chart').selectAll('.dots').attr('opacity', 1); - } - } /** * Creates a CDF chart by plotting the duration of completed pushes against * the probability of a push taking less time than that duration. Adds lines @@ -111,7 +114,7 @@ export class CDFComponent implements AfterViewChecked, AfterViewInit { } this.showDotsBoolean = this.showDots; this.durationUnit = findDurationUnit(this.pushInfos); - this.data = populateData(this.pushInfos); + this.data = populateData(this.pushInfos, false); const element = this.CDFContainer.nativeElement; const elementWidth = element.clientWidth; @@ -145,7 +148,6 @@ export class CDFComponent implements AfterViewChecked, AfterViewInit { probability: 1, endState: 5 }); - const maxExtendedDuration = d3.max(extendedData, d => d.duration); if (!maxExtendedDuration) { return; @@ -208,326 +210,19 @@ export class CDFComponent implements AfterViewChecked, AfterViewInit { .attr('y', margin.top / 2) .attr('text-anchor', 'middle') .style('font-size', '16px') - .text('CDF of completed push durations'); - - cdfChart.append('path') - .attr('id', 'cdf-area') - .attr('fill', COMPLETED_BLUE) - .attr( - 'd', - d3.area() - .x(d => xScale(d.duration)) - .y1(d => yScale(d.probability)) - .y0(yScale(0)) - .curve(d3.curveStepAfter)); - - cdfChart.append('path') - .attr('fill', 'none') - .attr( - 'd', - d3.line() - .x(d => xScale(d.duration)) - .y(d => yScale(d.probability)) - .curve(d3.curveStepAfter)) - .attr('id', 'cdf-stroke') - .attr('stroke', STROKE_COLOR) - .attr('stroke-width', 2); - - const percentileLines = - this.svg.append('g') - .attr('id', 'percentile-lines') - .attr('transform', `translate(${margin.left}, ${margin.top})`); - - const percentiles = generateQuantiles(this.data, [10, 50, 90], xScale); - - percentileLines.selectAll('.percentile-lines') - .data(percentiles) - .enter() - .append('line') - .attr('class', 'percentile-line') - .attr('stroke', 'lightgrey') - .attr('stroke-dasharray', '5,2') - .attr('x1', (d: Item) => xScale(d.duration)) - .attr('y1', height) - .attr('x2', (d: Item) => xScale(d.duration)) - .attr('y2', 0); - - percentileLines.selectAll('.percentile-lines') - .data(percentiles) - .enter() - .append('text') - .attr('text-anchor', 'middle') - .attr('x', (d: Item) => xScale(d.duration)) - .attr('y', -10) - .attr('class', 'percentile-text') - .attr('font-size', '10px') - .text((d: Item) => `${d.probability}%`); - - let radius = 2.5; - const xVals = this.data.map(d => d.duration); - let yPosition = generateYPosition(radius * 2 + 0.1, xScale, xVals); - - const maxYPosition = d3.max(yPosition); - if (!maxYPosition) { - return; - } - - if (maxYPosition > height) { - radius = 1.4; - yPosition = generateYPosition(radius * 2 + 0.1, xScale, xVals); + .text('CDF of push durations'); + + const rectHeight = Math.floor(height / this.data.length) + 1; + for (let i = this.data.length - 1; i >= 0; i--) { + const elem = this.data[i]; + cdfChart.append('rect') + .attr('class', 'rect-area') + .attr('fill', CDFComponent.STATE_TO_COLOR[elem.endState]) + .attr('x', xScale(elem.duration)) + .attr('y', yScale(elem.probability)) + .attr('height', rectHeight) + .attr('width', width - xScale(elem.duration)) + .attr('opacity', 1); } - - const currentPushLine = - this.svg.append('g') - .attr('id', 'current-push-line') - .attr('transform', `translate(${margin.left}, ${margin.top})`); - - addCurrentPushLine( - this.durationUnit, this.currentPush, currentPushLine, this.data, height, - xScale, yScale); - - // Sets up and handles mouse click. The vertical and horizontal lines and - // the percentages are placed on the graph where the mouse clicked. The area - // of the graph to the left of the click becomes a lighter color and the - // dots to the left of the click become grey. - let startValue = minDuration + 1e-6; - - const clipRect = cdfChart.append('clipPath') - .attr('id', 'area-clip') - .append('rect') - .attr('class', 'area-clip-rect') - .attr('x', 0) - .attr('y', 0) - .attr('width', xScale(startValue)) - .attr('height', height); - - cdfChart.append('path') - .attr('class', 'cdf-clipped') - .attr( - 'd', - d3.area() - .x(d => xScale(d.duration)) - .y1(d => yScale(d.probability)) - .y0(yScale(0)) - .curve(d3.curveStepAfter)) - .attr('fill-opacity', '0.6') - .attr('fill', 'white') - .attr('clip-path', 'url(#area-clip)'); - - for (let i = 0; i < this.data.length; i++) { - const cx = xScale(this.data[i].duration); - const cy = height - yPosition[i] - radius; - cdfChart.append('circle') - .attr('class', 'dots') - .attr('cx', cx) - .attr('r', radius) - .attr('cy', cy) - .attr('fill', 'black'); - } - - const lineY = - cdfChart.append('line') - .attr('class', 'click-line-y') - .attr('x1', xScale(startValue)) - .attr('x2', xScale(startValue)) - .attr( - 'y1', yScale(getProbabilityForDuration(this.data, startValue))) - .attr('y2', height) - .attr('stroke', 'black') - .attr('stroke-width', '1px') - .attr('opacity', 0); - - const lineX = - cdfChart.append('line') - .attr('class', 'click-line-x') - .attr('x1', 0) - .attr('x2', xScale(startValue)) - .attr( - 'y1', yScale(getProbabilityForDuration(this.data, startValue))) - .attr( - 'y2', yScale(getProbabilityForDuration(this.data, startValue))) - .attr('stroke', 'black') - .attr('stroke-width', '1px') - .attr('opacity', 0); - - cdfChart.append('text') - .attr('x', xScale(startValue) - 35) - .attr( - 'y', - yScale(getProbabilityForDuration(this.data, startValue) / 100) + 30) - .attr('class', 'click-line-y-text') - .attr('font-size', '10px') - .text(`${this.data.filter(c => c.duration <= startValue).length}/${ - this.data.length}`) - .attr('opacity', 0); - - cdfChart.append('text') - .attr('x', xScale(startValue) / 2) - .attr( - 'y', - yScale(getProbabilityForDuration(this.data, startValue) / 100) + 10) - .attr('class', 'click-line-x-text') - .attr('font-size', '10px') - .text(`${getProbabilityForDuration(this.data, startValue).toFixed(2)}%`) - .attr('opacity', 0); - - cdfChart.on('click', (d: Item[], i: number): void => { - if (!d) { - return; - } - const coordinates = d3.mouse(d3.event.currentTarget); - const xValue = xScale.invert(coordinates[0]); - if (xValue > minDuration) { - startValue = xValue; - const yValue = getProbabilityForDuration(d, startValue); - d3.select(d3.event.currentTarget) - .select('.area-clip-rect') - .attr('width', xScale(startValue)); - d3.select(d3.event.currentTarget) - .select('.click-line-y') - .attr('y1', yScale(yValue)) - .attr('x1', xScale(startValue)) - .attr('x2', xScale(startValue)) - .attr('opacity', 1); - d3.select(d3.event.currentTarget) - .select('.click-line-x') - .attr('y1', yScale(yValue)) - .attr('y2', yScale(yValue)) - .attr('x2', xScale(startValue)) - .attr('opacity', 1); - d3.select(d3.event.currentTarget) - .select('.click-line-y-text') - .attr('x', xScale(startValue) - 40) - .attr('y', yScale(yValue) + 20) - .text(`${this.data.filter(c => c.duration <= startValue).length}/${ - this.data.length}`) - .attr('opacity', 1); - d3.select(d3.event.currentTarget) - .select('.click-line-x-text') - .attr('x', xScale(startValue) / 2) - .attr('y', yScale(yValue) + 10) - .text(`${ - (getProbabilityForDuration(this.data, startValue)) - .toFixed(1)}%`) - .attr('opacity', 1); - d3.select(d3.event.currentTarget) - .selectAll('.dots') - .data(this.data) - .attr( - 'fill', - (dp: Item) => dp.duration <= startValue ? 'grey' : 'black'); - } - }); - - // Sets up and handles mouse hovering. The vertical and horizontal rulers, x - // and y labels and backgrounds are placed in the correct position when the - // mouse is over the graph - const highlightColor = '#b9edc4'; - const strokeColor = '#167364'; - const circleColor = '#a0aade'; - const labelWidth = 40; - const labelHeight = 25; - const labelFontSize = labelHeight / 2; - - cdfChart.append('rect') - .attr('class', 'hover x-label-bg') - .attr('x', 0) - .attr('y', height) - .attr('height', labelHeight) - .attr('width', labelWidth) - .attr('fill', highlightColor) - .attr('opacity', 0); - - cdfChart.append('text') - .attr('text-anchor', 'middle') - .attr('class', 'hover x-label') - .attr('x', 0) - .attr('y', height + 5 + labelFontSize) - .attr('opacity', 0) - .style('font-size', `${labelFontSize}px`) - .style('font-weight', 'bold') - .attr('fill', 'black'); - - cdfChart.append('rect') - .attr('class', 'hover y-label-bg') - .attr('x', -labelWidth) - .attr('y', 0) - .attr('height', labelHeight) - .attr('width', labelWidth) - .attr('fill', highlightColor) - .attr('opacity', 0); - - cdfChart.append('text') - .attr('text-anchor', 'middle') - .attr('class', 'hover y-label') - .attr('x', -labelWidth / 2) - .attr('y', 0) - .attr('opacity', 0) - .style('font-weight', 'bold') - .attr('fill', 'black') - .style('font-size', `${labelFontSize}px`); - - cdfChart.append('line') - .attr('x1', 0) - .attr('x2', 0) - .attr('y1', height) - .attr('y2', 0) - .attr('class', 'hover v-ruler') - .attr('stroke', 'grey') - .attr('stroke-width', 2) - .attr('stroke-dasharray', '5,2') - .attr('opacity', 0); - - cdfChart.append('line') - .attr('x1', 0) - .attr('x2', width) - .attr('y1', 0) - .attr('y2', 0) - .attr('class', 'hover h-ruler') - .attr('stroke', 'grey') - .attr('stroke-width', 2) - .attr('stroke-dasharray', '5,2') - .attr('opacity', 0); - - cdfChart.append('circle') - .attr('class', 'hover marker') - .attr('cx', 0) - .attr('cy', 0) - .attr('r', 5) - .attr('stroke', strokeColor) - .attr('stroke-width', 2) - .attr('fill', circleColor) - .style('opacity', 0); - - cdfChart.on('mousemove', (d: Item[], i: number): void => { - const mouseX = xScale.invert(d3.mouse(d3.event.currentTarget)[0]); - - if (mouseX >= minDuration && mouseX <= maxExtendedDuration) { - const xVal = d3.mouse(d3.event.currentTarget)[0]; - const xInverted = xScale.invert(xVal); - const yVal = yScale(getProbabilityForDuration(d, xInverted)); - - d3.select('.marker').attr('cx', xVal).attr('cy', yVal); - d3.select('.h-ruler').attr('y1', yVal).attr('y2', yVal); - d3.select('.v-ruler').attr('x1', xVal).attr('x2', xVal); - - const xText = d3.format(',.1f')(xInverted); - const yText = d3.format(',.0f')(yScale.invert(yVal)); - d3.select('.x-label').attr('x', xVal).text(xText); - d3.select('.x-label-bg').attr('x', xVal - labelWidth / 2); - d3.select('.y-label') - .attr('y', yVal + labelHeight / 5) - .text(`${yText}%`); - d3.select('.y-label-bg').attr('y', yVal - labelHeight / 2); - - d3.selectAll('.hover').style('opacity', 1); - } else { - d3.selectAll('.hover').style('opacity', 0); - } - }); - - cdfChart.on('mouseleave', () => { - d3.selectAll('.hover').style('opacity', 0); - }); } } diff --git a/web/src/app/components/cdf/cdf.utils.ts b/web/src/app/components/cdf/cdf.utils.ts index a05309d..9db133e 100644 --- a/web/src/app/components/cdf/cdf.utils.ts +++ b/web/src/app/components/cdf/cdf.utils.ts @@ -52,9 +52,10 @@ export const STROKE_COLOR = '#167364'; * @param pushInfos Array of pushes for a single push def * @return Array of Items sorted by increasing duration */ -export function populateData(pushInfos: step189_2020.IPushInfo[]): Item[] { +export function populateData( + pushInfos: step189_2020.IPushInfo[], completedBool: boolean): Item[] { const divisor = UNIT_CONVERSION[findDurationUnit(pushInfos)]; - const pushes: Item[] = []; + let pushes: Item[] = []; pushInfos.forEach(pushInfo => { if (!pushInfo) { return; @@ -81,9 +82,11 @@ export function populateData(pushInfos: step189_2020.IPushInfo[]): Item[] { } as Item); } }); - const completed = pushes.filter(d => d.endState === 5); + if (completedBool) { + pushes = pushes.filter(d => d.endState === 5); + } const sortedArray: Item[] = - completed.sort((n1, n2) => n1.duration - n2.duration); + pushes.sort((n1, n2) => n1.duration - n2.duration); const data: Item[] = []; const durationLength = sortedArray.length; diff --git a/web/src/app/components/colors.ts b/web/src/app/components/colors.ts index f02c44b..ad2125a 100644 --- a/web/src/app/components/colors.ts +++ b/web/src/app/components/colors.ts @@ -49,3 +49,4 @@ export const STATE_TO_COLOR: {[index: number]: string} = { 25: ORANGE, 29: RED }; + diff --git a/web/src/app/components/duration-utils.ts b/web/src/app/components/duration-utils.ts index 6080334..2a58447 100644 --- a/web/src/app/components/duration-utils.ts +++ b/web/src/app/components/duration-utils.ts @@ -21,6 +21,7 @@ export interface DurationItem { endNsec: number|Long; // nsec time of the last state } + const NANO_TO_SECONDS: number = (10 ** 9); const NANO_TO_MINUTES: number = (10 ** 9) * 60; const NANO_TO_HOURS: number = (10 ** 9) * 60 * 60; diff --git a/web/src/app/pages/one-push/one-push.component.html b/web/src/app/pages/one-push/one-push.component.html index 8458031..13ea3f5 100644 --- a/web/src/app/pages/one-push/one-push.component.html +++ b/web/src/app/pages/one-push/one-push.component.html @@ -13,7 +13,6 @@

-Show dots + diff --git a/web/src/app/pages/one-push/one-push.component.ts b/web/src/app/pages/one-push/one-push.component.ts index 59dfcd4..3ef6ed4 100644 --- a/web/src/app/pages/one-push/one-push.component.ts +++ b/web/src/app/pages/one-push/one-push.component.ts @@ -22,6 +22,7 @@ import {flatMap, map, shareReplay} from 'rxjs/operators'; import {step189_2020} from '../../../proto/step189_2020'; + @Component({ selector: 'app-one-push', templateUrl: './one-push.component.html', diff --git a/web/src/index.html b/web/src/index.html index f9a2cd5..83cc1da 100644 --- a/web/src/index.html +++ b/web/src/index.html @@ -9,7 +9,7 @@ - + diff --git a/web/src/styles.scss b/web/src/styles.scss index 11baa2f..1ba3e02 100644 --- a/web/src/styles.scss +++ b/web/src/styles.scss @@ -27,3 +27,6 @@ a { .hover-cursor-default:hover { cursor: default; } + +html, body { height: 100%; } +body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }