diff --git a/web/src/pages/billing/BillingDashboard.tsx b/web/src/pages/billing/BillingDashboard.tsx index 3970325c1..bede0d1da 100644 --- a/web/src/pages/billing/BillingDashboard.tsx +++ b/web/src/pages/billing/BillingDashboard.tsx @@ -7,8 +7,38 @@ const BillingDashboard: React.FunctionComponent = () => { const [end, setEnd] = React.useState('2022-10-31') return ( - -

Billing dashboard

+ +
+
+ Billing Dashboard +
+
+
+ Breakdown of proportional allocation of costs per project over time +
+
) diff --git a/web/src/pages/billing/SeqrProportionalMapGraph.tsx b/web/src/pages/billing/SeqrProportionalMapGraph.tsx index a7c8908bf..6ddb0b1d9 100644 --- a/web/src/pages/billing/SeqrProportionalMapGraph.tsx +++ b/web/src/pages/billing/SeqrProportionalMapGraph.tsx @@ -1,21 +1,19 @@ import * as React from 'react' import _ from 'lodash' -import { Container, Form, Message } from 'semantic-ui-react' - import { scaleLinear, extent, - scaleOrdinal, stack, - csv, area, stackOffsetExpand, - schemeAccent, scaleTime, utcDay, utcMonth, - timeFormat, + select, + pointer, + interpolateRainbow, + selectAll, } from 'd3' import LoadingDucks from '../../shared/components/LoadingDucks/LoadingDucks' @@ -46,6 +44,8 @@ const SeqrProportionalMapGraph: React.FunctionComponent { + const tooltipRef = React.useRef() + const [hovered, setHovered] = React.useState('') const [isLoading, setIsLoading] = React.useState(true) const [error, setError] = React.useState() const [showProjectSelector, setShowProjectSelector] = React.useState(false) @@ -133,7 +133,39 @@ const SeqrProportionalMapGraph: React.FunctionComponent { setProjectSelections( - ['project1', 'project2', 'project3'].reduce( + [ + 'acute-care', + 'seqr', + 'perth-neuro', + 'rdnow', + 'ravenscroft-rdstudy', + 'heartkids', + 'circa', + 'hereditary-neuro', + 'schr-neuro', + 'ravenscroft-arch', + 'leukodystrophies', + 'brain-malf', + 'mito-disease', + 'epileptic-enceph', + 'ohmr3-mendelian', + 'ohmr4-epilepsy', + 'validation', + 'flinders-ophthal', + 'ibmdx', + 'mcri-lrp', + 'mcri-lrp-test', + 'kidgen', + 'ag-hidden', + 'udn-aus', + 'udn-aus-training', + 'rdp-kidney', + 'broad-rgp', + 'genomic-autopsy', + 'mito-mdt', + 'ag-cardiac', + 'ag-very-hidden', + ].reduce( (prev: { [project: string]: boolean }, project: string) => ({ ...prev, [project]: true, @@ -142,38 +174,996 @@ const SeqrProportionalMapGraph: React.FunctionComponent ({ ...entry, date: new Date(entry.date) })) + ) }, []) if (!data) { @@ -185,9 +1175,11 @@ const SeqrProportionalMapGraph: React.FunctionComponent d.date)) // date is a string, will this take a date object? Yes :) .range([0, width - margin.left - margin.right]) - // .tickFormat(timeFormat('%B %d, %Y')) // function for generating the y Axis // no domain needed as it defaults to [0, 1] which is appropriate for proportions @@ -208,7 +1199,7 @@ const SeqrProportionalMapGraph: React.FunctionComponent 1000 * 60 * 60 * 24 * 90) { + if (new Date(end).valueOf() - new Date(start).valueOf() > 1000 * 60 * 60 * 24 * 90) { interval = utcMonth.every(1) } + const mouseover = ( + event: React.MouseEvent, + prevProp: number, + newProp: number, + project: string + ) => { + const tooltipDiv = tooltipRef.current + const pos = pointer(event) + if (tooltipDiv) { + select(tooltipDiv).transition().duration(200).style('opacity', 0.9) + select(tooltipDiv) + .html( + `

${project}

${(prevProp * 100).toFixed(1)}% → ${( + newProp * 100 + ).toFixed(1)}%
+ ` + ) + .style('left', `${pos[0] + 95}px`) + .style('top', `${pos[1] + 100}px`) + } + } + + const mouseout = () => { + const tooltipDiv = tooltipRef.current + if (tooltipDiv) { + select(tooltipDiv).transition().duration(500).style('opacity', 0) + } + } + return ( data && ( - - {/* transform and translate move the relative (0,0) so you can draw accurately. Consider svg as a cartesian plane with (0, 0) top left and positive directions left and down the page + <> +
+ + {/* transform and translate move the relative (0,0) so you can draw accurately. Consider svg as a cartesian plane with (0, 0) top left and positive directions left and down the page then to draw in svg you just need to give coordinates. We've specified the width and height above so this svg 'canvas' can be drawn on anywhere between pixel 0 and the max height and width pixels */} - - {/* x-axis */} - - {/* draws the main x axis line */} - - {/* draws the little ticks marks off the x axis + labels + + {/* x-axis */} + + {/* draws the little ticks marks off the x axis + labels xScale.ticks() generates a list of evenly spaces ticks from min to max domain You can pass an argument to ticks() to specify number of ticks to generate Calling xScale(tick) turns a tick value into a pixel position to be drawn eg in the domain [2000, 2010] and range[0, 200] passing 2005 would be 50% of the way across the domain so 50% of the way between min and max specified pixel positions so it would draw at 100 */} - {xScale.ticks(interval).map( - tick => ( + {xScale.ticks(interval).map((tick) => ( - {tick.toDateString()} + {`${tick.toLocaleString('en-us', { + month: 'short', + year: 'numeric', + })}`} {/* change this for different date formats */} - {' '} + {/* this is the tiny vertical tick line that getting drawn (6 pixels tall) */} - ) - )} - + ))} + - {/* y-axis (same as above) */} - - - {yScale.ticks().map((tick) => ( - - - {tick} - - - - - ))} - + {/* y-axis (same as above) */} + + {yScale.ticks().map((tick) => ( + + + {tick * 100}% + + + + ))} + + + {/* stacked areas */} + + {/* for each 'project', draws a path (using path function) and fills it a new colour (using colour function) */} + {stackedData.map((area, i) => ( + + {area.map((region, j) => { + // don't draw an extra area at the end + if (j + 1 >= area.length) { + return ( + + ) + } + const areas = area.slice(j, j + 2) + // don't draw empty areas + if ( + areas[0][1] - areas[0][0] === 0 && + areas[1][1] - areas[1][0] === 0 + ) { + return ( + + ) + } + + const colour = interpolateRainbow( + i / selectedProjects.length + ) + // const colour = colors[i] + return ( + { + select(e.target).style('opacity', 0.6) + }} + onMouseMove={(e) => + mouseover( + e, + areas[0][1] - areas[0][0], + areas[1][1] - areas[1][0], + selectedProjects[i] + ) + } + onMouseLeave={(e) => { + select(e.target).style('opacity', 1) + mouseout() + }} + /> + ) + })} + + ))} + + {stackedData.map((area, i) => { + const projectStart = area.findIndex((p) => p[1] - p[0]) + if (projectStart === -1) { + return + } + return ( + + ) + })} + + + {/* draws the main x axis line */} + + + {/* draws the main y axis line */} + - {/* stacked areas */} - - {/* for each 'project', draws a path (using path function) and fills it a new colour (using colour function) */} - {stackedData.map((area, i) => ( - + {/* x-axis label */} + + + {'Date'} + + + + {/* y-axis label */} + + + {'Proportion'} + + + + + Projects + {selectedProjects.map((project, i) => ( + + { + setHovered(project) + }} + onMouseLeave={() => { + setHovered('') + }} + /> + + {project} + + ))} - - + + ) ) }