diff --git a/BarChartRace2D.gif b/BarChartRace2D.gif new file mode 100644 index 0000000..e99c6b2 Binary files /dev/null and b/BarChartRace2D.gif differ diff --git a/BarChartRace3D.gif b/BarChartRace3D.gif new file mode 100755 index 0000000..ca4d2c7 Binary files /dev/null and b/BarChartRace3D.gif differ diff --git a/BarChartRace3D.m b/BarChartRace3D.m new file mode 100755 index 0000000..eac7c82 --- /dev/null +++ b/BarChartRace3D.m @@ -0,0 +1,288 @@ +classdef BarChartRace3D < handle + % Copyright 2021 The MathWorks, Inc. + + properties (Access=public) + colors; %colors for bars + labels; %labels for bars + title; %title + ticks; %horizontal ticks + positiveOnly; %all values are positive. Use whole canvas + animationFrames; %number of frames for animation. 3d always whole canvas + show3D; %3D barChartRace + axisColor; %axisColor + outfile; %animation gif output + end + + % private properties for internal use + properties (Access=private) + xlimit; + ylimit; + barWidth; + maxColors; + numBars; + dataCount; + gap; + + WidthLeft; + EdgeLeft; + end + + methods (Access=public) + function bcr = BarChartRace3D() + bcr.xlimit = [-400 400]; + bcr.ylimit = [-300 300]; + bcr.barWidth = 30; + bcr.gap = 10; + % animate 24 frames between each step + bcr.animationFrames = 24; + bcr.positiveOnly = false; % default to showing both + and - + % initialize default colors to be used + % user can overwrite later + bcr.colors = [ + 1 0 0; ... + 0 1 0; ... + 0 0 1; ... + 1 1 0; ... + 1 0 1; ... + 0 1 1; ... + 1 0.64 0; ... + 0 0 0.55; ... + 0.5 0 0; ... + 0.7 0.8 1; ... + 1 0.71 0.76; ... + 0.9 0.9 0.98]; + bcr.WidthLeft = 30; + bcr.EdgeLeft = 5; + bcr.axisColor = [0.8 0.8 0.8]; + bcr.outfile = 'BarChartRace3D.gif'; + end + + function preDraw(obj, data) + [obj.maxColors, ~] = size(obj.colors); + [obj.dataCount, obj.numBars] = size(data); + if obj.numBars > 8 + obj.barWidth = 30; + else + obj.barWidth = 50; + end + end + + function xScale = getXscale(obj, data, i) + if i > obj.dataCount + xScale = 1; + return; + end + data1 = data(i, :); + max1 = max(abs(data1)); + + lengthMax = 350 - 110; + if obj.positiveOnly || obj.show3D + lengthMax = lengthMax * 2; + end + xScale = lengthMax/max1; + end + + function [xScale1, xScale2] = computeXscales(obj, data, i) + xScale1 = obj.getXscale(data, i); + if i >= obj.dataCount + % last sample + xScale2 = xScale1; + else + xScale2 = obj.getXscale(data, i + 1); + end + end + + function yPos = getYpos(obj, yIndex) + if obj.show3D + if yIndex >= 5 + yleft = -(obj.WidthLeft/2 + (yIndex - 5)*(obj.WidthLeft + obj.EdgeLeft)); + else + yleft = (obj.EdgeLeft + obj.WidthLeft/2) + (4 - yIndex)*(obj.WidthLeft + obj.EdgeLeft); + end + yPos = yleft; + else + yPos = 250 - (obj.barWidth + obj.gap)*(yIndex - 1); + end + end + + function drawX(obj) + if obj.show3D + plot([-290 260], [-138 -273], 'Color', obj.axisColor); + plot([-290+obj.WidthLeft 260+60], [-138 -273], 'Color', obj.axisColor); + plot([260 260+60], [-273 -273], 'Color', obj.axisColor); + else + plot([-290 400], [-250 -250], 'Color', obj.axisColor); + end + end + + function drawY(obj) + if obj.show3D + plot([-290 -290], [-138 140], 'Color', obj.axisColor); + plot([-290+obj.WidthLeft -290+obj.WidthLeft], [-138 140], 'Color', [0.95 0.95 0.95]); + plot([-290 -290+obj.WidthLeft], [-138 -138], 'Color', [0.90 0.90 0.90]); + else + if obj.positiveOnly + plot([-290 -290], [295 -250], 'Color', obj.axisColor); + else + plot([0 0], [295 -250], 'Color', obj.axisColor); + end + end + end + + function drawTitle(obj) + if ~isempty(obj.title) + text(0, -295, obj.title, 'FontSize', 18, 'HorizontalAlignment','center'); + end + end + + function drawBar(obj, yPos, xTop, i, value) + + color = obj.colors(mod(i-1, obj.maxColors)+1, :); + edgeColor = arrayfun(@(x) (x - 0.5)*(x>0.5), color); + + if obj.positiveOnly + x = [-290 xTop-290 xTop-290 -290]; + else + x = [0 xTop xTop 0]; + end + y = [yPos - obj.barWidth/2 yPos - obj.barWidth/2 yPos + obj.barWidth/2 yPos + obj.barWidth/2]; + h = fill(x, y, color, 'EdgeColor', edgeColor); + set(h,'facealpha',.5); + if i < length(obj.labels) + lbl = obj.labels{i}; + text(-295, yPos, lbl, 'HorizontalAlignment', 'right', 'Color', edgeColor); + text(x(2) + 5, yPos, num2str(value), 'Color', edgeColor); + end + end + + function animateBar(obj, xInit, xEnd, yInit, yEnd, fStep, i, vInit, vEnd) + if fStep > obj.animationFrames + return; + end + + xPos = xInit + (xEnd - xInit)*fStep/obj.animationFrames; + yPos = yInit + (yEnd - yInit)*fStep/obj.animationFrames; + value = vInit + (vEnd - vInit)*fStep/obj.animationFrames; + + if obj.show3D + obj.drawBar3D(yPos, xPos, i, value); + else + obj.drawBar(yPos, xPos, i, value); + end + end + + + function race(obj, data) + [p, f, ~] = fileparts(obj.outfile); + obj.outfile = fullfile(p, [f '.gif']); + + firstFrame = true; + + obj.preDraw(data); + for step = 1:obj.dataCount - 1 + data_cur = data(step, :); + [~, yCurs] = sort(data_cur, 'descend'); + + data_next = data(step + 1, :); + [~, yNexts] = sort(data_next, 'descend'); + + [xScale1, xScale2] = obj.computeXscales(data, step); + for fStep = 1:obj.animationFrames + clf + axis off + hold on + xlim(obj.xlimit); + ylim(obj.ylimit); + + obj.drawX(); + obj.drawY(); + obj.drawTitle(); + for barOrder = 1:obj.numBars + % + yInit = obj.getYpos(barOrder); + barIndex = yCurs(barOrder); + endIndex = find(yNexts == barIndex); + yEnd = obj.getYpos(endIndex); + + xInit = data_cur(barIndex)*xScale1; + xEnd = data_next(barIndex)*xScale2; + + vInit = data_cur(barIndex); + vEnd = data_next(barIndex); + + obj.animateBar(xInit, xEnd, yInit, yEnd, fStep, barIndex, vInit, vEnd); + end + + [img, map] = rgb2ind(frame2im( getframe(gcf)),256); + if firstFrame + imwrite(img,map,obj.outfile,'gif','DelayTime',0.5); + firstFrame = false; + else + imwrite(img,map,obj.outfile,'gif','writemode', 'append','delaytime',1/obj.animationFrames); + end + hold off + end + + end + + end + + + function drawBar3D(obj, yPos, xTop, i, value) + yleft = yPos; + xLen = xTop; + + yright = BarChartRace3D.getRightY(yleft, xLen); + widthRight = BarChartRace3D.getRightWidth(obj.WidthLeft, xLen); + + colrs = BarChartRace3D.getColors(obj.colors(mod(i-1, obj.maxColors)+1, :)); + + %left polygon + h = fill([-290 xLen-290 xLen-290 -290], [yleft-obj.WidthLeft/2 (yright-widthRight/2) (yright+widthRight/2) (yleft+obj.WidthLeft/2)], colrs(1, :), 'EdgeColor', colrs(5, :)); + set(h,'facealpha',.5); + + %front polygon + h=fill([xLen-290 xLen-290+widthRight, xLen-290+widthRight, xLen-290], [yright-widthRight/2 yright-widthRight/2 yright+widthRight/2 yright+widthRight/2], colrs(2, :), 'EdgeColor', colrs(5, :)); + set(h,'facealpha',.5); + + if yPos < 0 + %Lower half section draw top + h = fill([-290 xLen-290 xLen-290+widthRight -290+obj.WidthLeft], [yleft+obj.WidthLeft/2 yright+widthRight/2 yright+widthRight/2 yleft+obj.WidthLeft/2], colrs(3, :), 'EdgeColor', colrs(5, :)); + else + h = fill([-290 xLen-290 xLen-290+widthRight -290+obj.WidthLeft], [yleft-obj.WidthLeft/2 yright-widthRight/2 yright-widthRight/2 yleft-obj.WidthLeft/2], colrs(4, :), 'EdgeColor', colrs(5, :)); + end + set(h,'facealpha',.5); + + if i < length(obj.labels) + lbl = obj.labels{i}; + text(-295, yPos, lbl, 'HorizontalAlignment', 'right', 'Color', colrs(5, :)); + text(xLen-290+widthRight + 5, yright, num2str(value), 'Color', colrs(5, :)); + end + end + + end + + methods(Static) + function rwidth = getRightWidth(lwidth, xLen) + scale = (580/(580+xLen)); + rwidth = lwidth/scale; + end + function yright = getRightY(yleft, xLen) + scale = (580/(580+xLen)); + yright = yleft/scale; + end + function colors = getColors(color) + colors = zeros(5, 3); + % left is the same + colors(1, :) = color; + % front shall be lighter + colors(2, :) = arrayfun(@(x) (x+0.4)*(x<0.6) + (x>=0.6), color); + % up shall be little lighter + colors(3, :) = arrayfun(@(x) (x+0.1)*(x<0.9) + (x>=0.9), color); + % buttom shall be darker + colors(4, :) = arrayfun(@(x) (x-0.2)*(x>0.2), color); + % edge + colors(5, :) = arrayfun(@(x) (x - 0.5)*(x>0.5), color); + end + end +end \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..25de320 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# BarChartRace3D +It is all about data and it is all about presentation. BarChartRace is a popular way to visualize change of data over time. To further impress your audiences, a 3D BarchartRace can help you to present your points with great power. See the following sample animation Gif's generated by BarChartRace3D. + +Refresh your browser to see the animation effect of the +sample gifs below. +## 3D sample output +![3D](./BarChartRace3D.gif) +## 2D sample output +![2D](./BarChartRace2D.gif) + +## Sample and Demo +Run testBCR3D for a demo. From this folder + +`testBCR3D` + +View testBCR3D for sample code. + +## Usage + +### Construct a BarChartRace3D +Call the constructor to create a BarChartRace3D object + +`bcr3 = BarchartRace3D();` + +### Optional configurations +These are optional configurations. If not set, the default will be used. + +#### labels +Set the labels for the bars. + +`bcr3.labels = {'Bar1', 'Bar2', 'Bar3'};` + +#### 2D/3D +Default to be 2D. Show 3D by setting this + +`bcr3.show3D=true;` + +#### output file name +Default to be BarChartRace3D.gif. Note only animation GIF out put is supported. +Change to another filename by setting + +`bcr3.outfile = tmp.gif` + +#### title +Default to be empty. + +`bcr3.title = Title` + +#### color for each bar +It has builtin (default) colors. If you want to set the color, you can do this to set 3 colors (R, G, B) +for example. If colors defined below is less than the bars, these colors will be +reused. For this example, the forth bar will use R again. + +`bcr3.colors = [1 0 0; 0 1 0; 0 0 1];` + +#### positive only +BarChartRace3D handles positive and negative data by default for 2D mode. +If you know all the data is positive, you can set the following, and then +BarChartRace3D will use the whole canvas for showing positive data. +Note 3D mode only handles positive data. + +`bcr3.positiveOnly=true;` + + +### set data and race +The data shall be an nxm array, where n is the number of iterations, and m is the number of bars. + +`data = randi(10, 8); bcr3.race(data);` + + + + + diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..97eaaf2 --- /dev/null +++ b/license.txt @@ -0,0 +1,11 @@ +Copyright (c) 2021, The MathWorks, Inc. +All rights reserved. +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +3. In all cases, the software is, and all modifications and derivatives of the software shall be, licensed to you solely for use in conjunction with MathWorks products and service offerings. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + + diff --git a/security.md b/security.md new file mode 100644 index 0000000..221952e --- /dev/null +++ b/security.md @@ -0,0 +1,6 @@ +# Reporting Security Vulnerabilities + +If you believe you have discovered a security vulnerability, please report it to +[security@mathworks.com](mailto:security@mathworks.com). Please see +[MathWorks Vulnerability Disclosure Policy for Security Researchers](https://www.mathworks.com/company/aboutus/policies_statements/vulnerability-disclosure-policy.html) +for additional information. \ No newline at end of file diff --git a/testBCR3D.m b/testBCR3D.m new file mode 100755 index 0000000..9618b68 --- /dev/null +++ b/testBCR3D.m @@ -0,0 +1,28 @@ +function testBCR3D() + clear + clc + close all + + bcr3 = BarChartRace3D(); + + % 1. Optional configuration + + % provide labels for each bar. Default to empty + bcr3.labels = {'a', 'b', 'c', 'd','e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p'}; + % set 3D. Default is false, showing 2D + bcr3.show3D=true; + % all possible data. Default to be false. Used only for 2D. + bcr3.positiveOnly = true; + % titile. Default to empty string + bcr3.title = 'BarChartRace3D'; + % output file. Default tp BarChartRace3D.gif + bcr3.outfile = 'BarChartRace3D'; + % colors. Use default if not set + % bcr3.colors = [1 0 0; 0 1 0; 0 0 1]; + + % 2. Simulate some random data + data = rand(3, 8); + + % 3. Start the race + bcr3.race(data); +end \ No newline at end of file