diff --git a/ui/src/minard/components/Axes.tsx b/ui/src/minard/components/Axes.tsx index 36d96ccd69..84a3edb514 100644 --- a/ui/src/minard/components/Axes.tsx +++ b/ui/src/minard/components/Axes.tsx @@ -1,6 +1,11 @@ import React, {useRef, useLayoutEffect, SFC} from 'react' -import {PlotEnv, TICK_PADDING_RIGHT, TICK_PADDING_TOP} from 'src/minard' +import { + PlotEnv, + TICK_PADDING_RIGHT, + TICK_PADDING_TOP, + PLOT_PADDING, +} from 'src/minard' import {clearCanvas} from 'src/minard/utils/clearCanvas' interface Props { @@ -20,9 +25,13 @@ export const drawAxes = ( const { width, height, + innerWidth, + innerHeight, margins, xTicks, yTicks, + xAxisLabel, + yAxisLabel, baseLayer: { scales: {x: xScale, y: yScale}, }, @@ -77,6 +86,31 @@ export const drawAxes = ( context.fillText(String(yTick), margins.left - TICK_PADDING_RIGHT, y) } + + // Draw the x axis label + if (xAxisLabel) { + context.textAlign = 'center' + context.textBaseline = 'bottom' + context.fillText( + xAxisLabel, + margins.left + innerWidth / 2, + height - PLOT_PADDING + ) + } + + // Draw the y axis label + if (yAxisLabel) { + const x = PLOT_PADDING + const y = margins.top + innerHeight / 2 + + context.save() + context.translate(x, y) + context.rotate(-Math.PI / 2) + context.textAlign = 'center' + context.textBaseline = 'top' + context.fillText(yAxisLabel, 0, 0) + context.restore() + } } export const Axes: SFC = props => { diff --git a/ui/src/minard/components/Plot.tsx b/ui/src/minard/components/Plot.tsx index 82ffb8bb25..8facd5fa97 100644 --- a/ui/src/minard/components/Plot.tsx +++ b/ui/src/minard/components/Plot.tsx @@ -9,6 +9,8 @@ import { setTable, setControlledXDomain, setControlledYDomain, + setXAxisLabel, + setYAxisLabel, } from 'src/minard/utils/plotEnvActions' import {plotEnvReducer, INITIAL_PLOT_ENV} from 'src/minard/utils/plotEnvReducer' @@ -29,6 +31,8 @@ export interface Props { axesStroke?: string tickFont?: string tickFill?: string + xAxisLabel?: string + yAxisLabel?: string // The x domain of the plot can be explicitly set. If this prop is passed, // then the component is operating in a "controlled" mode, where it always @@ -53,6 +57,8 @@ export const Plot: SFC = ({ axesStroke = '#31313d', tickFont = 'bold 10px Roboto', tickFill = '#8e91a1', + xAxisLabel = '', + yAxisLabel = '', xDomain = null, yDomain = null, }) => { @@ -62,12 +68,16 @@ export const Plot: SFC = ({ height, xDomain, yDomain, + xAxisLabel, + yAxisLabel, baseLayer: {...INITIAL_PLOT_ENV.baseLayer, table}, }) useMountedEffect(() => dispatch(setTable(table)), [table]) useMountedEffect(() => dispatch(setControlledXDomain(xDomain)), [xDomain]) useMountedEffect(() => dispatch(setControlledYDomain(yDomain)), [yDomain]) + useMountedEffect(() => dispatch(setXAxisLabel(xAxisLabel)), [xAxisLabel]) + useMountedEffect(() => dispatch(setYAxisLabel(yAxisLabel)), [yAxisLabel]) useMountedEffect(() => dispatch(setDimensions(width, height)), [ width, height, diff --git a/ui/src/minard/index.ts b/ui/src/minard/index.ts index cbec000dd0..e15c8c6043 100644 --- a/ui/src/minard/index.ts +++ b/ui/src/minard/index.ts @@ -9,6 +9,8 @@ export const TICK_PADDING_TOP = 5 export const TICK_CHAR_WIDTH = 7 export const TICK_CHAR_HEIGHT = 10 +export const AXIS_LABEL_PADDING_BOTTOM = 15 + export {Plot} from 'src/minard/components/Plot' export { @@ -146,6 +148,8 @@ export interface PlotEnv { margins: Margins xTicks: number[] yTicks: number[] + xAxisLabel: string + yAxisLabel: string // If the domains have been explicitly passed in to the `Plot` component, // they will be stored here. Scales and child layers use the `xDomain` and diff --git a/ui/src/minard/utils/plotEnvActions.ts b/ui/src/minard/utils/plotEnvActions.ts index 4005622eab..4561de3279 100644 --- a/ui/src/minard/utils/plotEnvActions.ts +++ b/ui/src/minard/utils/plotEnvActions.ts @@ -8,6 +8,8 @@ export type PlotAction = | ResetAction | SetControlledXDomainAction | SetControlledYDomainAction + | SetXAxisLabelAction + | SetYAxisLabelAction interface RegisterLayerAction { type: 'REGISTER_LAYER' @@ -91,3 +93,23 @@ export const setControlledYDomain = ( type: 'SET_CONTROLLED_Y_DOMAIN', payload: {yDomain}, }) + +interface SetXAxisLabelAction { + type: 'SET_X_AXIS_LABEL' + payload: {xAxisLabel: string} +} + +export const setXAxisLabel = (xAxisLabel: string): SetXAxisLabelAction => ({ + type: 'SET_X_AXIS_LABEL', + payload: {xAxisLabel}, +}) + +interface SetYAxisLabelAction { + type: 'SET_Y_AXIS_LABEL' + payload: {yAxisLabel: string} +} + +export const setYAxisLabel = (yAxisLabel: string): SetYAxisLabelAction => ({ + type: 'SET_Y_AXIS_LABEL', + payload: {yAxisLabel}, +}) diff --git a/ui/src/minard/utils/plotEnvReducer.ts b/ui/src/minard/utils/plotEnvReducer.ts index 48b8f55829..1759347afc 100644 --- a/ui/src/minard/utils/plotEnvReducer.ts +++ b/ui/src/minard/utils/plotEnvReducer.ts @@ -13,6 +13,7 @@ import { TICK_CHAR_HEIGHT, TICK_PADDING_RIGHT, TICK_PADDING_TOP, + AXIS_LABEL_PADDING_BOTTOM, } from 'src/minard' import {PlotAction} from 'src/minard/utils/plotEnvActions' import {getGroupKey} from 'src/minard/utils/getGroupKey' @@ -33,6 +34,8 @@ export const INITIAL_PLOT_ENV: PlotEnv = { }, xTicks: [], yTicks: [], + xAxisLabel: '', + yAxisLabel: '', xDomain: null, yDomain: null, baseLayer: { @@ -119,6 +122,26 @@ export const plotEnvReducer = (state: PlotEnv, action: PlotAction): PlotEnv => return } + + case 'SET_X_AXIS_LABEL': { + const {xAxisLabel} = action.payload + + draftState.xAxisLabel = xAxisLabel + + setLayout(draftState) + + return + } + + case 'SET_Y_AXIS_LABEL': { + const {yAxisLabel} = action.payload + + draftState.yAxisLabel = yAxisLabel + + setLayout(draftState) + + return + } } }) @@ -230,11 +253,20 @@ const setLayout = (draftState: PlotEnv): void => { const yTickWidth = Math.max(...draftState.yTicks.map(t => String(t).length)) * TICK_CHAR_WIDTH + const xAxisLabelHeight = draftState.xAxisLabel + ? TICK_CHAR_HEIGHT + AXIS_LABEL_PADDING_BOTTOM + : 0 + + const yAxisLabelHeight = draftState.yAxisLabel + ? TICK_CHAR_HEIGHT + AXIS_LABEL_PADDING_BOTTOM + : 0 + const margins = { top: PLOT_PADDING, right: PLOT_PADDING, - bottom: TICK_CHAR_HEIGHT + TICK_PADDING_TOP + PLOT_PADDING, - left: yTickWidth + TICK_PADDING_RIGHT + PLOT_PADDING, + bottom: + TICK_CHAR_HEIGHT + TICK_PADDING_TOP + PLOT_PADDING + xAxisLabelHeight, + left: yTickWidth + TICK_PADDING_RIGHT + PLOT_PADDING + yAxisLabelHeight, } const innerWidth = width - margins.left - margins.right