Skip to content

Commit

Permalink
Merge pull request #39 from magland/184-y-axis-ticks
Browse files Browse the repository at this point in the history
Add support for sophisticated y-axis information.
  • Loading branch information
magland authored Mar 18, 2022
2 parents 7d57a7c + d9b81d2 commit ec7ad2b
Show file tree
Hide file tree
Showing 12 changed files with 273 additions and 91 deletions.
1 change: 1 addition & 0 deletions src/View.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ViewData } from './ViewData';
export type TimeseriesLayoutOpts = {
hideToolbar?: boolean
hideTimeAxis?: boolean
useYAxis?: boolean
}

type Props = {
Expand Down
3 changes: 1 addition & 2 deletions src/views/Epochs/EpochsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { matrix, multiply } from 'mathjs'
import React, { FunctionComponent, useCallback, useMemo } from 'react'
import { TimeseriesLayoutOpts } from 'View'
import { DefaultToolbarWidth } from 'views/common/TimeWidgetToolbarEntries'
import { useTimeseriesMargins } from 'views/PositionPlot/PositionPlotView'
import TimeScrollView, { TimeScrollViewPanel, use1dTimeToPixelMatrix, usePanelDimensions, usePixelsPerSecond } from '../RasterPlot/TimeScrollView/TimeScrollView'
import TimeScrollView, { TimeScrollViewPanel, use1dTimeToPixelMatrix, usePanelDimensions, usePixelsPerSecond, useTimeseriesMargins } from '../RasterPlot/TimeScrollView/TimeScrollView'
import { EpochData, EpochsViewData } from './EpochsViewData'

type Props = {
Expand Down
3 changes: 2 additions & 1 deletion src/views/MultiTimeseries/ViewWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ const ViewWrapper: FunctionComponent<Props> = ({ label, figureDataSha1, isBottom
const timeseriesLayoutOpts: TimeseriesLayoutOpts = useMemo(() => {
return {
hideToolbar: true,
hideTimeAxis: !isBottomPanel
hideTimeAxis: !isBottomPanel,
useYAxis: true // TODO: THIS IS FOR TESTING, REVERT ME
}
}, [isBottomPanel])

Expand Down
3 changes: 1 addition & 2 deletions src/views/PositionPdfPlot/PositionPdfPlotWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { matrix, multiply } from 'mathjs'
import React, { FunctionComponent, useCallback, useMemo } from 'react'
import { TimeseriesLayoutOpts } from 'View'
import { DefaultToolbarWidth } from 'views/common/TimeWidgetToolbarEntries'
import { useTimeseriesMargins } from 'views/PositionPlot/PositionPlotView'
import TimeScrollView, { TimeScrollViewPanel, use1dTimeToPixelMatrix, usePanelDimensions, usePixelsPerSecond } from '../RasterPlot/TimeScrollView/TimeScrollView'
import TimeScrollView, { TimeScrollViewPanel, use1dTimeToPixelMatrix, usePanelDimensions, usePixelsPerSecond, useTimeseriesMargins } from '../RasterPlot/TimeScrollView/TimeScrollView'
import useFetchCache from './useFetchCache'

export type FetchSegmentQuery = {
Expand Down
39 changes: 12 additions & 27 deletions src/views/PositionPlot/PositionPlotView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { matrix, multiply } from 'mathjs'
import React, { FunctionComponent, useCallback, useMemo } from 'react'
import { TimeseriesLayoutOpts } from 'View'
import colorForUnitId from 'views/common/colorForUnitId'
import useYAxisTicks, { TickSet } from 'views/common/TimeScrollView/YAxisTicks'
import { DefaultToolbarWidth } from 'views/common/TimeWidgetToolbarEntries'
import TimeScrollView, { getYAxisPixelZero, TimeScrollViewPanel, use2dPanelDataToPixelMatrix, usePanelDimensions, usePixelsPerSecond } from '../RasterPlot/TimeScrollView/TimeScrollView'
import TimeScrollView, { getYAxisPixelZero, TimeScrollViewPanel, use2dPanelDataToPixelMatrix, usePanelDimensions, usePixelsPerSecond, useProjectedYAxisTicks, useTimeseriesMargins } from '../RasterPlot/TimeScrollView/TimeScrollView'
import { PositionPlotViewData } from './PositionPlotViewData'

type Props = {
Expand All @@ -27,32 +28,6 @@ type PanelProps = {

const panelSpacing = 4

export const useTimeseriesMargins = (timeseriesLayoutOpts: TimeseriesLayoutOpts | undefined) => {
return useMemo(() => {
const {hideTimeAxis, hideToolbar} = timeseriesLayoutOpts || {}
if (hideToolbar) {
return (
{
left: 30,
right: 20,
top: 20,
bottom: hideTimeAxis ? 20 : 50
}
)
}
else {
return (
{
left: 20,
right: 20,
top: 0,
bottom: hideTimeAxis ? 0 : 40
}
)
}
}, [timeseriesLayoutOpts])
}

const PositionPlotView: FunctionComponent<Props> = ({data, timeseriesLayoutOpts, width, height}) => {
const {visibleTimeStartSeconds, visibleTimeEndSeconds } = useTimeRange()

Expand Down Expand Up @@ -149,6 +124,15 @@ const PositionPlotView: FunctionComponent<Props> = ({data, timeseriesLayoutOpts,
true
)

// TODO: All this computational stuff should probably get pushed to the TimeScrollView...
const yTicks = useYAxisTicks({ datamin: valueRange.yMin, datamax: valueRange.yMax, pixelHeight: panelHeight })
const finalYTicks = useProjectedYAxisTicks(yTicks, pixelTransform)
const yTickSet: TickSet = {
ticks: finalYTicks,
datamin: valueRange.yMin,
datamax: valueRange.yMax
}

const panels: TimeScrollViewPanel<PanelProps>[] = useMemo(() => {
const pixelZero = getYAxisPixelZero(pixelTransform)
// this could also be done as one matrix multiplication by concatenating the dimensions;
Expand Down Expand Up @@ -187,6 +171,7 @@ const PositionPlotView: FunctionComponent<Props> = ({data, timeseriesLayoutOpts,
selectedPanelKeys={selectedPanelKeys}
setSelectedPanelKeys={setSelectedPanelKeys}
timeseriesLayoutOpts={timeseriesLayoutOpts}
yTickSet={yTickSet}
width={width}
height={height}
/>
Expand Down
3 changes: 1 addition & 2 deletions src/views/RasterPlot/RasterPlotView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import React, { FunctionComponent, useCallback, useMemo } from 'react'
import { TimeseriesLayoutOpts } from 'View'
import colorForUnitId from 'views/common/colorForUnitId'
import { DefaultToolbarWidth } from 'views/common/TimeWidgetToolbarEntries'
import { useTimeseriesMargins } from 'views/PositionPlot/PositionPlotView'
import { RasterPlotViewData } from './RasterPlotViewData'
import TimeScrollView, { use1dTimeToPixelMatrix, usePanelDimensions, usePixelsPerSecond } from './TimeScrollView/TimeScrollView'
import TimeScrollView, { use1dTimeToPixelMatrix, usePanelDimensions, usePixelsPerSecond, useTimeseriesMargins } from './TimeScrollView/TimeScrollView'

type Props = {
data: RasterPlotViewData
Expand Down
8 changes: 5 additions & 3 deletions src/views/RasterPlot/TimeScrollView/TSVAxesLayer.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import BaseCanvas from 'FigurlCanvas/BaseCanvas';
import React, { useMemo } from 'react';
import { TickSet } from 'views/common/TimeScrollView/YAxisTicks';
import { paintAxes } from './paint';
import { TimeScrollViewPanel, TimeTick } from './TimeScrollView';

export type TSVAxesLayerProps<T extends {[key: string]: any}> = {
panels: TimeScrollViewPanel<T>[]
timeRange: [number, number]
timeTicks: TimeTick[]
yTickSet?: TickSet
margins: {left: number, right: number, top: number, bottom: number}
selectedPanelKeys: string[]
panelHeight: number
Expand All @@ -17,10 +19,10 @@ export type TSVAxesLayerProps<T extends {[key: string]: any}> = {
}

const TSVAxesLayer = <T extends {[key: string]: any}>(props: TSVAxesLayerProps<T>) => {
const {width, height, panels, panelHeight, perPanelOffset, timeRange, timeTicks, margins, selectedPanelKeys, hideTimeAxis} = props
const {width, height, panels, panelHeight, perPanelOffset, timeRange, timeTicks, yTickSet, margins, selectedPanelKeys, hideTimeAxis} = props
const drawData = useMemo(() => ({
width, height, panels, panelHeight, perPanelOffset, timeRange, timeTicks, margins, selectedPanelKeys, hideTimeAxis
}), [width, height, panels, panelHeight, perPanelOffset, timeRange, timeTicks, margins, selectedPanelKeys, hideTimeAxis])
width, height, panels, panelHeight, perPanelOffset, timeRange, timeTicks, yTickSet, margins, selectedPanelKeys, hideTimeAxis
}), [width, height, panels, panelHeight, perPanelOffset, timeRange, timeTicks, yTickSet, margins, selectedPanelKeys, hideTimeAxis])

return (
<BaseCanvas
Expand Down
42 changes: 41 additions & 1 deletion src/views/RasterPlot/TimeScrollView/TimeScrollView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { abs, matrix, Matrix, multiply } from 'mathjs';
import Splitter from 'MountainWorkspace/components/Splitter/Splitter';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { TimeseriesLayoutOpts } from 'View';
import { Step, TickSet } from 'views/common/TimeScrollView/YAxisTicks';
import TimeWidgetToolbarEntries, { DefaultToolbarWidth } from 'views/common/TimeWidgetToolbarEntries';
import { Divider, ToolbarItem } from 'views/common/Toolbars';
import ViewToolbar from 'views/common/ViewToolbar';
Expand All @@ -19,6 +20,13 @@ export type TimeScrollViewPanel<T extends {[key: string]: any}> = {
paint: (context: CanvasRenderingContext2D, props: T) => void
}

type PartialMargins = {
left?: number,
right?: number,
top?: number,
bottom?: number
}

type Margins = {
left: number,
right: number,
Expand Down Expand Up @@ -46,6 +54,7 @@ type TimeScrollViewProps<T extends {[key: string]: any}> = {
optionalActionsAboveDefault?: ToolbarItem[]
optionalActionsBelowDefault?: ToolbarItem[]
timeseriesLayoutOpts?: TimeseriesLayoutOpts
yTickSet?: TickSet
width: number
height: number
}
Expand Down Expand Up @@ -76,6 +85,24 @@ export const usePixelsPerSecond = (panelWidth: number, startTimeSec: number | un
}, [panelWidth, startTimeSec, endTimeSec])
}

export const useTimeseriesMargins = (timeseriesLayoutOpts: TimeseriesLayoutOpts | undefined, manualMargins?: PartialMargins | undefined): Margins => {
return useMemo(() => {
const {hideTimeAxis, hideToolbar, useYAxis } = timeseriesLayoutOpts || {}
const yAxisLeftMargin = useYAxis ? 20 : 0
const defaultMargins = hideToolbar ? {
left: 30 + yAxisLeftMargin,
right: 20,
top: 20,
bottom: hideTimeAxis ? 20 : 50
} : {
left: 20 + yAxisLeftMargin,
right: 20,
top: 0,
bottom: hideTimeAxis ? 0 : 40
}
return { ...defaultMargins, ...manualMargins}
}, [timeseriesLayoutOpts, manualMargins])
}

/* Scaling matrix computations */

Expand Down Expand Up @@ -338,6 +365,18 @@ const useTimeTicks = (startTimeSec: number | undefined, endTimeSec: number | und
}, [startTimeSec, endTimeSec, timeToPixelMatrix, pixelsPerSecond])
}

export const useProjectedYAxisTicks = (ticks: Step[], transform: Matrix) => {
// transform is assumed to be the output of our use2dPanelDataToPixelMatrix
return useMemo(() => {
const augmentedValues = matrix([
new Array(ticks.length).fill(0),
ticks.map(t => t.dataValue),
new Array(ticks.length).fill(1)])
const pixelValues = (multiply(transform, augmentedValues).valueOf() as number[][])[1]
return ticks.map((t, ii) => {return {...t, pixelValue: pixelValues[ii]}})
}, [ticks, transform])
}


// Unfortunately, you can't nest generic type declarations here: so while this is properly a
// FunctionComponent<TimeScrollViewPanel<T>>, there just isn't a way to do that syntactically
Expand All @@ -347,7 +386,7 @@ const useTimeTicks = (startTimeSec: number | undefined, endTimeSec: number | und
// expects to consume, since the code will successfully infer that this is a FunctionComponent that
// takes a TimeScrollViewProps.
const TimeScrollView = <T extends {[key: string]: any}> (props: TimeScrollViewProps<T>) => {
const { margins, panels, panelSpacing, selectedPanelKeys, width, height, optionalActionsAboveDefault, optionalActionsBelowDefault, timeseriesLayoutOpts, highlightSpans } = props
const { margins, panels, panelSpacing, selectedPanelKeys, width, height, optionalActionsAboveDefault, optionalActionsBelowDefault, timeseriesLayoutOpts, highlightSpans, yTickSet } = props
const { hideToolbar, hideTimeAxis } = timeseriesLayoutOpts || {}
const { visibleTimeStartSeconds, visibleTimeEndSeconds, zoomRecordingSelection, panRecordingSelection, panRecordingSelectionDeltaT } = useTimeRange()
const { focusTime, setTimeFocusFraction, timeForFraction } = useTimeFocus()
Expand Down Expand Up @@ -535,6 +574,7 @@ const TimeScrollView = <T extends {[key: string]: any}> (props: TimeScrollViewPr
selectedPanelKeys={selectedPanelKeys}
timeRange={timeRange}
timeTicks={timeTicks}
yTickSet={yTickSet}
margins={definedMargins}
hideTimeAxis={hideTimeAxis}
/>
Expand Down
115 changes: 70 additions & 45 deletions src/views/RasterPlot/TimeScrollView/paint.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { TickSet } from "views/common/TimeScrollView/YAxisTicks";
import { TimeScrollViewPanel, TimeTick } from "./TimeScrollView";
import { TSVAxesLayerProps } from "./TSVAxesLayer";
import { TSVHighlightLayerProps } from './TSVHighlightLayer';
import { MainLayerProps } from "./TSVMainLayer";
Expand Down Expand Up @@ -45,64 +47,87 @@ export const paintSpanHighlights = (context: CanvasRenderingContext2D, props: TS

export const paintAxes = <T extends {[key: string]: any}>(context: CanvasRenderingContext2D, props: TSVAxesLayerProps<T> & {'selectedPanelKeys': string[]}) => {
// I've left the timeRange in the props list since we will probably want to display something with it at some point
// Q: maybe it'd be better to look at context.canvas.width rather than the width prop?
const {width, height, margins, panels, panelHeight, perPanelOffset, selectedPanelKeys, timeTicks, hideTimeAxis} = props
const {width, height, margins, panels, panelHeight, perPanelOffset, selectedPanelKeys, yTickSet, timeTicks, hideTimeAxis} = props
context.clearRect(0, 0, context.canvas.width, context.canvas.height)

// x-axes

const xAxisVerticalPosition = height - margins.bottom
paintTimeTicks(context, timeTicks, hideTimeAxis, xAxisVerticalPosition, margins.top)
if (!hideTimeAxis) {
context.strokeStyle = 'black'
drawLine(context, margins.left, height - margins.bottom, width - margins.right, height - margins.bottom)
drawLine(context, margins.left, xAxisVerticalPosition, width - margins.right, xAxisVerticalPosition)
}
yTickSet && paintYTicks(context, yTickSet, xAxisVerticalPosition, margins.left, width - margins.right, margins.top)
paintPanelHighlights(context, panels, selectedPanelKeys, margins.top, width, perPanelOffset, panelHeight)
paintPanelLabels(context, panels, margins.left, margins.top, perPanelOffset, panelHeight)
}

// TODO: This logic is highly similar to paintTimeTicks. Try to unify.
const paintYTicks = (context: CanvasRenderingContext2D, tickSet: TickSet, xAxisYCoordinate: number, yAxisXCoordinate: number, plotRightPx: number, topMargin: number) => {
const labelOffsetFromGridline = 2
const gridlineLeftEdge = yAxisXCoordinate - 5
const labelRightEdge = gridlineLeftEdge - labelOffsetFromGridline
const { datamax, datamin, ticks } = tickSet
context.fillStyle = 'black'
context.textAlign = 'right'
// Range-end labels
context.textBaseline = 'bottom'
context.fillText(datamax.toFixed(0), labelRightEdge, topMargin)
context.textBaseline = 'middle'
context.fillText(datamin.toString(), labelRightEdge, xAxisYCoordinate)

ticks.forEach(tick => {
if (!tick.pixelValue) return
const pixelValueWithMargin = tick.pixelValue + topMargin
context.strokeStyle = tick.isMajor ? 'gray' : 'lightgray'
context.fillStyle = tick.isMajor ? 'black' : 'gray'
drawLine(context, gridlineLeftEdge, pixelValueWithMargin, plotRightPx, pixelValueWithMargin)
context.fillText(tick.label, labelRightEdge, pixelValueWithMargin) // TODO: Add a max width thingy
})
}

// time ticks
for (let tt of timeTicks) {
// const frac = (tt.value - timeRange[0]) / (timeRange[1] - timeRange[0])
// const x = margins.left + frac * (width - margins.left - margins.right)
context.strokeStyle = tt.major ? 'gray' : 'lightgray'
// this is the tick line inside the plot view
drawLine(context, tt.pixelXposition, height - margins.bottom, tt.pixelXposition, margins.top)
const paintTimeTicks = (context: CanvasRenderingContext2D, timeTicks: TimeTick[], hideTimeAxis: boolean | undefined, xAxisPixelHeight: number, plotTopPixelHeight: number) => {
if (!timeTicks || timeTicks.length === 0) return
// Grid line length: if time axis is shown, grid lines extends 5 pixels below it. Otherwise they should stop at the edge of the plotting space.
const labelOffsetFromGridline = 2
const gridlineBottomEdge = xAxisPixelHeight + (hideTimeAxis ? 0 : + 5)
context.textAlign = 'center'
context.textBaseline = 'top'
timeTicks.forEach(tick => {
context.strokeStyle = tick.major ? 'gray' : 'lightgray'
drawLine(context, tick.pixelXposition, gridlineBottomEdge, tick.pixelXposition, plotTopPixelHeight)
if (!hideTimeAxis) {
// this is the tick line that extends below the plot view
drawLine(context, tt.pixelXposition, height - margins.bottom, tt.pixelXposition, height - margins.bottom + 5)
context.textAlign = 'center'
context.textBaseline = 'top'
const y1 = height - margins.bottom + 7
context.fillStyle = tt.major ? 'black' : 'gray'
context.fillText(tt.label, tt.pixelXposition, y1)
context.fillStyle = tick.major ? 'black' : 'gray'
context.fillText(tick.label, tick.pixelXposition, gridlineBottomEdge + labelOffsetFromGridline)
}
}
})
}

// selected panels
if (selectedPanelKeys) {
for (let i = 0; i < panels.length; i++) {
const p = panels[i]
if (selectedPanelKeys.includes(p.key)) {
const y1 = margins.top + i * (perPanelOffset)
const rect = {x: 0, y: y1, width: width, height: panelHeight}
context.fillStyle = highlightedRowFillStyle
context.fillRect(rect.x, rect.y, rect.width, rect.height)
}
const paintPanelHighlights = (context: CanvasRenderingContext2D, panels: TimeScrollViewPanel<any>[], selectedPanelKeys: string[], topMargin: number, width: number, perPanelOffset: number, panelHeight: number) => {
if (!selectedPanelKeys || selectedPanelKeys.length === 0) return
context.fillStyle = highlightedRowFillStyle
panels.forEach((panel, ii) => {
if (selectedPanelKeys.includes(panel.key)) {
const topOfHighlight = topMargin + ii * (perPanelOffset)
context.fillRect(0, topOfHighlight, width, panelHeight)
}
}

// // Highlighted spans
// if (highlightSpans && highlightSpans.length > 0) {
// paintSpanHighlights(context, margins.top, context.canvas.height - margins.bottom - margins.top, highlightSpans)
// }
})
}

const paintPanelLabels = (context: CanvasRenderingContext2D, panels: TimeScrollViewPanel<any>[], leftMargin: number, topMargin: number, perPanelOffset: number, panelHeight: number) => {
if (!panels.some(p => p.label) || perPanelOffset < 7.2) return // based on our default '10px sans-serif' font -- probably should be dynamic

// panel axes
for (let i = 0; i < panels.length; i++) {
const p = panels[i]
context.textAlign = 'right'
context.textBaseline = 'middle'
const y1 = margins.top + i * (perPanelOffset)
context.fillStyle = 'black'
context.fillText(p.label, margins.left - 5, y1 + panelHeight / 2)
}
context.textAlign = 'right'
context.textBaseline = 'middle'
context.fillStyle = 'black'
const rightEdgeOfText = leftMargin - 5
let yPosition = topMargin + panelHeight / 2
panels.forEach((panel) => {
context.fillText(panel.label, rightEdgeOfText, yPosition)
yPosition += perPanelOffset
})
}


const drawLine = (context: CanvasRenderingContext2D, x1: number, y1: number, x2: number, y2: number) => {
context.beginPath()
context.moveTo(x1, y1)
Expand Down
Loading

0 comments on commit ec7ad2b

Please sign in to comment.