Skip to content

Commit c55f9a9

Browse files
Merge branch 'main' into implementation/cat-box-strip/makie
2 parents 5e64937 + c5b10ae commit c55f9a9

6 files changed

Lines changed: 1708 additions & 0 deletions

File tree

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
// anyplot.ai
2+
// candlestick-volume: Stock Candlestick Chart with Volume
3+
// Library: highcharts 12.6.0 | JavaScript 22.23.2
4+
// Quality: 89/100 | Created: 2026-09-02
5+
6+
// The core bundle (no highcharts-more / modules/stock) has no "candlestick"
7+
// series — that type ships in modules/stock.js, which isn't loaded. Each
8+
// candle is built from an invisible "Floor" column stacked under a
9+
// colorByPoint "Body" column for the open-close range, with per-direction
10+
// "line" series (null-separated, one pair per candle) for the high-low wicks
11+
// — pure core series types, no add-on module. The volume pane is a second
12+
// yAxis (top/height split of the same plot area) sharing the chart's single
13+
// xAxis, so the built-in crosshair and vertical gridlines span both panes
14+
// automatically — no cross-pane sync code needed.
15+
const t = window.ANYPLOT_TOKENS;
16+
17+
// --- Data (in-memory, deterministic mulberry32 PRNG) ------------------------
18+
function mulberry32(seed) {
19+
return function () {
20+
seed |= 0;
21+
seed = (seed + 0x6d2b79f5) | 0;
22+
let z = Math.imul(seed ^ (seed >>> 15), 1 | seed);
23+
z = (z + Math.imul(z ^ (z >>> 7), 61 | z)) ^ z;
24+
return ((z ^ (z >>> 14)) >>> 0) / 4294967296;
25+
};
26+
}
27+
const rand = mulberry32(20240601);
28+
29+
const upColor = t.palette[0]; // #009E73 brand green — bullish (profit/up)
30+
const downColor = t.palette[4]; // #AE3030 matte red — bearish (loss/down), finance semantic anchor
31+
32+
const dayMs = 24 * 3600 * 1000;
33+
const numDays = 45; // continuous daily bars — crypto trades every calendar day
34+
const startTime = Date.UTC(2024, 5, 1); // Sat 1 Jun 2024
35+
const dates = Array.from({ length: numDays }, (_, i) => startTime + i * dayMs);
36+
37+
let price = 3200; // ETH-style token price in USD
38+
const candles = dates.map((time) => {
39+
const open = price;
40+
const drift = (rand() - 0.5) * 90;
41+
const close = Math.max(50, open + drift);
42+
const swing = Math.abs(close - open) + rand() * 40 + 12;
43+
const high = Math.max(open, close) + rand() * swing * 0.4;
44+
const low = Math.max(10, Math.min(open, close) - rand() * swing * 0.4);
45+
const bullish = close >= open;
46+
const volume = Math.round(420000 + Math.abs(close - open) * 9000 + rand() * 160000);
47+
price = close;
48+
return {
49+
time,
50+
open: +open.toFixed(2),
51+
high: +high.toFixed(2),
52+
low: +low.toFixed(2),
53+
close: +close.toFixed(2),
54+
volume,
55+
bullish,
56+
};
57+
});
58+
59+
const candleWidth = 9;
60+
61+
const floorData = candles.map((c) => ({ x: c.time, y: +Math.min(c.open, c.close).toFixed(2) }));
62+
const bodyData = candles.map((c) => ({
63+
x: c.time,
64+
y: +Math.abs(c.close - c.open).toFixed(2),
65+
color: c.bullish ? upColor : downColor,
66+
custom: c,
67+
}));
68+
const wickUpData = candles
69+
.filter((c) => c.bullish)
70+
.flatMap((c) => [{ x: c.time, y: c.low }, { x: c.time, y: c.high }, { x: c.time, y: null }]);
71+
const wickDownData = candles
72+
.filter((c) => !c.bullish)
73+
.flatMap((c) => [{ x: c.time, y: c.low }, { x: c.time, y: c.high }, { x: c.time, y: null }]);
74+
const volumeData = candles.map((c) => ({ x: c.time, y: c.volume, color: c.bullish ? upColor : downColor }));
75+
76+
const allLows = candles.map((c) => c.low);
77+
const allHighs = candles.map((c) => c.high);
78+
const pricePad = (Math.max(...allHighs) - Math.min(...allLows)) * 0.08;
79+
const priceMin = Math.floor(Math.min(...allLows) - pricePad);
80+
const priceMax = Math.ceil(Math.max(...allHighs) + pricePad);
81+
82+
// --- Chart -------------------------------------------------------------------
83+
Highcharts.chart("container", {
84+
chart: {
85+
type: "column",
86+
backgroundColor: "transparent",
87+
animation: false,
88+
style: { fontFamily: "inherit" },
89+
},
90+
credits: { enabled: false },
91+
colors: t.palette,
92+
title: {
93+
text: "candlestick-volume · javascript · highcharts · anyplot.ai",
94+
style: { color: t.ink, fontSize: "22px", fontWeight: "600" },
95+
},
96+
xAxis: {
97+
type: "datetime",
98+
lineColor: t.inkSoft,
99+
tickColor: t.inkSoft,
100+
gridLineColor: t.grid,
101+
gridLineWidth: 1,
102+
crosshair: { color: t.inkSoft, dashStyle: "Dash", width: 1 },
103+
labels: { style: { color: t.inkSoft, fontSize: "14px" } },
104+
title: { text: "Trading Date", style: { color: t.inkSoft, fontSize: "16px" } },
105+
},
106+
yAxis: [
107+
{
108+
// Price pane — top 68% of the plot area
109+
top: "0%",
110+
height: "68%",
111+
min: priceMin,
112+
max: priceMax,
113+
reversedStacks: false, // keep the invisible "Floor" series at the bottom of the stack
114+
title: { text: "Price (USD)", style: { color: t.inkSoft, fontSize: "16px" } },
115+
gridLineColor: t.grid,
116+
lineColor: t.inkSoft,
117+
labels: { style: { color: t.inkSoft, fontSize: "14px" } },
118+
},
119+
{
120+
// Volume pane — bottom 27%, a 5% gap separates it from the price pane
121+
top: "73%",
122+
height: "27%",
123+
offset: 0,
124+
min: 0,
125+
title: { text: "Volume", style: { color: t.inkSoft, fontSize: "16px" } },
126+
gridLineColor: t.grid,
127+
lineColor: t.inkSoft,
128+
labels: {
129+
style: { color: t.inkSoft, fontSize: "14px" },
130+
formatter() {
131+
return this.value >= 1000000
132+
? Highcharts.numberFormat(this.value / 1000000, 1).replace(/\.0$/, "") + "M"
133+
: Highcharts.numberFormat(this.value / 1000, 0) + "k";
134+
},
135+
},
136+
},
137+
],
138+
legend: {
139+
enabled: true,
140+
itemStyle: { color: t.inkSoft, fontSize: "14px" },
141+
itemHoverStyle: { color: t.ink },
142+
symbolRadius: 6,
143+
itemDistance: 24,
144+
padding: 12,
145+
},
146+
tooltip: {
147+
shared: true,
148+
outside: false,
149+
formatter: function () {
150+
const bodyPoint = this.points && this.points.find((p) => p.series.name === "Body");
151+
if (!bodyPoint) return false;
152+
const c = bodyPoint.point.custom;
153+
const volumePoint = this.points.find((p) => p.series.name === "Volume");
154+
let html =
155+
`<b>${Highcharts.dateFormat("%b %e, %Y", c.time)}</b><br/>` +
156+
`Open: ${c.open.toFixed(2)}<br/>High: ${c.high.toFixed(2)}<br/>` +
157+
`Low: ${c.low.toFixed(2)}<br/>Close: ${c.close.toFixed(2)}`;
158+
if (volumePoint) html += `<br/>Volume: ${Highcharts.numberFormat(volumePoint.y, 0)}`;
159+
return html;
160+
},
161+
},
162+
plotOptions: {
163+
series: { animation: false },
164+
column: { borderRadius: 0, animation: false },
165+
},
166+
series: [
167+
{
168+
name: "Floor",
169+
data: floorData,
170+
yAxis: 0,
171+
stacking: "normal",
172+
stack: "price",
173+
pointWidth: candleWidth,
174+
color: "transparent",
175+
borderWidth: 0,
176+
enableMouseTracking: false,
177+
showInLegend: false,
178+
},
179+
{
180+
type: "line",
181+
name: "Wicks (up)",
182+
data: wickUpData,
183+
yAxis: 0,
184+
color: upColor,
185+
lineWidth: 1.5,
186+
marker: { enabled: false },
187+
enableMouseTracking: false,
188+
showInLegend: false,
189+
},
190+
{
191+
type: "line",
192+
name: "Wicks (down)",
193+
data: wickDownData,
194+
yAxis: 0,
195+
color: downColor,
196+
lineWidth: 1.5,
197+
marker: { enabled: false },
198+
enableMouseTracking: false,
199+
showInLegend: false,
200+
},
201+
{
202+
name: "Body",
203+
data: bodyData,
204+
yAxis: 0,
205+
stacking: "normal",
206+
stack: "price",
207+
pointWidth: candleWidth,
208+
borderColor: t.pageBg,
209+
borderWidth: 1,
210+
showInLegend: false,
211+
},
212+
{
213+
name: "Volume",
214+
data: volumeData,
215+
yAxis: 1,
216+
pointWidth: candleWidth,
217+
borderWidth: 0,
218+
showInLegend: false,
219+
},
220+
{
221+
name: "Bullish (Close ≥ Open)",
222+
data: [],
223+
color: upColor,
224+
showInLegend: true,
225+
},
226+
{
227+
name: "Bearish (Close < Open)",
228+
data: [],
229+
color: downColor,
230+
showInLegend: true,
231+
},
232+
],
233+
});

0 commit comments

Comments
 (0)