forked from electricitymaps/electricitymaps-contrib
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Bar Breakdown Chart (electricitymaps#65)
Co-authored-by: Markus Killendahl <[email protected]>
- Loading branch information
1 parent
2fdd169
commit fe330cc
Showing
19 changed files
with
1,625 additions
and
178 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
75 changes: 75 additions & 0 deletions
75
web/src/features/charts/bar-breakdown/BarBreakdownChart.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { useAtom } from 'jotai'; | ||
import { PulseLoader } from 'react-spinners'; | ||
import { useTranslation } from 'translation/translation'; | ||
import { TimeAverages } from 'utils/constants'; | ||
import { displayByEmissionsAtom } from 'utils/state/atoms'; | ||
import { useRefWidthHeightObserver } from 'utils/viewport'; | ||
import useBarBreakdownChartData from '../hooks/useBarBreakdownProductionChartData'; | ||
import BarBreakdownEmissionsChart from './BarBreakdownEmissionsChart'; | ||
import BarBreakdownProductionChart from './BarBreakdownProductionChart'; | ||
|
||
function BarBreakdownChart({ timeAverage }: { timeAverage: TimeAverages }) { | ||
const { data, productionData, exchangeData, isLoading, height } = | ||
useBarBreakdownChartData(); | ||
const [displayByEmissions] = useAtom(displayByEmissionsAtom); | ||
const { ref, width } = useRefWidthHeightObserver(); | ||
const { __ } = useTranslation(); | ||
|
||
if (isLoading || !data) { | ||
// TODO: Replace with skeleton graph (maybe full graph with no data?) | ||
return <PulseLoader />; | ||
} | ||
|
||
// TODO: Show CountryTableOverlayIfNoData when required | ||
|
||
const todoHandler = () => { | ||
console.warn('TODO: Handle tooltips'); | ||
// see countrytable.jsx | ||
// handleProductionRowMouseOver | ||
//handleProductionRowMouseOut | ||
//handleExchangeRowMouseOver | ||
//handleExchangeRowMouseOut | ||
}; | ||
|
||
return ( | ||
<div className="relative w-full text-md" ref={ref}> | ||
<div className="relative top-2 mb-1 text-sm"> | ||
{__( | ||
timeAverage !== TimeAverages.HOURLY | ||
? 'country-panel.averagebysource' | ||
: 'country-panel.bysource' | ||
)} | ||
</div> | ||
|
||
{displayByEmissions ? ( | ||
<BarBreakdownEmissionsChart | ||
data={data} | ||
productionData={productionData} | ||
exchangeData={exchangeData} | ||
onProductionRowMouseOver={todoHandler} | ||
onProductionRowMouseOut={todoHandler} | ||
onExchangeRowMouseOver={todoHandler} | ||
onExchangeRowMouseOut={todoHandler} | ||
width={width} | ||
height={height} | ||
isMobile={false} | ||
/> | ||
) : ( | ||
<BarBreakdownProductionChart | ||
data={data} | ||
productionData={productionData} | ||
exchangeData={exchangeData} | ||
onProductionRowMouseOver={todoHandler} | ||
onProductionRowMouseOut={todoHandler} | ||
onExchangeRowMouseOver={todoHandler} | ||
onExchangeRowMouseOut={todoHandler} | ||
width={width} | ||
height={height} | ||
isMobile={false} | ||
/> | ||
)} | ||
</div> | ||
); | ||
} | ||
|
||
export default BarBreakdownChart; |
122 changes: 122 additions & 0 deletions
122
web/src/features/charts/bar-breakdown/BarBreakdownEmissionsChart.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import { CountryFlag } from 'components/Flag'; | ||
import { max as d3Max } from 'd3-array'; | ||
|
||
import { scaleLinear } from 'd3-scale'; | ||
import { useMemo } from 'react'; | ||
import { useTranslation } from 'translation/translation'; | ||
import { ZoneDetail } from 'types'; | ||
import { modeColor } from 'utils/constants'; | ||
import { LABEL_MAX_WIDTH, PADDING_X } from './constants'; | ||
import Axis from './elements/Axis'; | ||
import HorizontalBar from './elements/HorizontalBar'; | ||
import Row from './elements/Row'; | ||
import { ExchangeDataType, getDataBlockPositions, ProductionDataType } from './utils'; | ||
|
||
interface BarBreakdownEmissionsChartProps { | ||
height: number; | ||
width: number; | ||
data: ZoneDetail; | ||
exchangeData: ExchangeDataType[]; | ||
productionData: ProductionDataType[]; | ||
isMobile: boolean; | ||
onProductionRowMouseOver: (data: ZoneDetail) => void; | ||
onProductionRowMouseOut: () => void; | ||
onExchangeRowMouseOver: (data: ZoneDetail) => void; | ||
onExchangeRowMouseOut: () => void; | ||
} | ||
|
||
function BarBreakdownEmissionsChart({ | ||
data, | ||
exchangeData, | ||
height, | ||
isMobile, | ||
productionData, | ||
onProductionRowMouseOver, | ||
onProductionRowMouseOut, | ||
onExchangeRowMouseOver, | ||
onExchangeRowMouseOut, | ||
width, | ||
}: BarBreakdownEmissionsChartProps) { | ||
const { __ } = useTranslation(); | ||
const { productionY, exchangeY } = getDataBlockPositions( | ||
productionData.length || 0, | ||
exchangeData | ||
); | ||
|
||
const maxCO2eqExport = d3Max(exchangeData, (d) => Math.max(0, -d.tCo2eqPerMin)) || 0; | ||
const maxCO2eqImport = d3Max(exchangeData, (d) => Math.max(0, d.tCo2eqPerMin)); | ||
const maxCO2eqProduction = d3Max(productionData, (d) => d.tCo2eqPerMin); | ||
|
||
// in tCO₂eq/min | ||
const co2Scale = useMemo( | ||
() => | ||
scaleLinear() | ||
.domain([ | ||
-maxCO2eqExport || 0, | ||
Math.max(maxCO2eqProduction || 0, maxCO2eqImport || 0), | ||
]) | ||
.range([0, width - LABEL_MAX_WIDTH - PADDING_X]), | ||
[maxCO2eqExport, maxCO2eqProduction, maxCO2eqImport, width] | ||
); | ||
|
||
const formatTick = (t: number) => { | ||
const [x1, x2] = co2Scale.domain(); | ||
if (x2 - x1 <= 1) { | ||
return `${t * 1e3} kg/min`; | ||
} | ||
return `${t} t/min`; | ||
}; | ||
|
||
return ( | ||
<svg className="w-full overflow-visible" height={height}> | ||
<Axis formatTick={formatTick} height={height} scale={co2Scale} /> | ||
<g transform={`translate(0, ${productionY})`}> | ||
{productionData.map((d, index) => ( | ||
<Row | ||
key={d.mode} | ||
index={index} | ||
label={__(d.mode)} | ||
width={width} | ||
scale={co2Scale} | ||
value={Math.abs(d.tCo2eqPerMin)} | ||
onMouseOver={(event) => onProductionRowMouseOver(d.mode, data, event)} | ||
onMouseOut={onProductionRowMouseOut} | ||
isMobile={isMobile} | ||
> | ||
<HorizontalBar | ||
className="production" | ||
fill={modeColor[d.mode]} | ||
range={[0, Math.abs(d.tCo2eqPerMin)]} | ||
scale={co2Scale} | ||
/> | ||
</Row> | ||
))} | ||
</g> | ||
<g transform={`translate(0, ${exchangeY})`}> | ||
{exchangeData.map((d, index) => ( | ||
<Row | ||
key={d.mode} | ||
index={index} | ||
label={d.mode} | ||
width={width} | ||
scale={co2Scale} | ||
value={d.exchange} | ||
onMouseOver={(event) => onExchangeRowMouseOver(d.mode, data, event)} | ||
onMouseOut={onExchangeRowMouseOut} | ||
isMobile={isMobile} | ||
> | ||
<CountryFlag zoneId={d.mode} className="pointer-events-none" /> | ||
<HorizontalBar | ||
className="exchange" | ||
fill={'gray'} | ||
range={[0, d.tCo2eqPerMin]} | ||
scale={co2Scale} | ||
/> | ||
</Row> | ||
))} | ||
</g> | ||
</svg> | ||
); | ||
} | ||
|
||
export default BarBreakdownEmissionsChart; |
163 changes: 163 additions & 0 deletions
163
web/src/features/charts/bar-breakdown/BarBreakdownProductionChart.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import { CountryFlag } from 'components/Flag'; | ||
import { max as d3Max, min as d3Min } from 'd3-array'; | ||
|
||
import { scaleLinear } from 'd3-scale'; | ||
import { useCo2ColorScale } from 'hooks/theme'; | ||
import { useMemo } from 'react'; | ||
import { useTranslation } from 'translation/translation'; | ||
import { ZoneDetail } from 'types'; | ||
import { modeColor } from 'utils/constants'; | ||
import { LABEL_MAX_WIDTH, PADDING_X } from './constants'; | ||
import Axis from './elements/Axis'; | ||
import HorizontalBar from './elements/HorizontalBar'; | ||
import Row from './elements/Row'; | ||
import { | ||
ExchangeDataType, | ||
getDataBlockPositions, | ||
getElectricityProductionValue, | ||
ProductionDataType, | ||
} from './utils'; | ||
|
||
interface BarBreakdownProductionChartProps { | ||
height: number; | ||
width: number; | ||
data: ZoneDetail; | ||
exchangeData: ExchangeDataType[]; | ||
productionData: ProductionDataType[]; | ||
isMobile: boolean; | ||
onProductionRowMouseOver: (data: ZoneDetail) => void; | ||
onProductionRowMouseOut: () => void; | ||
onExchangeRowMouseOver: (data: ZoneDetail) => void; | ||
onExchangeRowMouseOut: () => void; | ||
} | ||
|
||
function BarBreakdownProductionChart({ | ||
data, | ||
exchangeData, | ||
height, | ||
isMobile, | ||
productionData, | ||
onProductionRowMouseOver, | ||
onProductionRowMouseOut, | ||
onExchangeRowMouseOver, | ||
onExchangeRowMouseOut, | ||
width, | ||
}: BarBreakdownProductionChartProps) { | ||
const { __ } = useTranslation(); | ||
const co2ColorScale = useCo2ColorScale(); | ||
const { productionY, exchangeY } = getDataBlockPositions( | ||
productionData.length, | ||
exchangeData | ||
); | ||
|
||
// Use the whole history to determine the min/max values in order to avoid | ||
// graph jumping while sliding through the time range. | ||
const [minPower, maxPower] = useMemo(() => { | ||
return [ | ||
d3Min( | ||
Object.values(data.zoneStates).map((zoneData) => | ||
Math.min( | ||
-zoneData.maxStorageCapacity || 0, | ||
-zoneData.maxStorage || 0, | ||
-zoneData.maxExport || 0, | ||
-zoneData.maxExportCapacity || 0 | ||
) | ||
) | ||
) || 0, | ||
d3Max( | ||
Object.values(data.zoneStates).map((zoneData) => | ||
Math.max( | ||
zoneData.maxCapacity || 0, | ||
zoneData.maxProduction || 0, | ||
zoneData.maxDischarge || 0, | ||
zoneData.maxStorageCapacity || 0, | ||
zoneData.maxImport || 0, | ||
zoneData.maxImportCapacity || 0 | ||
) | ||
) | ||
) || 0, | ||
]; | ||
}, [data]); | ||
|
||
// Power in MW | ||
const powerScale = scaleLinear() | ||
.domain([minPower, maxPower]) | ||
.range([0, width - LABEL_MAX_WIDTH - PADDING_X]); | ||
|
||
const formatTick = (t: number) => { | ||
const [x1, x2] = powerScale.domain(); | ||
if (x2 - x1 <= 1) { | ||
return `${t * 1e3} kW`; | ||
} | ||
if (x2 - x1 <= 1e3) { | ||
return `${t} MW`; | ||
} | ||
return `${t * 1e-3} GW`; | ||
}; | ||
|
||
return ( | ||
<svg className="w-full overflow-visible" height={height}> | ||
<Axis formatTick={formatTick} height={height} scale={powerScale} /> | ||
<g transform={`translate(0, ${productionY})`}> | ||
{productionData.map((d, index) => ( | ||
<Row | ||
key={d.mode} | ||
index={index} | ||
label={__(d.mode)} | ||
width={width} | ||
scale={powerScale} | ||
value={getElectricityProductionValue(d)} | ||
onMouseOver={(event) => onProductionRowMouseOver(d.mode, data, event)} | ||
onMouseOut={onProductionRowMouseOut} | ||
isMobile={isMobile} | ||
> | ||
<HorizontalBar | ||
className="capacity" | ||
fill="rgba(0, 0, 0, 0.15)" | ||
range={d.isStorage ? [-d.capacity, d.capacity] : [0, d.capacity]} | ||
scale={powerScale} | ||
/> | ||
<HorizontalBar | ||
className="production" | ||
fill={modeColor[d.mode]} | ||
range={[0, getElectricityProductionValue(d)]} | ||
scale={powerScale} | ||
/> | ||
</Row> | ||
))} | ||
</g> | ||
<g transform={`translate(0, ${exchangeY})`}> | ||
{exchangeData.map((d, index) => ( | ||
<Row | ||
key={d.mode} | ||
index={index} | ||
label={d.mode} | ||
width={width} | ||
scale={powerScale} | ||
value={d.exchange} | ||
onMouseOver={(event) => onExchangeRowMouseOver(d.mode, data, event)} | ||
onMouseOut={onExchangeRowMouseOut} | ||
isMobile={isMobile} | ||
> | ||
<CountryFlag zoneId={d.mode} className="pointer-events-none" /> | ||
|
||
<HorizontalBar | ||
className="capacity" | ||
fill="rgba(0, 0, 0, 0.15)" | ||
range={d.exchangeCapacityRange} | ||
scale={powerScale} | ||
/> | ||
<HorizontalBar | ||
className="exchange" | ||
fill={co2ColorScale(d.gCo2eqPerkWh)} | ||
range={[0, d.exchange]} | ||
scale={powerScale} | ||
/> | ||
</Row> | ||
))} | ||
</g> | ||
</svg> | ||
); | ||
} | ||
|
||
export default BarBreakdownProductionChart; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export const LABEL_MAX_WIDTH = 102; | ||
export const TEXT_ADJUST_Y = 11; | ||
export const ROW_HEIGHT = 13; | ||
export const PADDING_Y = 7; | ||
export const PADDING_X = 5; | ||
export const RECT_OPACITY = 0.8; | ||
export const X_AXIS_HEIGHT = 15; | ||
export const DEFAULT_FLAG_SIZE = 16; | ||
export const SCALE_TICKS = 4; |
Oops, something went wrong.