基于 CGraphic  用于分析数据数组(时间序列)之间相互关联的 PairPlot  图·进阶篇
📊

基于 CGraphic 用于分析数据数组(时间序列)之间相互关联的 PairPlot 图·进阶篇

(2/3)· 当多个货币对报价塞进同一面板,散点加柱形如何不糊成一团

案例拆解 第 2/3 篇
很多交易者手里攒了一堆货币对报价,却只会两两手动比对斜率。把序列一次性铺成交叉表,关联强弱一眼可辨,本篇接着讲怎么把底层图形块拼起来。

「直方图类的统计内核怎么写」

在 MT5 里做自定义直方图,核心不是画图而是先把数据分桶。下面这段 CHistogram 的成员声明给出了接口骨架:Create 负责在指定图表子窗口按坐标建对象,AddTimeserie 灌入序列,UpdateTimeserie 按索引刷新,SetCells 直接改桶数 i_cells。 CalculateHistogramArray 有个硬约束容易踩坑:样本量必须 ≥ 桶数 × 10,否则直接返回 false。比如 i_cells=20 时,data 长度至少得 200,否则直方图不绘制——这能避免小样本把分布画成噪声。 分桶逻辑本身不复杂:先取数组极小极大值得 range,width=range/i_cells;间隔中心取 minv+(i+0.5)*width,最后把频率乘 100.0/size 归一成百分比。末段 HistogramPlot 从曲线取 Y 值喂给该函数,失败就提前 return,直方图宽度至少锁 2 像素防不可见。 外汇与贵金属波动聚集,用这类直方图看回报分布时须记高风险:样本区间选错,分布形态可能完全误导仓位判断。

MQL5 / C++
class="type">bool Create(class="kw">const class="type">long chart,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwin,class="kw">const class="type">int x1,class="kw">const class="type">int y1,class="kw">const class="type">int x2,class="kw">const class="type">int y2, ENUM_HISTOGRAM_ORIENTATION orientation=HISTOGRAM_HORIZONTAL);
 class="type">int AddTimeserie(class="kw">const class="type">class="kw">double &timeserie[]);
 class="type">bool UpdateTimeserie(class="kw">const class="type">class="kw">double &timeserie[],class="type">uint timeserie=class="num">0);
 class="type">bool SetCells(class="type">uint value) { i_cells=value; }
class="kw">protected:
 class="kw">virtual class="type">void HistogramPlot(CCurve *curve);
 class="type">bool CalculateHistogramArray(class="kw">const class="type">class="kw">double &data[],class="type">class="kw">double &intervals[],class="type">class="kw">double &frequency[], class="type">class="kw">double &maxv,class="type">class="kw">double &minv);
};
class="type">bool CHistogram::CalculateHistogramArray(class="kw">const class="type">class="kw">double &data[],class="type">class="kw">double &intervals[],class="type">class="kw">double &frequency[], class="type">class="kw">double &maxv,class="type">class="kw">double &minv)
 {
 class="type">int size=ArraySize(data);
 if(size<(class="type">int)i_cells*class="num">10) class="kw">return (class="kw">false);
 minv=data[ArrayMinimum(data)];
 maxv=data[ArrayMaximum(data)];
 class="type">class="kw">double range=maxv-minv;
 class="type">class="kw">double width=range/i_cells;
 if(width==class="num">0) class="kw">return class="kw">false;
 ArrayResize(intervals,i_cells);
 ArrayResize(frequency,i_cells);
class=class="str">"cmt">//--- 设置间隔中心 
 for(class="type">uint i=class="num">0; i<i_cells; i++)
  {
  intervals[i]=minv+(i+class="num">0.5)*width;
  frequency[i]=class="num">0;
  }
class=class="str">"cmt">//--- 填充间隔和频率 
 for(class="type">int i=class="num">0; i<size; i++)
  {
  class="type">uint ind=class="type">int((data[i]-minv)/width);
  if(ind>=i_cells) ind=i_cells-class="num">1;
  frequency[ind]++;
  }
class=class="str">"cmt">//--- 把频率规范化为百分比
 for(class="type">uint i=class="num">0; i<i_cells; i++)
  frequency[i]*=(class="num">100.0/(class="type">class="kw">double)size);
 class="kw">return (true);
 }
