summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-ui/src/components
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2022-09-15 22:52:00 +0200
committerFabian Mastenbroek <mail.fabianm@gmail.com>2022-09-20 16:07:06 +0200
commit98bc4c3e9458aea98890b770493f14327a7bc7c4 (patch)
tree201d23874b21a9e9006e5a9e4d8edacd8868acc5 /opendc-web/opendc-web-ui/src/components
parent7199e2c15838d78fedd3c6127beddf1656dbeae2 (diff)
refactor(web/ui): Use PatternFly Charts for plots
This change updates the OpenDC web interface to use the PatternFly Charts package to render the results of a portfolio. Previously, we used Recharts, but this package does not support SSR, whereas the PatternFly Charts package matches our design framework.
Diffstat (limited to 'opendc-web/opendc-web-ui/src/components')
-rw-r--r--opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js132
1 files changed, 74 insertions, 58 deletions
diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js
index 33604896..f50105ed 100644
--- a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js
+++ b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js
@@ -20,12 +20,11 @@
* SOFTWARE.
*/
+import { mean, std } from 'mathjs'
import React, { useMemo } from 'react'
import PropTypes from 'prop-types'
-import { Bar, CartesianGrid, ComposedChart, ErrorBar, ResponsiveContainer, Scatter, XAxis, YAxis } from 'recharts'
-import { METRIC_NAMES, METRIC_UNITS } from '../../util/available-metrics'
-import { mean, std } from 'mathjs'
-import approx from 'approximate-number'
+import { VictoryErrorBar } from 'victory-errorbar'
+import { METRIC_NAMES, METRIC_UNITS, AVAILABLE_METRICS } from '../../util/available-metrics'
import {
Bullseye,
Card,
@@ -41,6 +40,7 @@ import {
Spinner,
Title,
} from '@patternfly/react-core'
+import { Chart, ChartAxis, ChartBar, ChartTooltip } from '@patternfly/react-charts'
import { ErrorCircleOIcon, CubesIcon } from '@patternfly/react-icons'
import { usePortfolio } from '../../data/project'
import PortfolioResultInfo from './PortfolioResultInfo'
@@ -48,7 +48,28 @@ import NewScenario from './NewScenario'
function PortfolioResults({ projectId, portfolioId }) {
const { status, data: portfolio } = usePortfolio(projectId, portfolioId)
- const scenarios = portfolio?.scenarios ?? []
+ const scenarios = useMemo(() => portfolio?.scenarios ?? [], [portfolio])
+
+ const label = ({ datum }) =>
+ `${datum.x}: ${datum.y.toLocaleString()} ± ${datum.errorY.toLocaleString()} ${METRIC_UNITS[datum.metric]}`
+ const selectedMetrics = new Set(portfolio?.targets?.metrics ?? [])
+ const dataPerMetric = useMemo(() => {
+ const dataPerMetric = {}
+ AVAILABLE_METRICS.forEach((metric) => {
+ dataPerMetric[metric] = scenarios
+ .filter((scenario) => scenario.job?.results)
+ .map((scenario) => ({
+ metric,
+ x: scenario.name,
+ y: mean(scenario.job.results[metric]),
+ errorY: std(scenario.job.results[metric]),
+ label,
+ }))
+ })
+ return dataPerMetric
+ }, [scenarios])
+
+ const categories = useMemo(() => ({ x: scenarios.map((s) => s.name).reverse() }), [scenarios])
if (status === 'loading') {
return (
@@ -93,62 +114,57 @@ function PortfolioResults({ projectId, portfolioId }) {
)
}
- const metrics = portfolio?.targets?.metrics ?? []
- const dataPerMetric = useMemo(() => {
- const dataPerMetric = {}
- metrics.forEach((metric) => {
- dataPerMetric[metric] = scenarios
- .filter((scenario) => scenario.job?.results)
- .map((scenario) => ({
- name: scenario.name,
- value: mean(scenario.job.results[metric]),
- errorX: std(scenario.job.results[metric]),
- }))
- })
- return dataPerMetric
- }, [scenarios, metrics])
-
return (
<Grid hasGutter>
- {metrics.map((metric) => (
- <GridItem xl={6} lg={12} key={metric}>
- <Card>
- <CardHeader>
- <CardActions>
- <PortfolioResultInfo metric={metric} />
- </CardActions>
- <CardTitle>{METRIC_NAMES[metric]}</CardTitle>
- </CardHeader>
- <CardBody>
- <ResponsiveContainer aspect={16 / 9} width="100%">
- <ComposedChart
- data={dataPerMetric[metric]}
- margin={{ left: 35, bottom: 15 }}
- layout="vertical"
- >
- <CartesianGrid strokeDasharray="3 3" />
- <XAxis
- tickFormatter={(tick) => approx(tick)}
- label={{ value: METRIC_UNITS[metric], position: 'bottom', offset: 0 }}
- type="number"
- />
- <YAxis dataKey="name" type="category" />
- <Bar dataKey="value" fill="#3399FF" isAnimationActive={false} />
- <Scatter dataKey="value" opacity={0} isAnimationActive={false}>
- <ErrorBar
- dataKey="errorX"
- width={10}
- strokeWidth={3}
- stroke="#FF6600"
- direction="x"
+ {AVAILABLE_METRICS.map(
+ (metric) =>
+ selectedMetrics.has(metric) && (
+ <GridItem xl={6} lg={12} key={metric}>
+ <Card>
+ <CardHeader>
+ <CardActions>
+ <PortfolioResultInfo metric={metric} />
+ </CardActions>
+ <CardTitle>{METRIC_NAMES[metric]}</CardTitle>
+ </CardHeader>
+ <CardBody>
+ <Chart
+ width={650}
+ height={250}
+ padding={{
+ top: 10,
+ bottom: 60,
+ left: 130,
+ }}
+ domainPadding={25}
+ >
+ <ChartAxis />
+ <ChartAxis
+ dependentAxis
+ showGrid
+ label={METRIC_UNITS[metric]}
+ fixLabelOverlap
+ />
+ <ChartBar
+ categories={categories}
+ data={dataPerMetric[metric]}
+ labelComponent={<ChartTooltip constrainToVisibleArea />}
+ barWidth={25}
+ horizontal
+ />
+ <VictoryErrorBar
+ categories={categories}
+ data={dataPerMetric[metric]}
+ errorY={(d) => d.errorY}
+ labelComponent={<></>}
+ horizontal
/>
- </Scatter>
- </ComposedChart>
- </ResponsiveContainer>
- </CardBody>
- </Card>
- </GridItem>
- ))}
+ </Chart>
+ </CardBody>
+ </Card>
+ </GridItem>
+ )
+ )}
</Grid>
)
}