Skip to content

Commit

Permalink
feat: option to fill area below chart
Browse files Browse the repository at this point in the history
  • Loading branch information
sgratzl committed Oct 24, 2021
1 parent 7e2b838 commit 8b03115
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 54 deletions.
24 changes: 21 additions & 3 deletions packages/components/src/components/LineChart.css
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
}
119 changes: 82 additions & 37 deletions packages/components/src/components/LineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -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>
);
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
@import './components/Summary.css';
@import './components/UpSetLine.css';
@import './components/HeatMap1D.css';
@import './components/LineChart.css';
38 changes: 34 additions & 4 deletions packages/docs/docs/components/numbers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,13 @@ function Example() {

return (
<div>
<LineChart value={data[0]} scale={stats.scale} color={stats.color} format={stats.format} />
<LineChart value={data[1]} scale={stats.scale} color={stats.color} format={stats.format} />
<LineChart value={data[2]} scale={stats.scale} color={stats.color} format={stats.format} />
<LineChart value={data[0]} scale={stats.scale} color={stats.color} format={stats.format} style={{ height: 25 }} />
<LineChart value={data[1]} scale={stats.scale} color={stats.color} format={stats.format} style={{ height: 25 }} />
<LineChart value={data[2]} scale={stats.scale} color={stats.color} format={stats.format} style={{ height: 25 }} />

<LineChart value={data[0]} scale={stats.scale} color={stats.color} format={stats.format} style={{ height: 25 }} fill/>
<LineChart value={data[1]} scale={stats.scale} color={stats.color} format={stats.format} style={{ height: 25 }} fill/>
<LineChart value={data[2]} scale={stats.scale} color={stats.color} format={stats.format} style={{ height: 25 }} fill/>
</div>
);
}
Expand All @@ -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();
Expand All @@ -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 (
<div>
<MultiLineChart value={data} scale={stats.scale} style={{ height: 100 }} />
<MultiLineChart value={data} scale={stats.scale} style={{ height: 100 }} fill />
</div>
);
}
```

### Interactive Summary

[`FilterRangeHistogram`](https://lineup-lite.js.org/api/components/modules.html#filterrangehistogram) React component or
Expand Down
13 changes: 9 additions & 4 deletions packages/hooks/src/renderers/LineChartRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<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
{...p}
value={props.value}
fill={options.fill}
style={options.style}
className={clsx(missingClass(props.value), options.className)}
/>
Expand All @@ -31,7 +36,7 @@ export function LineChartRenderer<D extends UnknownObject, P extends CellProps<D
export function LineChartRendererFactory<
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}>
<LineChartRenderer<D, P> {...props} />
Expand Down
13 changes: 7 additions & 6 deletions packages/hooks/src/renderers/MultiLineChartRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
/>
Expand All @@ -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} />
Expand Down

0 comments on commit 8b03115

Please sign in to comment.