CHistogram::HistogramPlot(CCurve *curve)
 {
 class="type">class="kw">double data[],intervals[],frequency[];
 class="type">class="kw">double max_value, min_value;
 curve.GetY(data);
 if(!CalculateHistogramArray(data,intervals,frequency,max_value,min_value))
  class="kw">return;
class=class="str">"cmt">//--- 柱形图参数
 class="type">int histogram_width=fmax(curve.HistogramWidth(),class="num">2);
class=class="str">"cmt">//--- 检查

直方图坐标轴的两种摆法

在 MT5 自定义指标里画频率直方图,先得防住空数组:若 frequency 或 intervals 长度为 0,直接 return,否则后面取 ArrayMaximum 会踩坑。 横向和纵向两种朝向,本质只是把 X/Y 轴职责对调。横向时 X 轴吃 intervals 的最小最大值,标签只留 3 个、格式 "%.0f";Y 轴从 0 到频率峰值,格式 "%.2f"。纵向反过来,Y 轴承载区间、X 轴承载频率。 画图前用 m_height-m_down 和 m_width-m_right 算出生图原点,再用 ScaleY(0.0)/ScaleX(0.0) 拿到零值屏幕坐标。循环里每根柱子先过 MathIsValidNumber 校验,无效就 continue 跳过。 横向柱体以区间坐标 xc 为中心向两侧各展 histogram_width/2,纵向则从 yc 向上下展。柱底默认贴 originalY,若 yc0 在有效范围内则贴 yc0。最后 FillRectangle 按计算出的对角坐标填充曲线色 clr。

MQL5 / C++
if(ArraySize(frequency)==class="num">0 || ArraySize(intervals)==class="num">0)
      class="kw">return;
class=class="str">"cmt">//---
   class="kw">switch(e_orientation)
     {
      case HISTOGRAM_HORIZONTAL:
         m_y.AutoScale(class="kw">false);
         m_x.Min(intervals[ArrayMinimum(intervals)]);
         m_x.Max(intervals[ArrayMaximum(intervals)]);
         m_x.MaxLabels(class="num">3);
         m_x.ValuesFormat("%.0f");
         m_y.Min(class="num">0);
         m_y.Max(frequency[ArrayMaximum(frequency)]);
         m_y.ValuesFormat("%.2f");
         class="kw">break;
      case HISTOGRAM_VERTICAL:
         m_x.AutoScale(class="kw">false);
         m_y.Min(intervals[ArrayMinimum(intervals)]);
         m_y.Max(intervals[ArrayMaximum(intervals)]);
         m_y.MaxLabels(class="num">3);
         m_y.ValuesFormat("%.0f");
         m_x.Min(class="num">0);
         m_x.Max(frequency[ArrayMaximum(frequency)]);
         m_x.ValuesFormat("%.2f");
         class="kw">break;
     }
class=class="str">"cmt">//---
   CalculateXAxis();
   CalculateYAxis();
class=class="str">"cmt">//--- 计算 y 的起点
   class="type">int originalY=m_height-m_down;
   class="type">int originalX=m_width-m_right;
   class="type">int yc0=ScaleY(class="num">0.0);
   class="type">int xc0=ScaleX(class="num">0.0);
class=class="str">"cmt">//--- 取得曲线颜色
   class="type">uint clr=curve.Color();
class=class="str">"cmt">//--- 画图 
   for(class="type">uint i=class="num">0; i<i_cells; i++)
     {
      class=class="str">"cmt">//--- 检查坐标
      if(!MathIsValidNumber(frequency[i]) || !MathIsValidNumber(intervals[i]))
         class="kw">continue;
      if(e_orientation==HISTOGRAM_HORIZONTAL)
        {
         class="type">int xc=ScaleX(intervals[i]);
         class="type">int yc=ScaleY(frequency[i]);
         class="type">int xc1 = xc - histogram_width/class="num">2;
         class="type">int xc2 = xc + histogram_width/class="num">2;
         class="type">int yc1 = yc;
         class="type">int yc2 = (originalY>yc0 && yc0>class="num">0) ? yc0 : originalY;
         class=class="str">"cmt">//---
         if(yc1>yc2)
            yc2++;
         else
            yc2--;
         class=class="str">"cmt">//---
         m_canvas.FillRectangle(xc1,yc1,xc2,yc2,clr);
        }
      else
        {
         class="type">int yc=ScaleY(intervals[i]);
         class="type">int xc=ScaleX(frequency[i]);
         class="type">int yc1 = yc - histogram_width/class="num">2;
         class="type">int yc2 = yc + histogram_width/class="num">2;
         class="type">int xc1 = xc;

◍ 画布坐标对齐与序列类封装

在 MT5 自定义指标里做图形叠加时,经常要先算好矩形两端的 x 坐标再交给画布。下面这段逻辑就是典型处理:当原始坐标 originalX 大于基准 xc0 且 xc0 为正时取 xc0,否则回落到 originalX;随后比较 xc1 与 xc2,前者大就给 xc2 加 1,否则减 1,最后用 FillRectangle 把区块涂满。 xc2 = (originalX>xc0 && xc0>0) ? xc0 : originalX; if(xc1>xc2) xc2++; else xc2--; m_canvas.FillRectangle(xc1,yc1,xc2,yc2,clr); 这一处 ±1 的微调很关键,它能避免两根坐标线重合时矩形宽度为 0 导致什么都不显示。实盘加载到 XAUUSD 的 M5 图,若 xc0 取 20、originalX 取 35,xc2 会落到 20 再按 xc1 大小偏移 1 像素。 真正可复用的是把行情数据收进 CTimeserie 类。它继承 CObject,内部存了品种、周期、应用价格和双精度数组 d_timeserie[],对外暴露 SetBars / Symbol / Timeframe / Price 四个 setter,每次改品种或周期都会把 dt_last_load 清 0,强制下次 UpdateTimeserie 重新拉数。 GetTimeserie 用 ArrayCopy 把内部数组拷出去,返回值大于 0 才算拿到数据。你在 EA 里调这个类,只要 Create("XAUUSD",PERIOD_M15,PRICE_CLOSE) 后周期切到 PERIOD_H1,dt_last_load 变 0,旧缓存不会污染新序列——外汇和贵金属杠杆高,重拉数据前务必确认品种权限已开通。

MQL5 / C++
class="type">int xc2 = (originalX>xc0 && xc0>class="num">0) ? xc0 : originalX;
class=class="str">"cmt">//---
if(xc1>xc2)
   xc2++;
else
   xc2--;
class=class="str">"cmt">//---
 m_canvas.FillRectangle(xc1,yc1,xc2,yc2,clr);
 }
  }
class=class="str">"cmt">//---
 }
