✅作者简介:热爱科研的Matlab仿真开发者,修心和技术同步精进,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 endendfunction [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"); endendfunction isIncreasing = validateIsIncreasing(x) for slice = 1:size(x, 2) if ~issorted(x(:, slice), 'strictascend') isIncreasing = false; return; end end isIncreasing = true;endfunction resultAr = binData(data, bins) resultAr = histcounts(data, bins);endfunction [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'; endend 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]; endendfunction yi = transformPolygon(yi, lower, upper, baseline) if lower >= baseline yi = yi - (lower - baseline); else yi = yi + (baseline - upper); yi = baseline + abs(baseline - yi); endend
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 ChartData Intialization and PreprocessingFirst, 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 chartbasicHorizonChart = 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 SchemeThe 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);BaselineWe 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 chartTo 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');
⛄ 运行结果