From 8b031155ab3606f2230c1fde0befa18c70f0a60b Mon Sep 17 00:00:00 2001 From: Samuel Gratzl Date: Sun, 24 Oct 2021 17:09:17 -0400 Subject: [PATCH] feat: option to fill area below chart --- .../components/src/components/LineChart.css | 24 +++- .../components/src/components/LineChart.tsx | 119 ++++++++++++------ packages/components/src/style.css | 1 + packages/docs/docs/components/numbers.mdx | 38 +++++- .../hooks/src/renderers/LineChartRenderer.tsx | 13 +- .../src/renderers/MultiLineChartRenderer.tsx | 13 +- 6 files changed, 154 insertions(+), 54 deletions(-) diff --git a/packages/components/src/components/LineChart.css b/packages/components/src/components/LineChart.css index 371e532..494a242 100644 --- a/packages/components/src/components/LineChart.css +++ b/packages/components/src/components/LineChart.css @@ -9,28 +9,46 @@ display: flex; flex-direction: column; position: relative; + padding: 2px; } .lt-line-chart-container { flex: 1 1 0; } +.lt-line-chart-points { + position: absolute; + left: 2px; + top: 2px; + right: 2px; + bottom: 2px; +} + .lt-line-chart-line { stroke: var(--current-color, black); vector-effect: non-scaling-stroke; + fill: none; +} + +.lt-line-chart-area { + fill: var(--current-color, black); + opacity: 0.25; } .lt-line-chart-pre { opacity: 0.2; } +.lt-line-chart-area-pre { + opacity: 0.05; +} + .lt-line-chart-point { position: absolute; - left: 0; - top: 0; width: 4px; height: 4px; + border-radius: 50%; + transform: translate(-50%, -50%); transform-origin: center center; background-color: var(--current-color, black); - border-radius: 50%; } \ No newline at end of file diff --git a/packages/components/src/components/LineChart.tsx b/packages/components/src/components/LineChart.tsx index 7949f09..ff40240 100644 --- a/packages/components/src/components/LineChart.tsx +++ b/packages/components/src/components/LineChart.tsx @@ -5,7 +5,7 @@ * Copyright (c) 2021 Samuel Gratzl */ -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 ( -
+
+ {typeof props.format === 'string' && {props.format}} {typeof props.format === 'string' && {props.format}} + {props.fill && props.preFilter && } {props.preFilter && ( )} + {props.fill && } - {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 ( -
- ); - })} +
+ {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 ( +
+ ); + })} +
); } 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 ( -
+
- {props.values.map((vs, i) => ( - - ))} + {props.value.map((vs, i) => ( + {props.fill && } + + ))}
); diff --git a/packages/components/src/style.css b/packages/components/src/style.css index 307438c..f68d4b8 100644 --- a/packages/components/src/style.css +++ b/packages/components/src/style.css @@ -18,3 +18,4 @@ @import './components/Summary.css'; @import './components/UpSetLine.css'; @import './components/HeatMap1D.css'; +@import './components/LineChart.css'; diff --git a/packages/docs/docs/components/numbers.mdx b/packages/docs/docs/components/numbers.mdx index aedaf39..9b2150c 100644 --- a/packages/docs/docs/components/numbers.mdx +++ b/packages/docs/docs/components/numbers.mdx @@ -86,9 +86,13 @@ function Example() { return (
- - - + + + + + + +
); } @@ -99,7 +103,7 @@ function Example() { [`BoxPlotArray`](https://lineup-lite.js.org/api/components/modules.html#boxplotarray) React component ```jsx live -// import { numberStatsGenerator, Histogram, BoxPlot } from '@lineup-lite/components'; +// import { numberStatsGenerator, BoxPlotArray } from '@lineup-lite/components'; function Example() { const compute = numberStatsGenerator(); @@ -122,6 +126,32 @@ function Example() { } ``` + +[`MultiLineChart`](https://lineup-lite.js.org/api/components/modules.html#multilinechart) React component + +```jsx live +// import { numberStatsGenerator, MultiLineChart } from '@lineup-lite/components'; + +function Example() { + const compute = numberStatsGenerator(); + const data = [ + [10, 20, 40, 30, 15], + [21, 39, 25, 42, 18], + [16, 44, 30, 32, 31], + [39, 50, 43, 32, 24], + [36, 19, 37, 38, 17], + ]; + const stats = compute(data); + + return ( +
+ + +
+ ); +} +``` + ### Interactive Summary [`FilterRangeHistogram`](https://lineup-lite.js.org/api/components/modules.html#filterrangehistogram) React component or diff --git a/packages/hooks/src/renderers/LineChartRenderer.tsx b/packages/hooks/src/renderers/LineChartRenderer.tsx index 5abee6c..506d78f 100644 --- a/packages/hooks/src/renderers/LineChartRenderer.tsx +++ b/packages/hooks/src/renderers/LineChartRenderer.tsx @@ -8,20 +8,25 @@ import { clsx, LineChart } 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 { BarRendererOptions } from './BarRenderer'; +import type { UnknownObject } from '../types'; import deriveNumberOptions from './deriveNumberOptions'; import { missingClass, optionContext } from './utils'; +export interface LineChartRendererOptions extends BarRendererOptions { + fill?: boolean; +} + export function LineChartRenderer>( props: P ): JSX.Element { - const options = useContext(optionContext) as BarRendererOptions; + const options = useContext(optionContext) as LineChartRendererOptions; const p = deriveNumberOptions(props, options); return ( @@ -31,7 +36,7 @@ export function LineChartRenderer ->(options: BarRendererOptions = {}): Renderer

{ +>(options: LineChartRendererOptions = {}): Renderer

{ return (props: P) => ( {...props} /> diff --git a/packages/hooks/src/renderers/MultiLineChartRenderer.tsx b/packages/hooks/src/renderers/MultiLineChartRenderer.tsx index 8f630f0..e37fdb3 100644 --- a/packages/hooks/src/renderers/MultiLineChartRenderer.tsx +++ b/packages/hooks/src/renderers/MultiLineChartRenderer.tsx @@ -5,23 +5,24 @@ * Copyright (c) 2021 Samuel Gratzl */ -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>( props: P ): JSX.Element { - const options = useContext(optionContext) as BarRendererOptions; + const options = useContext(optionContext) as LineChartRendererOptions; const p = deriveNumberOptions(props, options); return ( - @@ -31,7 +32,7 @@ export function MultiLineChartRenderer ->(options: BarRendererOptions = {}): Renderer

{ +>(options: LineChartRendererOptions = {}): Renderer

{ return (props: P) => ( {...props} />