class CTimeserie :  class="kw">public CObject
 {
class="kw">protected:
  class="type">class="kw">string                s_symbol;
  ENUM_TIMEFRAMES      e_timeframe;
  ENUM_APPLIED_PRICE   e_price;
  class="type">class="kw">double               d_timeserie[];
  class="type">int                  i_bars;
  class="type">class="kw">datetime             dt_last_load;

class="kw">public:
                     CTimeserie(class="type">void);
                    ~CTimeserie(class="type">void);
  class="type">bool               Create(class="kw">const class="type">class="kw">string symbol=NULL, class="kw">const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, class="kw">const ENUM_APPLIED_PRICE price=PRICE_CLOSE);
class=class="str">"cmt">//--- 改变时间序列的设置
  class="type">void               SetBars(class="kw">const class="type">int value)            {  i_bars=value;  }
  class="type">void               Symbol(class="type">class="kw">string value)                {  s_symbol=value;       dt_last_load=class="num">0;  }
  class="type">void               Timeframe(ENUM_TIMEFRAMES value)    {  e_timeframe=value;  dt_last_load=class="num">0;  }
  class="type">void               Price(ENUM_APPLIED_PRICE value)     {  e_price=value;       dt_last_load=class="num">0;  }
class=class="str">"cmt">//---
  class="type">class="kw">string             Symbol(class="type">void)                        {  class="kw">return s_symbol;    }
  ENUM_TIMEFRAMES    Timeframe(class="type">void)                     {  class="kw">return e_timeframe;  }
  ENUM_APPLIED_PRICE Price(class="type">void)                         {  class="kw">return e_price;      }
class=class="str">"cmt">//--- 载入数据
  class="kw">virtual class="type">bool       UpdateTimeserie(class="type">void);
  class="type">bool               GetTimeserie(class="type">class="kw">double ×erie[])   {  class="kw">return ArrayCopy(timeserie,d_timeserie)>class="num">0;  }
 };
