Volume Scatter Plot [UAlgo]Volume Scatter Plot is a visual analytics tool that transforms recent candles into a two dimensional distribution of price and volume. Instead of plotting volume in the traditional way at the bottom of the chart, the script projects each recent bar as a point inside a custom scatter plot area drawn directly on the chart. This allows the user to study the relationship between traded volume and price location in a much more spatial and intuitive format.
Each point represents one candle from the selected lookback period. The vertical location of the point is taken from price, while the horizontal location is determined by that candle’s volume relative to all other bars inside the lookback window. As a result, the plot makes it possible to quickly see whether higher volume tends to cluster near higher prices, lower prices, or specific sections of the recent range.
The script also colors every point according to candle direction, which adds a simple but useful order flow style layer. Bullish candles are shown with one color and bearish candles with another, so the scatter plot can reveal not only the price and volume relationship, but also whether those clusters were formed more often by bullish or bearish candles.
To add structure, the indicator draws a framed plotting area with guide lines, then overlays a linear regression line across the cloud of points. This regression line gives the user a fast view of the overall relationship between volume and price. If enabled, deviation bands are also plotted above and below the regression line, which can help identify points that stand out from the average relationship.
In practical use, this script is useful for studying whether strong participation is appearing at premium or discount prices, whether extreme volume is clustering around certain regions, and whether recent bars are tightly aligned with the broader price volume relationship or scattered away from it.
🔹 Features
🔸 Price and Volume Scatter Mapping
The script converts each candle in the lookback period into a scatter point. Price controls vertical placement and volume controls horizontal placement. This creates a true two dimensional view of recent market behavior rather than a separate price chart and volume pane.
🔸 Bullish and Bearish Point Coloring
Each point is colored according to candle direction. Bullish candles use the bullish point color, while bearish candles use the bearish point color. This makes it easier to visually separate positive and negative participation inside the same distribution.
🔸 Custom Plot Area Overlay
The scatter plot is drawn in a dedicated chart area offset to the right of current price. The width of this area and the horizontal offset are both configurable, giving the user control over how large and how far away the visualization appears.
🔸 Flexible Scatter Symbols
The plotted points can use different characters such as circles, stars, squares, crosses, or other symbols. This helps users adapt the style of the plot to their preferred chart appearance.
🔸 Automatic Bounds Calculation
The indicator automatically calculates the highest price, lowest price, highest volume, and lowest volume across the current point set. These values are then used to scale the scatter plot so the entire distribution fits inside the frame cleanly.
🔸 Framed Axes and Mid Guides
The script draws the outer borders of the plot area as well as horizontal and vertical midpoint guides. This creates a clearer analytical space and makes it easier to judge where points sit relative to the full distribution.
🔸 Tooltip Enabled Data Points
Every scatter point includes a tooltip that shows the underlying price and volume values. This allows the user to inspect specific points directly on the chart without losing the visual overview.
🔸 Linear Regression Overlay
A regression line is calculated from the relationship between volume and price across the current dataset. This gives the user an immediate view of whether the cloud of points implies a positive, negative, or flat volume price relationship.
🔸 Optional Deviation Bands
When enabled, the script also draws lines one standard deviation above and below the regression line. These bands help visualize how tightly or loosely the scatter cloud is distributed around the fitted relationship.
🔸 Live Refresh on the Latest Bar
The plot refreshes on the latest bar and in realtime conditions, ensuring that the scatter map always reflects the most recent market state.
🔹 Calculations
1) Defining the Scatter Point and Plot Structures
type ScatterPoint
float price
float volume
color pt_color
int bar_time
type ScatterPlot
array points
int lookback
int width
int x_offset
float min_price
float max_price
float min_vol
float max_vol
array drawn_labels
array drawn_lines
This is the data model behind the whole indicator.
Each ScatterPoint stores one candle’s contribution to the plot. It contains:
the target price,
the candle volume,
the point color,
and the candle time.
The ScatterPlot structure stores the full plotting state:
the rolling point array,
the configuration values,
the current plot bounds,
and the labels and lines used for drawing.
So before any calculations begin, the script already has a full container for both the raw data and the visual objects used to display it.
2) Adding New Points Into the Rolling Dataset
method add_point(ScatterPlot this, float p, float v, color c, int t) =>
this.points.unshift(ScatterPoint.new(p, v, c, t))
if this.points.size() > this.lookback
this.points.pop()
This method manages the rolling dataset.
Every new bar creates a new ScatterPoint and inserts it at the front of the array with unshift() . That means the newest point is always stored first. If the array grows larger than the selected lookback size, the oldest point is removed from the end with pop() .
This gives the script a continuously updating point cloud that always contains only the most recent bars.
In practical terms, the scatter plot is always a moving window of recent market behavior rather than an ever growing history.
3) Choosing the Price and Color for Each Point
color bar_color = close >= open ? c_bull : c_bear
float target_price = hl2
data_plot.add_point(target_price, volume, bar_color, time)
This block explains how each bar is converted into a scatter point.
First, the script determines the point color from candle direction. If the close is above or equal to the open, the point uses the bullish color. Otherwise it uses the bearish color.
Second, the script chooses hl2 as the target price. That means the vertical position of each point is the midpoint of the bar’s high and low, not the close or the open. This is a useful choice because it represents the candle’s central traded location rather than only its final close.
Finally, the script sends that price, the candle volume, the directional color, and the time into the rolling plot dataset.
So each plotted point reflects:
where the candle sat in price,
how much volume it traded,
and whether it closed bullish or bearish.
4) Calculating the Plot Bounds
method calculate_bounds(ScatterPlot this) =>
float max_p = na
float min_p = na
float max_v = na
float min_v = na
if this.points.size() > 0
max_p := this.points.get(0).price
min_p := this.points.get(0).price
max_v := this.points.get(0).volume
min_v := this.points.get(0).volume
for p in this.points
if p.price > max_p
max_p := p.price
if p.price < min_p
min_p := p.price
if p.volume > max_v
max_v := p.volume
if p.volume < min_v
min_v := p.volume
This method scans all stored points and finds the extreme values needed for scaling.
It identifies:
the maximum price,
the minimum price,
the maximum volume,
and the minimum volume.
These values form the raw boundaries of the point cloud. Without them, the script would not know how to map prices and volumes into the framed plotting area.
So this is the normalization step that prepares the scatter plot for accurate positioning.
5) Adding a Small Price Margin Around the Point Cloud
float p_range = math.max(max_p - min_p, 0.0001)
this.max_price := max_p + (p_range * 0.05)
this.min_price := min_p - (p_range * 0.05)
this.max_vol := max_v
this.min_vol := min_v
After the raw bounds are found, the script adds a small vertical margin to the price range.
It computes the price span and then extends the upper and lower bounds by five percent of that range. This prevents the highest and lowest points from sitting directly on the frame border.
Volume bounds are stored without an added margin because they are used mainly for horizontal scaling.
In practical terms, this makes the plot easier to read and visually less cramped.
6) Mapping Volume Into Horizontal Position
method get_x_pos(ScatterPlot this, float vol, int current_bar) =>
float max_range = math.max((this.max_vol - this.min_vol), 0.0001)
float ratio = (vol - this.min_vol) / max_range
float active_width = this.width * 0.95
int pos = current_bar + this.x_offset + int(ratio * active_width)
pos
This function is what turns volume into horizontal placement.
First, it measures the full volume range across the current dataset. Then it converts the current point’s volume into a ratio between zero and one:
ratio = (vol - this.min_vol) / max_range
That ratio tells the script where the volume sits between the smallest and largest volume values in the lookback.
The ratio is then multiplied by the active plot width and shifted to the right of current price using x_offset .
So low volume points appear closer to the left side of the scatter area, and high volume points appear closer to the right side.
This is the key transformation that makes the chart behave like a true scatter plot rather than a simple time series.
7) Drawing the Plot Frame and Guides
method draw_axes(ScatterPlot this, int current_bar) =>
int x_start = current_bar + this.x_offset
int x_end = x_start + this.width
this.push_line(line.new(x_start, this.min_price, x_start, this.max_price, color = c_axis, style = line.style_dotted, width = 1))
this.push_line(line.new(x_end, this.min_price, x_end, this.max_price, color = c_axis, style = line.style_dotted, width = 1))
this.push_line(line.new(x_start, this.min_price, x_end, this.min_price, color = c_axis, style = line.style_dotted, width = 1))
this.push_line(line.new(x_start, this.max_price, x_end, this.max_price, color = c_axis, style = line.style_dotted, width = 1))
int mid_x = x_start + math.round(this.width / 2)
float mid_y = (this.max_price + this.min_price) / 2
this.push_line(line.new(mid_x, this.min_price, mid_x, this.max_price, color = color.new(c_axis, 70), style = line.style_dashed, width = 1))
this.push_line(line.new(x_start, mid_y, x_end, mid_y, color = color.new(c_axis, 70), style = line.style_dashed, width = 1))
This method draws the visual frame of the scatter plot.
It defines the left and right horizontal edges of the plotting area, then draws four dotted boundary lines:
left border,
right border,
bottom border,
and top border.
After that, it draws a vertical midpoint guide and a horizontal midpoint guide.
These guides help the user interpret where the point cloud sits relative to the full price and volume range. For example, it becomes much easier to see whether most points cluster in the upper half of price or the right half of volume.
8) Drawing Axis Labels
this.push_label(label.new(x_start, this.max_price, "Price Max", textcolor = c_axis, color = transparent, style = label.style_label_down, size = size.small))
this.push_label(label.new(x_start, this.min_price, "Price Min", textcolor = c_axis, color = transparent, style = label.style_label_up, size = size.small))
this.push_label(label.new(x_end, this.min_price, "Vol Max", textcolor = c_axis, color = transparent, style = label.style_label_left, size = size.small))
These labels give the plot basic orientation.
The script marks:
the highest price boundary,
the lowest price boundary,
and the far right side of the plot as the maximum volume direction.
This is a simple but useful usability feature because it immediately tells the user how to read the scatter space:
vertical movement corresponds to price,
and movement toward the right corresponds to increasing volume.
9) Drawing the Scatter Points Themselves
method draw_points(ScatterPlot this, int current_bar, string char_symbol) =>
color transparent = color.new(color.white, 100)
for p in this.points
int x_pos = this.get_x_pos(p.volume, current_bar)
string tooltip_txt = "P: " + str.tostring(p.price, format.mintick) + " V: " + str.tostring(p.volume, format.volume)
this.push_label(label.new(x_pos, p.price, text = char_symbol, textcolor = p.pt_color, color = transparent, style = label.style_none, size = size.small, tooltip = tooltip_txt))
This method plots every stored point inside the scatter area.
For each point, the script first converts volume into an x position using get_x_pos() . The y position is simply the stored point price. Then it draws a label using the selected point symbol and the stored bullish or bearish color.
Each point also gets a tooltip showing:
the exact price,
and the exact volume.
So visually, the user sees a clean scatter cloud, but each point still preserves its detailed numeric information.
10) Computing the Regression Line
method draw_regression(ScatterPlot this, int current_bar, bool show_dev) =>
int n = this.points.size()
if n > 1
float sum_x = 0.0
float sum_y = 0.0
float sum_xy = 0.0
float sum_xx = 0.0
for p in this.points
sum_x += p.volume
sum_y += p.price
sum_xy += p.volume * p.price
sum_xx += p.volume * p.volume
float denom = (n * sum_xx - sum_x * sum_x)
float slope = denom == 0 ? 0 : (n * sum_xy - sum_x * sum_y) / denom
float intercept = (sum_y - slope * sum_x) / n
This is the statistical core of the indicator.
The script performs a standard linear regression where:
x is volume,
and y is price.
It first accumulates the sums needed for the regression formula:
sum of x,
sum of y,
sum of xy,
and sum of xx.
From those totals, it calculates:
the slope,
and the intercept.
So the regression line answers a simple analytical question:
as volume changes across the recent dataset, what is the average linear relationship with price?
A positive slope suggests higher volume tends to align with higher prices.
A negative slope suggests higher volume tends to align with lower prices.
A flat slope suggests little directional relationship between the two.
11) Measuring Dispersion Around the Regression
float variance = 0.0
for p in this.points
float expected_y = slope * p.volume + intercept
variance += math.pow(p.price - expected_y, 2)
float std_dev = math.sqrt(variance / n)
After the regression line is found, the script measures how far the actual points deviate from that fitted relationship.
For each point, it calculates the expected price on the regression line for that point’s volume. It then measures the squared difference between the actual price and the expected price. The average of those squared differences becomes the variance, and the square root of that value becomes the standard deviation.
This tells the user how tightly or loosely the point cloud clusters around the regression line. A small deviation means the relationship is relatively consistent. A large deviation means the cloud is more dispersed.
12) Converting the Regression Into Drawable Chart Coordinates
float y_min_vol = slope * this.min_vol + intercept
float y_max_vol = slope * this.max_vol + intercept
int x_start_clamped = this.get_x_pos(this.min_vol, current_bar)
int x_end_clamped = this.get_x_pos(this.max_vol, current_bar)
this.push_line(line.new(x_start_clamped, y_min_vol, x_end_clamped, y_max_vol, color = c_reg, width = 2, style = line.style_solid))
This block translates the regression model into something the chart can display.
The script evaluates the regression line at the minimum and maximum volume values of the dataset. Those two calculated prices define the start and end of the regression segment in price space.
Then it converts the minimum and maximum volumes into actual chart x positions using get_x_pos() .
Finally, it draws a straight line between those two points.
So even though the regression is calculated in price and volume coordinates, it becomes a visible line inside the custom scatter plot area.
13) Drawing the Deviation Bands
if show_dev
color dev_color = color.new(c_reg, 60)
this.push_line(line.new(x_start_clamped, y_min_vol + std_dev, x_end_clamped, y_max_vol + std_dev, color = dev_color, width = 1, style = line.style_dashed))
this.push_line(line.new(x_start_clamped, y_min_vol - std_dev, x_end_clamped, y_max_vol - std_dev, color = dev_color, width = 1, style = line.style_dashed))
If deviation display is enabled, the script draws two additional dashed lines:
one standard deviation above the regression,
and one standard deviation below it.
These bands help the user judge whether points are staying close to the average relationship or whether some bars are standing far away from the expected line.
In practical terms, points well outside these bands can be interpreted as unusually strong or unusually weak price locations relative to their traded volume.
14) Clearing and Redrawing the Plot
method clear_drawings(ScatterPlot this) =>
if this.drawn_labels.size() > 0
for l in this.drawn_labels
l.delete()
this.drawn_labels.clear()
if this.drawn_lines.size() > 0
for b in this.drawn_lines
b.delete()
this.drawn_lines.clear()
Before each refresh, the script deletes all previously drawn labels and lines. This ensures that the scatter plot does not accumulate stale points or outdated regression segments.
Because the visualization is rebuilt from the current rolling dataset, clearing old drawings first is necessary for a clean and accurate live display.
15) Final Execution Flow
if barstate.islast or barstate.isrealtime
data_plot.clear_drawings()
data_plot.calculate_bounds()
data_plot.draw_axes(bar_index)
data_plot.draw_points(bar_index, i_pt_char)
data_plot.draw_regression(bar_index, i_show_dev)
This block summarizes the entire display engine.
On the latest bar or in realtime:
the script clears old drawings,
recalculates the current bounds,
draws the plot frame,
plots all scatter points,
and overlays the regression line with optional deviation bands.
So the chart always shows a fresh snapshot of the current price volume relationship based on the selected lookback window.
Pine Script®指标






















