✅作者简介:热爱科研的Matlab仿真开发者,修心和技术同步精进,matlab项目合作可私信。
🍎个人主页:Matlab科研工作室
🍊个人信条:格物致知。
⛄ 内容介绍
描述
Horizon Chart 是一种数据可视化技术,它以紧凑且分层的格式显示时间序列数据,提高可读性并能够随时间比较多个数据集。
依赖关系
为了获得最佳性能和准确的图表,请安装Mapping Toolbox。
句法
- HorizonChart 为每个切片创建 Horizon Chart x 轴数据和 y 轴数据
horizonChart(x, y)
创建一个包含 x 和 y 数据且 NumBands = 2 的水平图表,即在每个切片中,数据分为 2 个区域 - 一个位于基线上方,一个位于基线下方horizonChart(x, y, numBands)
使用 x 和 y 数据创建一个水平图表,其中波段数 = numBands。Number of Bands 是数据被划分为的部分的数量horizonChart(__ , Name, Value)
使用一个或多个名称-值对参数指定地平线图的其他选项。在所有其他输入参数之后指定选项。
名称-值对参数/属性
XData
XData 是一个二维矩阵,其中每列指定要在图表的各个切片中显示的数据的 x 坐标。XData
也可以是一维矩阵,它可以表示每个子图/单独切片的公共 x 值。YData
YData 是一个二维矩阵,其中每列指定要在图表的各个切片中显示的数据的 y 坐标。Labels
图表中每个切片的标签列表NumBands
用于划分地平线图表的波段/段数
风格名称-值对参数/属性
ColorAboveBaseline
它用于确定基线上方的条带着色的颜色梯度。ColorBelowBaseline
它用于确定基线以下的带被着色的颜色梯度XLabel
X 轴标签YLabel
Y 轴标签Title
地平线图的标题
⛄ 部分代码
classdef horizonChart < matlab.graphics.chartcontainer.ChartContainer & ...
matlab.graphics.chartcontainer.mixin.Legend
% horizonChart creates a Horizon Chart x-axis data and
% y-axis data for each slice
%
% horizonChart(x, y) creates a horizon chart with x an y data and
% NumBands = 2 i.e. in each of the slices, the data is divided into
% 2 regions - one above the baseline and one below the baseline
%
% horizonChart(x, y, numBands) creates a horizon chart with x and y data
% with number of bands = numBands. Number of Bands is the number of
% sections that the data is divided into.
%
% horizonChart(__ , Name, Value) specifies additional options for the horizon chart
% using one or more name-value pair arguments. Specify the options after all other
% input arguments.
properties
XData (:, :) double = [];
YData (:, :) double = [];
Labels (1, :) string = [];
NumBands double {mustBeGreaterThanOrEqual(NumBands, 1)} = 2;
XLabel string = "";
YLabel string = "";
Title string = "";
end
properties(Dependent)
Baseline double = 0;
ColorAboveBaseline {validatecolor} = [0, 0, 1];
ColorBelowBaseline {validatecolor} = [1, 0, 0];
end
properties(Access = private,Transient,NonCopyable)
PatchObjectListPerSlice (:, :) = [];
AxisHandlePerSlice (1, :) = [];
SegRanges (1, :) double = [];
NumSlices;
ColorMap (:, 3) double;
end
properties(Access = private)
Baseline_I = NaN ;
BaselineSetMethod string = "auto";
ColorAboveBaseline_I = NaN;
ColorAboveBaselineSetMethod = "auto";
ColorBelowBaseline_I = NaN;
ColorBelowBaselineSetMethod = "auto";
ContainsMappingToolbox matlab.lang.OnOffSwitchState = 'on';
end
methods
function obj = horizonChart(varargin)
% Intialize list of arguments
args = varargin;
leadingArgs = cell(0);
if numel(args) >= 2 && isnumeric(args{1}) ...
&& isnumeric(args{2})
x = args{1};
y = args{2};
if mod(numel(args), 2) == 1 && isnumeric(args{3})
% horizonChart(x, y, numBands)
numBands = args{3};
leadingArgs = [leadingArgs {'XData', x, 'YData', y , 'NumBands', numBands}];
args = args(4:end);
else
% horizonChart(x, y)
leadingArgs = [leadingArgs {'XData', x, 'YData', y }];
args = args(3:end);
end
else
if numel(args) < 2
error('Invalid Input Arguments. Too few inputs were provided. ')
else
error('The first two arguments are not numeric and do not conform to XData and YData definition')
end
end
% Combine positional arguments with name/value pairs
args = [leadingArgs args];
% Call superclass constructor method
obj@matlab.graphics.chartcontainer.ChartContainer(args{:});
end
end
methods(Access=protected)
function setup(obj)
if ~any(strcmp('Mapping Toolbox', {ver().Name}))
obj.ContainsMappingToolbox = 'off';
warning("Mapping Toolbox is not installed. " + ...
"This may lead to degraded performance of the horizon chart. " + ...
"Install Mapping Toolbox for better performance")
end
end
function update(obj)
[obj.XData, obj.YData] = transformInputData(obj.XData, obj.YData) ;
% Validate Inputs
validateInputs(obj.XData, obj.YData, obj.Labels);
% If Grid Layout is already defined then clear the layout
% during the update step
children = obj.getLayout().Children;
set(children, "Parent" , []);
% Clear all patch objects
obj.PatchObjectListPerSlice = [];
obj.AxisHandlePerSlice = [];
% Set GridLayout to be vertical layout
obj.NumSlices = size(obj.YData, 2);
obj.getLayout().GridSize = [obj.NumSlices 1];
title(obj.getLayout(), obj.Title);
xlabel(obj.getLayout(), obj.XLabel);
ylabel(obj.getLayout(), obj.YLabel);
% If user doesn't specify baseline we setup the baseline as the
% median of the data. If the user specifies baseline, we adjust
% the segment lengths to match the new baseline.
if obj.BaselineSetMethod == "auto"
obj.calculateSegmentsWithoutUserSetBaseline();
else
obj.calculateSegmentsWithUserSetBaseline();
end
for slice = 1:obj.NumSlices
% We obtain XData/ YData for each slice and calculate
% which band each data point belongs to
sliceXData = obj.XData(:, min(slice,size(obj.XData,2)))';
sliceYData = obj.YData(:, slice)';
binsPoint = binData(sliceYData, obj.SegRanges);
PatchObjectList = [];
order = [];
% Get axis for the current tile and set all the properties
ax = nexttile(obj.getLayout());
% Disable data tips
disableDefaultInteractivity(ax)
% Specify labels only if the user has specified labels for
% each slice
if slice <= numel(obj.Labels)
title(ax, obj.Labels(slice));
end
y_min_slice = max(obj.YData(:));
ax.XLim = [min(sliceXData), max(sliceXData)];
ax.YTickLabel = [];
ax.YTick = [];
polygonOrder = gobjects(0);
hold(ax, 'all')
% Calculate color map for all the bands
bgColor = get(ax, 'Color');
order = ax.ColorOrder;
if obj.ColorAboveBaselineSetMethod == "auto"
obj.ColorAboveBaseline_I = order(1, :);
end
if obj.ColorBelowBaselineSetMethod == "auto"
obj.ColorBelowBaseline_I = order(2, :);
end
obj.CalculateColorMap(bgColor);
% For each band we create a polygon that makes up the area
% of the band.
for band = 1:obj.NumBands
lower = obj.SegRanges(band);
upper = obj.SegRanges(band + 1);
color = obj.ColorMap(band, :);
% Calculate the vertices of the polygon depeding on
% whether it lies above the baseline/ or below the
% baseline
[x_vertices, y_vertices] = generatePolygonPoints(sliceXData, sliceYData, lower, upper, lower >= obj.Baseline_I, obj.ContainsMappingToolbox);
% Transform polygon and reflect it over the baseline
y_vertices = transformPolygon(y_vertices, lower, upper, obj.Baseline_I);
% Create the PatchObject for the band
PatchObject = patch(ax, x_vertices, y_vertices, color, 'DisplayName', num2str(lower) + " - " + num2str(upper));
% If x_vertices/ y_vertices are empty then we need to
% create an empty patch object
if numel(x_vertices) == 0
PatchObject = patch(ax, NaN, NaN, color, 'DisplayName', num2str(lower) + " - " + num2str(upper));
else
% Find minimum all transformed y data in a particular slice
y_min_slice = min(y_min_slice, min(y_vertices(:)));
end
% The bands that lie furthest from the baseline are
% displayed in the front. While the bands that are the
% closest to the baseline are displayed in the back
if lower >= obj.Baseline_I
polygonOrder = [PatchObject, polygonOrder];
else
polygonOrder = [polygonOrder, PatchObject];
end
PatchObjectList = [PatchObjectList, PatchObject];
end
if y_min_slice ~= obj.Baseline_I
ax.YLim(1) = y_min_slice;
end
ax.Children = polygonOrder;
hold(ax, 'off')
obj.PatchObjectListPerSlice = [obj.PatchObjectListPerSlice; PatchObjectList];
obj.AxisHandlePerSlice = [obj.AxisHandlePerSlice, ax];
end
cbh = obj.buildColorBar(obj.AxisHandlePerSlice(end));
cbh.Layout.Tile = 'east';
end
function CalculateColorMap(obj, backgroundColor)
% The color of a band is decided by whether it lies above or
% below the baseline. In case of bands that lie below the
% baseline, the lower bands have a darker shade of
% obj.colorsBelowBaseline. In case of bands that lie above the
% baseline, the upper bands have a darker shade of
% obj.colorsAboveBaseline
nBandsBelowBaseline = sum(obj.SegRanges(2:end)<=obj.Baseline_I);
nBandsAboveBaseline = obj.NumBands - nBandsBelowBaseline;
% Calculate color gradient for the bands below the baseline
alphas = fliplr(linspace(0.5, 1, nBandsBelowBaseline))';
colorsBelowBaseline = alphas .* obj.ColorBelowBaseline_I + (1 - alphas) .* backgroundColor;
% Calculate color gradient for the bands above the baseline
alphas = linspace(0.5, 1, nBandsAboveBaseline)';
colorsAboveBaseline = alphas .* obj.ColorAboveBaseline_I + (1 - alphas) .* backgroundColor;
obj.ColorMap = [colorsBelowBaseline; colorsAboveBaseline];
end
end
methods(Access = private)
function calculateSegmentsWithoutUserSetBaseline(obj)
% We divide the data into segments which contain equal amount of
% data. For eg: If NumBands = 5, the first segment contains 20%
% of the data. The second segment represents 20% - 40% of data
% and so on
obj.SegRanges = quantile(obj.YData(:), linspace(0, 1, obj.NumBands + 1));
if mod(obj.NumBands, 2) == 0
obj.Baseline_I = obj.SegRanges(obj.NumBands / 2 + 1);
else
obj.Baseline_I = obj.SegRanges((obj.NumBands + 1) / 2);
end
end
function calculateSegmentsWithUserSetBaseline(obj)
% In the first step, we divide data into segments using the
% method proposed above.
% Then we calculate segments below the baseline and above the
% baseline. We accordingly divide the data below/above the
% baseline using the newly found segments.
all_data = obj.YData(:);
max_data = max(all_data);
min_data = min(all_data);
if obj.Baseline_I >= max_data
nSegmentsAboveBaseline = 0;
nSegmentsBelowBaseline = obj.NumBands;
elseif obj.Baseline_I <= min_data
nSegmentsAboveBaseline = obj.NumBands;
nSegmentsBelowBaseline = 0;
else
segRanges = quantile(all_data, linspace(0, 1, obj.NumBands + 1));
nSegmentsBelowBaseline = find(obj.Baseline_I >= segRanges, 1, 'last');
if nSegmentsBelowBaseline == 0
nSegmentsBelowBaseline = nSegmentsBelowBaseline + 1;
elseif nSegmentsBelowBaseline == obj.NumBands
nSegmentsBelowBaseline = nSegmentsBelowBaseline - 1;
end
nSegmentsAboveBaseline = obj.NumBands- nSegmentsBelowBaseline;
end
dataBelowBaseline = all_data(all_data < obj.Baseline_I);
dataAboveBaseline = all_data(all_data > obj.Baseline_I);
segRangesBelowBaseline = [];
segRangesAboveBaseline = [];
if nSegmentsBelowBaseline ~= 0
segRangesBelowBaseline = quantile(dataBelowBaseline, linspace(0, 1, nSegmentsBelowBaseline + 1));
end
if nSegmentsAboveBaseline ~= 0
segRangesAboveBaseline = quantile(dataAboveBaseline, linspace(0, 1, nSegmentsAboveBaseline + 1));
end
obj.SegRanges = [segRangesBelowBaseline(1:nSegmentsBelowBaseline), obj.Baseline_I, segRangesAboveBaseline(2: nSegmentsAboveBaseline + 1)];
end
function cbh = buildColorBar(obj, ax)
% Build a colorbar where the length of each color is proportional
% to the ratio of the length of each segment
segLengths = obj.SegRanges(2:end) - obj.SegRanges(1: end - 1);
lengthRatios = segLengths / sum(segLengths);
lengthRatios = round(lengthRatios * 100);
tLength = sum(lengthRatios);
modifiedColorMap = colormap(repelem(obj.ColorMap, lengthRatios, 1));
cbh = colorbar(ax);
cumulLengthRatios = cumsum(lengthRatios) / tLength;
cbh.Ticks = [0, cumulLengthRatios];
cbh.TickLabels = num2cell(obj.SegRanges);
end
end
methods
function set.Baseline(obj, newBaseline)
obj.BaselineSetMethod = "manual";
obj.Baseline_I = newBaseline;
end
function baseline = get.Baseline(obj)
baseline = obj.Baseline_I;
end
function set.ColorAboveBaseline(obj, color)
obj.ColorAboveBaselineSetMethod = "manual";
obj.ColorAboveBaseline_I = validatecolor(color);
end
function colorAboveBaseline = get.ColorAboveBaseline(obj)
colorAboveBaseline = obj.ColorAboveBaseline_I;
end
function set.ColorBelowBaseline(obj, color)
obj.ColorBelowBaselineSetMethod = "manual";
obj.ColorBelowBaseline_I = validatecolor(color);
end
function colorBelowBaseline = get.ColorBelowBaseline(obj)
colorBelowBaseline = obj.ColorBelowBaseline_I;
end
end
end
function [x, y] = validateInputs(x, y, labels)
x_size = size(x);
y_size = size(y);
if x_size(1) == 0 || x_size(2) == 0 || y_size(1) == 0 || y_size(2) == 0
error("Horizon chart cannot be constructed with empty data");
end
if ~isreal(x) || ~isreal(y)
error("Chart does not work for complex data")
end
if x_size(1) ~= y_size(1)
error("Number of datapoints for each slice does not match between X and Y");
end
if ~validateIsIncreasing(x)
error("X values should be strictly monotonically increasing")
end
if x_size(2) > 1 && x_size(2) < y_size(2)
error("Number of slices for X-Data can either be 1 or need to match the Y-Data");
end
if numel(labels) ~= 0 && numel(labels) ~= y_size(2)
error("Size of Labels is incorrect. It should either be empty or equal to the number of slices/ 1st dimension of Y data");
end
end
function isIncreasing = validateIsIncreasing(x)
for slice = 1:size(x, 2)
if ~issorted(x(:, slice), 'strictascend')
isIncreasing = false;
return;
end
end
isIncreasing = true;
end
function resultAr = binData(data, bins)
resultAr = histcounts(data, bins);
end
function [x_data, y_data] = transformInputData(x_data, y_data)
if size(x_data, 1) == 1
x_data = x_data';
end
if size(y_data, 1) == 1
y_data = y_data';
end
end
function [x_vertices, y_vertices] = generatePolygonPoints(dataX, dataY, lower, upper, isSegmentOverBaseline, containsMappingToolbox)
if isSegmentOverBaseline
keep = dataY >= lower;
else
keep = dataY < upper;
end
xi = dataX(keep);
yi = dataY(keep);
yi(yi >= upper) = upper;
yi(yi < lower) = lower;
if containsMappingToolbox
[x_u, y_u] = polyxpoly(dataX, dataY, dataX, upper * ones(1, numel(dataX)));
[x_l, y_l] = polyxpoly(dataX, dataY, dataX, lower * ones(1, numel(dataX)));
xi = [xi, x_u', x_l'];
yi = [yi, y_u', y_l'];
end
[xi, idx] = sort(xi);
yi = yi(idx);
x_vertices = [xi fliplr(xi)];
if isSegmentOverBaseline
y_vertices = [yi ones(1, numel(xi)) * lower];
else
y_vertices = [yi ones(1, numel(xi)) * upper];
end
end
function yi = transformPolygon(yi, lower, upper, baseline)
if lower >= baseline
yi = yi - (lower - baseline);
else
yi = yi + (baseline - upper);
yi = baseline + abs(baseline - yi);
end
end
function [ y_data, x_data, labels] = GenerateRandomData(n)
x_data = [2000:2021];
y_data_all = [[791, 829, 881, 950, 1010, 1080, 1190, 1260, 1300, 1420, 1530, 1620, 1700, 1820, 1940, 2110, 2280, 2430, 2600, 2680, 2520, 2740],
[770, 830, 849, 931, 988, 1050, 1150, 1230, 1290, 1190, 1240, 1300, 1360, 1370, 1380, 1350, 1380, 1380, 1420, 1450, 1410, 1500],
[3980, 4010, 3990, 4080, 4150, 4220, 4300, 4360, 4310, 4040, 4210, 4230, 4270, 4370, 4370, 4430, 4480, 4550, 4590, 4580, 4370, 4450],
[2040, 2100, 2120, 2130, 2200, 2220, 2290, 2340, 2330, 2280, 2320, 2370, 2390, 2400, 2400, 2430, 2470, 2530, 2570, 2620, 2400, 259],
[1830, 1890, 1880, 1880, 1920, 1940, 1950, 2000, 1970, 1880, 1890, 1920, 1860, 1820, 1810, 1850, 1870, 1900, 1900, 1920, 1750, 1850]];
labels_all = ["Apple(AAPL)", "Microsoft(MSFT)", "Amazon(AMZN)", "Google(GOOGL)", "Coca Cola(KO)"];
y_data = y_data_all(1:n, :)';
labels = labels_all(1:n);
end
Horizon Chart
Data Intialization and Preprocessing
First, we need to read the data and do some inital preprocessing to ensure that y is a 2d matrix and that x is either a 2d matrix with same dimensions as y or that x is 1d matrix with same number of rows as y. Labels is the individual title for each slice in the tiled layout.
Note: All data is random and is for representation purposes only. The data is not indicative of real stock market data. Further, each column indicates the price of each stock for the years 2000 - 2021.
n_stocks = 5;
[y_data, x_data, labels] = GenerateRandomData(5);
Let us create a basic horizon chart
basicHorizonChart = horizonChart(x_data, y_data);
Let us add some labels to our chart. Here, each slice of the chart denotes the gdp of a country. So, we pass in country names as labels.
basicHorizonChart = horizonChart(x_data, y_data, 6, 'Labels', labels);
Color Scheme
The horizon chart's color scheme is split into two zones: above the baseline and below the baseline. The user is required to specify a color for each zone. The segments in the chart are colored with a darker shade of the specified color based on their distance from the baseline. To set colors for the segments above and below the baseline, we can..
coloredHorizonChart = horizonChart(x_data, y_data, 6, 'Labels', labels, 'ColorAboveBaseline' , 'g' , 'ColorBelowBaseline' , 'm', 'Baseline', 1800);
Baseline
We can also set the baseline according to our preferences. In this case, depending on distribution of data we accordingly rearrange the number of bands above the baseline/ below the baseline.
baselineHorizonChart = horizonChart(x_data, y_data , 6, 'Labels', labels, 'Baseline', 2000);
Labelling the chart
To decorate the chart, we can also add the X-Label, Y-Label and chart title.
basicHorizonChart = horizonChart(x_data, y_data, 6, 'Labels', labels, 'XLabel', 'Time(in years)', 'YLabel', 'Stock Price(In USD)', 'Title', 'Stock Price over the years');