class="type">bool CTimeserie::UpdateTimeserie(class="type">void)
  {

「按价格类型拉序列并算跳点差值」

这段逻辑先判断本地缓存是否还新鲜:用 SeriesInfoInteger 取当前品种当前周期最后一根 K 线时间,若上次加载时间不早于它、且已缓存数组长度够用,直接返回 true 跳过拷贝,能省掉不少重复 IO。 真正拉数据靠 switch 分流。PRICE_CLOSE / OPEN / HIGH / LOW 直接调 CopyClose 等函数,从偏移 1 开始取 i_bars+1 根;中位数、典型价、加权价则先 CopyRates 拿整包 MqlRates,再按 (H+L)/2、(H+L+C)/3、(H+L+2C)/4 逐根算进 data。注意 PRICE_OPEN 和 PRICE_HIGH 的 case 里漏了 break,会穿透执行,属于原代码隐患,移植时建议补上。 拿到 bars 后若小于等于 0 直接返回 false;否则把 dt_last_load 更新为 cur_date,并把 d_timeserie 重设成 bars-1 长度。最后用 SymbolInfoDouble 取 SYMBOL_POINT,循环算 (data[i+1]-data[i])/point,得到每根相对于前一根的「跳点数」序列,供后续波动率判定使用。外汇与贵金属杠杆高,这类差值序列只反映历史波动结构,实盘信号概率仍需自行验证。

MQL5 / C++
  class="type">class="kw">datetime cur_date=(class="type">class="kw">datetime)SeriesInfoInteger(s_symbol,e_timeframe,SERIES_LASTBAR_DATE);
  if(dt_last_load>=cur_date && ArraySize(d_timeserie)>=i_bars)
      class="kw">return true;
class=class="str">"cmt">//---
  class="type">MqlRates rates[];
  class="type">int bars=class="num">0,i;
  class="type">class="kw">double data[];
  class="kw">switch(e_price)
    {
     case PRICE_CLOSE:
         bars=CopyClose(s_symbol,e_timeframe,class="num">1,i_bars+class="num">1,data);
         class="kw">break;
     case PRICE_OPEN:
         bars=CopyOpen(s_symbol,e_timeframe,class="num">1,i_bars+class="num">1,data);
     case PRICE_HIGH:
         bars=CopyHigh(s_symbol,e_timeframe,class="num">1,i_bars+class="num">1,data);
     case PRICE_LOW:
         bars=CopyLow(s_symbol,e_timeframe,class="num">1,i_bars+class="num">1,data);
     case PRICE_MEDIAN:
         bars=CopyRates(s_symbol,e_timeframe,class="num">1,i_bars+class="num">1,rates);
         bars=ArrayResize(data,bars);
         for(i=class="num">0;i<bars;i++)
           data[i]=(rates[i].high+rates[i].low)/class="num">2;
         class="kw">break;
     case PRICE_TYPICAL:
         bars=CopyRates(s_symbol,e_timeframe,class="num">1,i_bars+class="num">1,rates);
         bars=ArrayResize(data,bars);
         for(i=class="num">0;i<bars;i++)
           data[i]=(rates[i].high+rates[i].low+rates[i].close)/class="num">3;
         class="kw">break;
     case PRICE_WEIGHTED:
         bars=CopyRates(s_symbol,e_timeframe,class="num">1,i_bars+class="num">1,rates);
         bars=ArrayResize(data,bars);
         for(i=class="num">0;i<bars;i++)
           data[i]=(rates[i].high+rates[i].low+class="num">2*rates[i].close)/class="num">4;
         class="kw">break;
    }
class=class="str">"cmt">//---
  if(bars<=class="num">0)
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
  dt_last_load=cur_date;
class=class="str">"cmt">//---
  if(ArraySize(d_timeserie)!=(bars-class="num">1) && ArrayResize(d_timeserie,bars-class="num">1)<=class="num">0)
      class="kw">return class="kw">false;
  class="type">class="kw">double point=SymbolInfoDouble(s_symbol,SYMBOL_POINT);
  for(i=class="num">0;i<bars-class="num">1;i++)
      d_timeserie[i]=(data[i+class="num">1]-data[i])/point;
class=class="str">"cmt">//---
  class="kw">return true;
  }

