-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: option to fill area below chart
- Loading branch information
Showing
6 changed files
with
154 additions
and
54 deletions.
There are no files selected for viewing
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
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 |
---|---|---|
|
@@ -5,7 +5,7 @@ | |
* Copyright (c) 2021 Samuel Gratzl <[email protected]> | ||
*/ | ||
|
||
import React from 'react'; | ||
import React, {Fragment} from 'react'; | ||
import { defaultColorScale } from '../math'; | ||
import type { CommonProps } from './common'; | ||
import type { HeatMap1DProps } from './HeatMap1D'; | ||
|
@@ -16,8 +16,15 @@ export interface LineChartProps extends HeatMap1DProps { | |
* value line before filtering | ||
*/ | ||
preFilter?: readonly (number | null | undefined)[]; | ||
/** | ||
* fill the line chart at the bottom | ||
*/ | ||
fill?: boolean; | ||
} | ||
|
||
const width = 100; | ||
const height = 20; | ||
|
||
function generateLine( | ||
vs: readonly (number | null | undefined)[], | ||
xScale: number, | ||
|
@@ -29,88 +36,126 @@ function generateLine( | |
return ''; | ||
} | ||
const prefix = i === 0 || vs[i - 1] == null ? ' M' : ' L'; | ||
const x = i * xScale; | ||
const y = yScale(v); | ||
const x = (i * xScale) * width; | ||
const y = (1-yScale(v)) * height; | ||
return `${prefix}${x},${y}`; | ||
}) | ||
.join(''); | ||
} | ||
const width = 100; | ||
const height = 20; | ||
|
||
function generateArea( | ||
vs: readonly (number | null | undefined)[], | ||
xScale: number, | ||
yScale: (v: number) => number | ||
): string { | ||
let last = -1; | ||
let segments: string[] = []; | ||
|
||
vs.forEach((v, i) => { | ||
if (v == null) { | ||
if (last >= 0) { | ||
// close last | ||
segments.push(`M${last},${height} Z`); | ||
last = -1; | ||
} | ||
return; | ||
} | ||
const x = width * i * xScale; | ||
const y = (1 - yScale(v)) * height; | ||
if (last < 0) { | ||
segments.push(`M${x},${height} L${x},${y}`); | ||
} else { | ||
segments.push(`L${x},${y}`); | ||
} | ||
last = x; | ||
}); | ||
if (last >= 0) { | ||
segments.push(`L${last},${height} Z`); | ||
} | ||
return segments.join(' '); | ||
} | ||
|
||
/** | ||
* renders a line chart | ||
*/ | ||
export function LineChart(props: LineChartProps): JSX.Element { | ||
const values = props.value ?? []; | ||
const xNorm = 1 / (props.value.length - 1); | ||
const xScale = width * xNorm; | ||
const xScale = 1 / (props.value.length - 1); | ||
const yScale = typeof props.scale === 'function' ? props.scale : (v: number) => v; | ||
const colorScale = typeof props.color === 'string' ? () => props.color as string : props.color ?? defaultColorScale; | ||
|
||
return ( | ||
<div className={clsx('lt-line-chart', props.className)}> | ||
<div className={clsx('lt-line-chart', props.className)} style={props.style}> | ||
{typeof props.format === 'string' && <span aria-hidden="false">{props.format}</span>} | ||
<svg | ||
viewBox={`0 0 ${width} ${height}`} | ||
className={'lt-line-chart-container'} | ||
style={props.style} | ||
preserveAspectRatio="none" | ||
> | ||
{typeof props.format === 'string' && <title>{props.format}</title>} | ||
{props.fill && props.preFilter && <path className="lt-line-chart-area lt-line-chart-area-pre" d={generateArea(props.preFilter, xScale, yScale)} />} | ||
{props.preFilter && ( | ||
<path className="lt-line-chart-line lt-line-chart-pre" d={generateLine(props.preFilter, xScale, yScale)} /> | ||
)} | ||
{props.fill && <path className="lt-line-chart-area" d={generateArea(values, xScale, yScale)} />} | ||
<path className="lt-line-chart-line" d={generateLine(values, xScale, yScale)} /> | ||
</svg> | ||
{values.map((v, i) => { | ||
if (v == null) { | ||
return null; | ||
} | ||
const label = typeof props.format === 'function' ? props.format(v, i) : toLocaleString(v); | ||
const normalized = typeof props.scale === 'function' && v != null ? props.scale(v) : v; | ||
const color = colorScale(normalized, i); | ||
return ( | ||
<div | ||
key={i} | ||
className="lt-line-chart-point" | ||
style={{ | ||
backgroundColor: color, | ||
transform: `translate(${toPercent(i * xNorm)},${toPercent(1 - normalized)})`, | ||
}} | ||
title={label} | ||
/> | ||
); | ||
})} | ||
<div className="lt-line-chart-points"> | ||
{values.map((v, i) => { | ||
if (v == null) { | ||
return null; | ||
} | ||
const label = typeof props.format === 'function' ? props.format(v, i) : toLocaleString(v); | ||
const normalized = typeof props.scale === 'function' && v != null ? props.scale(v) : v; | ||
const color = colorScale(normalized, i); | ||
return ( | ||
<div | ||
key={i} | ||
className="lt-line-chart-point" | ||
style={{ | ||
backgroundColor: color, | ||
left: toPercent(i * xScale), | ||
top: toPercent(1 - normalized) | ||
}} | ||
title={label} | ||
/> | ||
); | ||
})} | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
export interface MultiLineChartProps extends CommonProps { | ||
values: readonly (readonly (number | null | undefined)[])[]; | ||
value: readonly (readonly (number | null | undefined)[])[]; | ||
/** | ||
* optional scale to convert the number in the 0..1 range | ||
*/ | ||
scale?: (v: number) => number; | ||
/** | ||
* fill the line chart at the bottom | ||
*/ | ||
fill?: boolean; | ||
} | ||
/** | ||
* renders multiple line charts | ||
*/ | ||
export function MultiLineChart(props: MultiLineChartProps): JSX.Element { | ||
const maxX = props.values.reduce((acc, v) => Math.max(acc, v ? v.length : 0), 0); | ||
const xNorm = 1 / (maxX - 1); | ||
const xScale = width * xNorm; | ||
const maxX = props.value.reduce((acc, v) => Math.max(acc, v ? v.length : 0), 0); | ||
const xScale = 1 / (maxX - 1); | ||
const yScale = typeof props.scale === 'function' ? props.scale : (v: number) => v; | ||
return ( | ||
<div className={clsx('lt-line-chart', props.className)}> | ||
<div className={clsx('lt-line-chart', props.className)} | ||
style={props.style}> | ||
<svg | ||
viewBox={`0 0 ${width} ${height}`} | ||
className={'lt-line-chart-container'} | ||
style={props.style} | ||
preserveAspectRatio="none" | ||
> | ||
{props.values.map((vs, i) => ( | ||
<path key={i} className="lt-line-chart-line" d={generateLine(vs ?? {}, xScale, yScale)} /> | ||
))} | ||
{props.value.map((vs, i) => (<Fragment key={i}> | ||
{props.fill && <path className="lt-line-chart-area" d={generateArea(vs ?? [], xScale, yScale)} />} | ||
<path className="lt-line-chart-line" d={generateLine(vs ?? [], xScale, yScale)} /> | ||
</Fragment>))} | ||
</svg> | ||
</div> | ||
); | ||
|
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
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
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
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 |
---|---|---|
|
@@ -5,23 +5,24 @@ | |
* Copyright (c) 2021 Samuel Gratzl <[email protected]> | ||
*/ | ||
|
||
import { clsx, LineChart } from '@lineup-lite/components'; | ||
import { clsx, MultiLineChart } from '@lineup-lite/components'; | ||
import React, { useContext } from 'react'; | ||
import type { CellProps, Renderer } from 'react-table'; | ||
import type { BarRendererOptions } from '.'; | ||
import type { UnknownObject } from '..'; | ||
import type { LineChartRendererOptions } from './LineChartRenderer'; | ||
import deriveNumberOptions from './deriveNumberOptions'; | ||
import { missingClass, optionContext } from './utils'; | ||
import type { UnknownObject } from '../types'; | ||
|
||
export function MultiLineChartRenderer<D extends UnknownObject, P extends CellProps<D, (number | null | undefined)[]>>( | ||
props: P | ||
): JSX.Element { | ||
const options = useContext(optionContext) as BarRendererOptions; | ||
const options = useContext(optionContext) as LineChartRendererOptions; | ||
const p = deriveNumberOptions<D, P>(props, options); | ||
return ( | ||
<LineChart | ||
<MultiLineChart | ||
{...p} | ||
value={props.value} | ||
fill={options.fill} | ||
style={options.style} | ||
className={clsx(missingClass(props.value), options.className)} | ||
/> | ||
|
@@ -31,7 +32,7 @@ export function MultiLineChartRenderer<D extends UnknownObject, P extends CellPr | |
export function MultiLineChartRendererFactory< | ||
D extends UnknownObject, | ||
P extends CellProps<D, (number | null | undefined)[]> | ||
>(options: BarRendererOptions = {}): Renderer<P> { | ||
>(options: LineChartRendererOptions = {}): Renderer<P> { | ||
return (props: P) => ( | ||
<optionContext.Provider value={options}> | ||
<MultiLineChartRenderer<D, P> {...props} /> | ||
|