Skip to content

Commit 204e0c6

Browse files
authored
Merge pull request #1930 from tradingview/add-pretty-histogram-plugin-example
Added example of pretty histogram plugin
2 parents 36f123b + 8d06e78 commit 204e0c6

8 files changed

Lines changed: 324 additions & 0 deletions

File tree

plugin-examples/src/examples-base.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,22 @@ button {
4747
cursor: pointer;
4848
}
4949

50+
input[type='text'] {
51+
all: initial;
52+
font-family: -apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu,
53+
sans-serif;
54+
font-size: 16px;
55+
font-style: normal;
56+
font-weight: 510;
57+
line-height: 24px; /* 150% */
58+
letter-spacing: -0.32px;
59+
padding: 8px 24px;
60+
border-radius: 8px;
61+
background-color: #fff;
62+
border: 1px solid rgba(224, 227, 235, 1);
63+
cursor: pointer;
64+
}
65+
5066
button:hover {
5167
background-color: rgba(30, 83, 229, 1);
5268
}

plugin-examples/src/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ <h2>Custom Series</h2>
2929
<li><a href="./plugins/grouped-bars-series/example/">GroupedBars Series</a></li>
3030
<li>Heatmap Series: <a href="./plugins/heatmap-series/example/">Example 1</a> / <a href="./plugins/heatmap-series/example/example2.html">Example 2</a></li>
3131
<li><a href="./plugins/hlc-area-series/example/">HLC Area Series</a></li>
32+
<li><a href="./plugins/pretty-histogram/example/">Pretty Histogram</a></li>
3233
<li><a href="./plugins/lollipop-series/example/">Lollipop Series</a></li>
3334
<li><a href="./plugins/rounded-candles-series/example/">Rounded Candle Series</a></li>
3435
<li><a href="./plugins/background-shade-series/example/">Shaded Background Series</a></li>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { SeriesDataItemTypeMap, SingleValueData } from 'lightweight-charts';
2+
3+
export type PrettyHistogramData<HorzScaleItem> = SeriesDataItemTypeMap<HorzScaleItem>['Histogram'] & SingleValueData<HorzScaleItem>;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { AutoscaleInfo, DeepPartial, HistogramData, SeriesDataItemTypeMap, SingleValueData, Time, WhitespaceData, createChart } from 'lightweight-charts';
2+
import { generateLineData } from '../../../sample-data';
3+
import { PrettyHistogramSeries } from '../pretty-histogram-series';
4+
import { PrettyHistogramSeriesOptions } from '../options';
5+
import { PrettyHistogramData } from '../data';
6+
7+
const chart = ((window as unknown as any).chart = createChart('chart', {
8+
autoSize: true,
9+
}));
10+
11+
const customSeriesView = new PrettyHistogramSeries();
12+
13+
let autoscaleToZero = true;
14+
15+
const options: DeepPartial<PrettyHistogramSeriesOptions> = {
16+
autoscaleInfoProvider: (baseImplementation: () => AutoscaleInfo | null) => {
17+
const baseRes = baseImplementation();
18+
if (!autoscaleToZero) {
19+
return baseRes;
20+
}
21+
if (!baseRes?.priceRange) {
22+
return { priceRange: { minValue: 0, maxValue: 0 } };
23+
}
24+
const minValue = Math.min(baseRes.priceRange.minValue, 0);
25+
const maxValue = Math.max(baseRes.priceRange.maxValue, 0);
26+
return { ...baseRes, priceRange: { minValue, maxValue } };
27+
},
28+
radius: 6,
29+
widthPercent: 50,
30+
};
31+
32+
const myCustomSeries = chart.addCustomSeries(customSeriesView, options);
33+
34+
const data: PrettyHistogramData<Time>[] = generateLineData(6);
35+
data.forEach((item: PrettyHistogramData<Time>, i: number) => {
36+
(item as HistogramData<Time>).color = (i % 2) ? '#6438D6' : undefined;
37+
});
38+
39+
myCustomSeries.setData(data);
40+
41+
chart.timeScale().fitContent();
42+
43+
data.forEach((item: (SeriesDataItemTypeMap<Time>['Custom'] & SingleValueData<Time>), i: number) => {
44+
const element = document.getElementById(`bar_${i + 1}`) as HTMLInputElement;
45+
element.value = item.value.toFixed(2);
46+
element.onchange = () => {
47+
const newValue = parseFloat(element.value);
48+
if (!isNaN(newValue)) {
49+
item.value = newValue;
50+
myCustomSeries.setData(data);
51+
}
52+
};
53+
});
54+
55+
const radiusElement = document.getElementById('radius') as HTMLInputElement;
56+
radiusElement.value = options.radius!.toString();
57+
radiusElement.onchange = () => {
58+
const newRadius = parseFloat(radiusElement.value);
59+
if (newRadius >= 0) {
60+
options.radius = newRadius;
61+
myCustomSeries.applyOptions(options);
62+
}
63+
}
64+
65+
const widthElement = document.getElementById('width') as HTMLInputElement;
66+
widthElement.value = options.widthPercent!.toString();
67+
widthElement.onchange = () => {
68+
const newWidth = parseFloat(widthElement.value);
69+
if (newWidth >= 0 && newWidth <= 100) {
70+
options.widthPercent = newWidth;
71+
myCustomSeries.applyOptions(options);
72+
}
73+
};
74+
75+
const zeroAutoscaleElement = document.getElementById('autoscaleToZero') as HTMLInputElement;
76+
zeroAutoscaleElement.checked = autoscaleToZero;
77+
zeroAutoscaleElement.onchange = () => {
78+
autoscaleToZero = zeroAutoscaleElement.checked;
79+
myCustomSeries.applyOptions(options);
80+
};
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Lightweight Charts - Pretty Histogram Series Plugin Example</title>
7+
<link href="../../../examples-base.css" rel="stylesheet" />
8+
<style>
9+
.barEditor {
10+
display: flex;
11+
flex-direction: row;
12+
align-items: center;
13+
margin: 10px;
14+
justify-content: space-between;
15+
white-space: nowrap;
16+
}
17+
.barEditor input {
18+
padding-left: 10px;
19+
margin-left: 15px;
20+
}
21+
</style>
22+
</head>
23+
<body>
24+
<div id="chart"></div>
25+
<div id="description">
26+
<h1>Pretty Histogram Series</h1>
27+
<p>
28+
Histogram series with custom rendering. Round corners and customazible column widths.
29+
</p>
30+
<div style="display: flex; flex-direction: row;">
31+
<div style="display: flex; flex-direction: column;margin:10px">
32+
<div class="barEditor">
33+
Bar 1:
34+
<input id="bar_1" type="text">
35+
</div>
36+
<div class="barEditor">
37+
Bar 2:
38+
<input id="bar_2" type="text">
39+
</div>
40+
<div class="barEditor">
41+
Bar 3:
42+
<input id="bar_3" type="text">
43+
</div>
44+
<div class="barEditor" type="text">
45+
Bar 4:
46+
<input id="bar_4" type="text">
47+
</div>
48+
<div class="barEditor" type="text">
49+
Bar 5:
50+
<input id="bar_5" type="text">
51+
</div>
52+
<div class="barEditor" type="text">
53+
Bar 5:
54+
<input id="bar_6" type="text">
55+
</div>
56+
</div>
57+
<div style="display: flex; flex-direction: column;margin:10px">
58+
<div class="barEditor">
59+
Radius:
60+
<input id="radius" type="text">
61+
</div>
62+
<div class="barEditor">
63+
Width %:
64+
<input id="width" type="text">
65+
</div>
66+
<div class="barEditor">
67+
<label for="autoscaleToZero">Autoscale to zero</label>
68+
<input id="autoscaleToZero" type="checkbox" checked>
69+
</div>
70+
</div>
71+
</div>
72+
</div>
73+
<script type="module" src="./example.ts"></script>
74+
</body>
75+
</html>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { customSeriesDefaultOptions, CustomSeriesOptions } from 'lightweight-charts';
2+
3+
export interface PrettyHistogramSeriesOptions extends CustomSeriesOptions {
4+
color: string;
5+
widthPercent: number;
6+
radius: number;
7+
}
8+
9+
export const defaultOptions: PrettyHistogramSeriesOptions = {
10+
...customSeriesDefaultOptions,
11+
color: '#D63864',
12+
widthPercent: 50,
13+
radius: 4,
14+
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {
2+
CustomSeriesPricePlotValues,
3+
ICustomSeriesPaneView,
4+
PaneRendererCustomData,
5+
SeriesDataItemTypeMap,
6+
SingleValueData,
7+
Time,
8+
WhitespaceData
9+
} from 'lightweight-charts';
10+
import { defaultOptions, PrettyHistogramSeriesOptions } from './options';
11+
import { PrettyHistogramSeriesRenderer } from './renderer';
12+
13+
export class PrettyHistogramSeries<
14+
HorzScaleItem = Time,
15+
TData extends (SeriesDataItemTypeMap<HorzScaleItem>['Custom'] & SingleValueData<HorzScaleItem>) = SeriesDataItemTypeMap<HorzScaleItem>['Custom'] & SingleValueData<HorzScaleItem>
16+
> implements ICustomSeriesPaneView<HorzScaleItem, TData, PrettyHistogramSeriesOptions> {
17+
private _renderer: PrettyHistogramSeriesRenderer<HorzScaleItem, TData>;
18+
19+
public constructor() {
20+
this._renderer = new PrettyHistogramSeriesRenderer();
21+
}
22+
23+
public priceValueBuilder(plotRow: TData): CustomSeriesPricePlotValues {
24+
return [plotRow.value];
25+
}
26+
27+
public isWhitespace(data: TData | WhitespaceData<HorzScaleItem>): data is WhitespaceData<HorzScaleItem> {
28+
return (data as Partial<TData>).value === undefined;
29+
}
30+
31+
public renderer(): PrettyHistogramSeriesRenderer<HorzScaleItem, TData> {
32+
return this._renderer;
33+
}
34+
35+
public update(
36+
data: PaneRendererCustomData<HorzScaleItem, TData>,
37+
options: PrettyHistogramSeriesOptions
38+
): void {
39+
this._renderer.update(data, options);
40+
}
41+
42+
public defaultOptions(): PrettyHistogramSeriesOptions {
43+
return defaultOptions;
44+
}
45+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { BitmapCoordinatesRenderingScope, CanvasRenderingTarget2D } from 'fancy-canvas';
2+
3+
import { CustomBarItemData, ICustomSeriesPaneRenderer, PaneRendererCustomData, PriceToCoordinateConverter } from 'lightweight-charts';
4+
5+
import { PrettyHistogramSeriesOptions } from './options';
6+
import { PrettyHistogramData } from './data';
7+
import { positionsBox } from '../../helpers/dimensions/positions';
8+
9+
interface PrettyHistogramBarItem {
10+
x: number;
11+
value: number;
12+
color: string;
13+
}
14+
15+
export class PrettyHistogramSeriesRenderer<HorzScaleItem, TData extends PrettyHistogramData<HorzScaleItem>> implements ICustomSeriesPaneRenderer {
16+
private _data: PaneRendererCustomData<HorzScaleItem, TData> | null = null;
17+
private _options: PrettyHistogramSeriesOptions | null = null;
18+
19+
public draw(
20+
target: CanvasRenderingTarget2D,
21+
priceConverter: PriceToCoordinateConverter
22+
): void {
23+
target.useBitmapCoordinateSpace((scope: BitmapCoordinatesRenderingScope) =>
24+
this._drawImpl(scope, priceConverter)
25+
);
26+
}
27+
28+
public update(
29+
data: PaneRendererCustomData<HorzScaleItem, TData>,
30+
options: PrettyHistogramSeriesOptions
31+
): void {
32+
this._data = data;
33+
this._options = options;
34+
}
35+
36+
private _drawImpl(
37+
renderingScope: BitmapCoordinatesRenderingScope,
38+
priceToCoordinate: PriceToCoordinateConverter
39+
): void {
40+
if (
41+
this._data === null ||
42+
this._data.bars.length === 0 ||
43+
this._data.visibleRange === null ||
44+
this._options === null
45+
) {
46+
return;
47+
}
48+
const options = this._options;
49+
const bars: PrettyHistogramBarItem[] = this._data.bars.map((bar: CustomBarItemData<HorzScaleItem, TData>) => {
50+
return {
51+
x: bar.x * renderingScope.horizontalPixelRatio,
52+
value: priceToCoordinate(bar.originalData.value!)!,
53+
color: bar.barColor ?? options.color,
54+
};
55+
});
56+
57+
const zeroCoordinate = priceToCoordinate(0) ?? 0;
58+
59+
const ctx = renderingScope.context;
60+
61+
let prevColor: string | null = null;
62+
ctx.beginPath();
63+
const width = Math.max(1, Math.round(0.01 * options.widthPercent * this._data.barSpacing * renderingScope.horizontalPixelRatio));
64+
const radius = Math.floor(options.radius * renderingScope.horizontalPixelRatio);
65+
bars.slice(this._data.visibleRange.from, this._data.visibleRange.to + 1).forEach((item: PrettyHistogramBarItem) => {
66+
const color = item.color;
67+
if (prevColor !== null && prevColor !== color) {
68+
ctx.fill();
69+
ctx.beginPath();
70+
}
71+
ctx.fillStyle = color;
72+
const yPositionBox = positionsBox(
73+
zeroCoordinate,
74+
item.value,
75+
renderingScope.verticalPixelRatio
76+
)
77+
const actualRadius = Math.floor(Math.min(radius, width / 2, Math.abs(yPositionBox.length)));
78+
const left = Math.round(item.x - width / 2);
79+
ctx.roundRect(
80+
left,
81+
yPositionBox.position,
82+
width,
83+
yPositionBox.length,
84+
item.value < zeroCoordinate ? [actualRadius, actualRadius, 0, 0] : [0, 0, actualRadius, actualRadius]
85+
);
86+
prevColor = color;
87+
});
88+
ctx.fill();
89+
}
90+
}

0 commit comments

Comments
 (0)