把砖块拼成 PairPlot 面板

做完了单个图形砖块,下一步是把它们塞进一个能挂在 MT5 对话框里的容器。CPairPlot 直接派生于 CWndClient,这样用标准 CAppDialog 搭的图形面板就能直接托管它,不用自己重写窗口消息循环。 类内部用 CPlotBase 指针数组存所有图形实例,CArrayObj 存时间序列指针,再配几个变量管时段、价格类型、历史深度和资产名颜色。对外只暴露 Create 和 Refresh:Create 要你传图表 ID、坐标、品种数组、时段、历史深度和柱形列数,进来先卡两道底线——品种数组为空或历史深度不达标直接返回 false,连图都不画。 图形表格按资产两两组合生成:同名资产交叉画柱形图,其余配对画散点图。Refresh 里先循环刷所有时间序列,注意配对图要求两侧数据可比,所以只要有一个序列更新失败就整体退出;传数据给图形时用 Update(false),意思是别顺手去刷主图,能明显压低终端负载、缩短函数执行时间。 图形元素基于 CGraphic 而非 CObject,所以基类里的 Shift、Show、Hide 必须在 CPairPlot 重写一遍,否则面板拖不动也藏不住。下面这段是类声明的骨架,private 里的 i_bars 默认传 1000、cells 默认 10,对应千根 K 线和十列分布。

MQL5 / C++
class CPairPlot : class="kw">public CWndClient
  {
class="kw">private:
   CPlotBase                  *m_arr_graphics[];
   CArrayObj                  m_arr_symbols;
   ENUM_TIMEFRAMES            e_timeframe;
   ENUM_APPLIED_PRICE         e_price;
   class="type">int                        i_total_symbols;
   class="type">uint                       i_bars;
   ENUM_HISTOGRAM_ORIENTATION e_orientation;
   class="type">uint                       i_text_color;

class="kw">public:
                        CPairPlot();
                       ~CPairPlot();
class=class="str">"cmt">//---
   class="type">bool      Create(class="kw">const class="type">long chart,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwin,class="kw">const class="type">int x1,class="kw">const class="type">int y1,class="kw">const class="type">int x2,class="kw">const class="type">int y2, class="kw">const class="type">class="kw">string &symbols[],class="kw">const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, class="kw">const class="type">int bars=class="num">1000, class="kw">const class="type">uint cells=class="num">10, class="kw">const ENUM_APPLIED_PRICE price=PRICE_CLOSE);
   class="type">bool      Refresh(class="type">void);
   class="type">bool      HistogramOrientation(ENUM_HISTOGRAM_ORIENTATION value);
   ENUM_HISTOGRAM_ORIENTATION HistogramOrientation(class="type">void)   {  class="kw">return e_orientation;  }
   class="type">bool      SetTextColor(class="type">color value);
class=class="str">"cmt">//--- 几何
   class="kw">virtual class="type">bool      Shift(class="kw">const class="type">int dx,class="kw">const class="type">int dy);
class=class="str">"cmt">//--- 状态
   class="kw">virtual class="type">bool      Show(class="type">void);
   class="kw">virtual class="type">bool      Hide(class="type">void);
   };

◍ 构建多品种关联矩阵的初始化逻辑

在 MT5 里做跨品种相关性热力图,第一步是把品种数组塞进自定义容器并校验底线参数。Create 函数要求传入品种名数组、时间框架、 bars 数量与单元格分辨率,其中 bars 默认 1000、单元格默认 10,低于 100 根 K 线直接返回 false,这是为了避免小样本下分布失真。 代码里先用 ArraySize 拿品种总数,若小于等于 1 或 bars 不达标立刻退出;随后给每个品种 new 一个 CTimeserie 并调用 Create 拉数据,任何一步失败都返回 false。品种数最终存进 i_total_symbols,若仍不足 2 个同样放弃构建。 图形层先把客户区窗口建出来,再按 i_total_symbols 的平方 resize 图形对象数组——比如 5 个品种就要 25 个格子。窗口宽高除以品种数得到单格尺寸,双重循环里对角线位置(i==j)走 CHistogram 分支画自身分布直方图,其余位置留作跨品种散点。 开 MT5 把这段贴进你的 CPairPlot 类,先传 EURUSD、GBPUSD、XAUUSD 三个符号跑通,留意 bars 设 100 以下会直接构造失败;外汇与贵金属波动剧烈,矩阵仅反映历史样本关联,实盘参考须自担高风险。

MQL5 / C++
class="type">bool CPairPlot::Create(class="kw">const class="type">long chart,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwin,class="kw">const class="type">int x1,class="kw">const class="type">int y1,class="kw">const class="type">int x2,class="kw">const class="type">int y2, class="kw">const class="type">class="kw">string &symbols[],class="kw">const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, class="kw">const class="type">int bars=class="num">1000, class="kw">const class="type">uint cells=class="num">10, class="kw">const ENUM_APPLIED_PRICE price=PRICE_CLOSE)
  {
   i_total_symbols=class="num">0;
   class="type">int total=ArraySize(symbols);
   if(total<=class="num">1 || bars<class="num">100)
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   e_timeframe=timeframe;
   i_bars=bars;
   e_price=price;
   for(class="type">int i=class="num">0;i<total;i++)
     {
       CTimeserie *temp=new CTimeserie;
       if(temp==NULL)
         class="kw">return class="kw">false;
       temp.SetBars(i_bars);
       if(!temp.Create(symbols[i],e_timeframe,e_price))
         class="kw">return class="kw">false;
       if(!m_arr_symbols.Add(temp))
         class="kw">return class="kw">false;
     }
   i_total_symbols=m_arr_symbols.Total();
   if(i_total_symbols<=class="num">1)
      class="kw">return class="kw">false;
   if(!CWndClient::Create(chart,name,subwin,x1,y1,x2,y2))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   if(ArraySize(m_arr_graphics)!=(i_total_symbols*i_total_symbols))
      if(ArrayResize(m_arr_graphics,i_total_symbols*i_total_symbols)<=class="num">0)
         class="kw">return class="kw">false;
   class="type">int width=Width()/i_total_symbols;
   class="type">int height=Height()/i_total_symbols;
   for(class="type">int i=class="num">0;i<i_total_symbols;i++)
     {
       CTimeserie *timeserie1=m_arr_symbols.At(i);
       if(timeserie1==NULL)
         class="kw">continue;
       for(class="type">int j=class="num">0;j<i_total_symbols;j++)
         {
           class="type">class="kw">string obj_name=m_name+"_"+(class="type">class="kw">string)i+"_"+(class="type">class="kw">string)j;
           class="type">int obj_x1=m_rect.left+j*width;
           class="type">int obj_x2=obj_x1+width;
           class="type">int obj_y1=m_rect.top+i*height;
           class="type">int obj_y2=obj_y1+height;
           if(i==j)
             {
               CHistogram *temp=new CHistogram();
               if(CheckPointer(temp)==POINTER_INVALID)
                 class="kw">return class="kw">false;
               if(!temp.Create(m_chart_id,obj_name,m_subwin,obj_x1,obj_y1,obj_x2,obj_y2,e_orientation))
                 class="kw">return class="kw">false;
             }
     }
  }
交给小布盯盘看盘口
这些多序列关联的初筛,小布盯盘的 AIGC 已内置,打开对应品种页即可看到散点簇与分布偏态,你只管判断要不要调仓。

常见问题

把画布边宽和坐标绘制收进基类方法,后续散点图、柱形图直接复用,不必每类图重写底层渲染。
目前内置的是关联初判视图,完整可交互的 CGraphic 面板仍需按本文基类自行组装,小布负责替你跑日常扫描。
柱形图展示单序列分布,散点图才暴露两序列协同偏移,趋势线作视觉参考更易看出线性相关方向。
以交叉表为骨架,按序列数动态生成行列表格,缺失对角位用分布柱形填补,保证可读性与统一表现。