{"id":222,"date":"2015-03-31T14:16:21","date_gmt":"2015-03-31T18:16:21","guid":{"rendered":"https:\/\/blogs.mathworks.com\/graphics\/?p=222"},"modified":"2019-06-16T14:15:38","modified_gmt":"2019-06-16T18:15:38","slug":"keeling-spiral","status":"publish","type":"post","link":"https:\/\/blogs.mathworks.com\/graphics\/2015\/03\/31\/keeling-spiral\/","title":{"rendered":"Keeling Spiral"},"content":{"rendered":"<!DOCTYPE html\r\n  PUBLIC \"-\/\/W3C\/\/DTD HTML 4.01 Transitional\/\/EN\">\r\n<html><head>\r\n      <meta http-equiv=\"Content-Type\" content=\"text\/html; charset=utf-8\">\r\n   <!--\r\nThis HTML was auto-generated from MATLAB code.\r\nTo make changes, update the MATLAB code and republish this document.\r\n      --><title>The Keeling Spiral<\/title><meta name=\"generator\" content=\"MATLAB 8.5\"><meta name=\"DC.date\" content=\"2015-03-31\"><meta name=\"DC.source\" content=\"keelingspiral.m\"><style type=\"text\/css\">\r\nhtml,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}:focus{outine:0}ins{text-decoration:none}del{text-decoration:line-through}table{border-collapse:collapse;border-spacing:0}\r\n\r\nhtml { min-height:100%; margin-bottom:1px; }\r\nhtml body { height:100%; margin:0px; font-family:Arial, Helvetica, sans-serif; font-size:10px; color:#000; line-height:140%; background:#fff none; overflow-y:scroll; }\r\nhtml body td { vertical-align:top; text-align:left; }\r\n\r\nh1 { padding:0px; margin:0px 0px 25px; font-family:Arial, Helvetica, sans-serif; font-size:1.5em; color:#d55000; line-height:100%; font-weight:normal; }\r\nh2 { padding:0px; margin:0px 0px 8px; font-family:Arial, Helvetica, sans-serif; font-size:1.2em; color:#000; font-weight:bold; line-height:140%; border-bottom:1px solid #d6d4d4; display:block; }\r\nh3 { padding:0px; margin:0px 0px 5px; font-family:Arial, Helvetica, sans-serif; font-size:1.1em; color:#000; font-weight:bold; line-height:140%; }\r\n\r\na { color:#005fce; text-decoration:none; }\r\na:hover { color:#005fce; text-decoration:underline; }\r\na:visited { color:#004aa0; text-decoration:none; }\r\n\r\np { padding:0px; margin:0px 0px 20px; }\r\nimg { padding:0px; margin:0px 0px 20px; border:none; }\r\np img, pre img, tt img, li img, h1 img, h2 img { margin-bottom:0px; } \r\n\r\nul { padding:0px; margin:0px 0px 20px 23px; list-style:square; }\r\nul li { padding:0px; margin:0px 0px 7px 0px; }\r\nul li ul { padding:5px 0px 0px; margin:0px 0px 7px 23px; }\r\nul li ol li { list-style:decimal; }\r\nol { padding:0px; margin:0px 0px 20px 0px; list-style:decimal; }\r\nol li { padding:0px; margin:0px 0px 7px 23px; list-style-type:decimal; }\r\nol li ol { padding:5px 0px 0px; margin:0px 0px 7px 0px; }\r\nol li ol li { list-style-type:lower-alpha; }\r\nol li ul { padding-top:7px; }\r\nol li ul li { list-style:square; }\r\n\r\n.content { font-size:1.2em; line-height:140%; padding: 20px; }\r\n\r\npre, code { font-size:12px; }\r\ntt { font-size: 1.2em; }\r\npre { margin:0px 0px 20px; }\r\npre.codeinput { padding:10px; border:1px solid #d3d3d3; background:#f7f7f7; }\r\npre.codeoutput { padding:10px 11px; margin:0px 0px 20px; color:#4c4c4c; }\r\npre.error { color:red; }\r\n\r\n@media print { pre.codeinput, pre.codeoutput { word-wrap:break-word; width:100%; } }\r\n\r\nspan.keyword { color:#0000FF }\r\nspan.comment { color:#228B22 }\r\nspan.string { color:#A020F0 }\r\nspan.untermstring { color:#B20000 }\r\nspan.syscmd { color:#B28C00 }\r\n\r\n.footer { width:auto; padding:10px 0px; margin:25px 0px 0px; border-top:1px dotted #878787; font-size:0.8em; line-height:140%; font-style:italic; color:#878787; text-align:left; float:none; }\r\n.footer p { margin:0px; }\r\n.footer a { color:#878787; }\r\n.footer a:hover { color:#878787; text-decoration:underline; }\r\n.footer a:visited { color:#878787; }\r\n\r\ntable th { padding:7px 5px; text-align:left; vertical-align:middle; border: 1px solid #d6d4d4; font-weight:bold; }\r\ntable td { padding:7px 5px; text-align:left; vertical-align:top; border:1px solid #d6d4d4; }\r\n\r\n\r\n\r\n\r\n\r\n  <\/style><\/head><body><div class=\"content\"><h1>The Keeling Spiral<\/h1><p>Today we're going to take a look at what is probably the most famous dataset in atmospheric science, the <a href=\"https:\/\/scripps.ucsd.edu\/programs\/keelingcurve\/\">Keeling Curve<\/a>. This is a timeseries dataset of the concentration of carbon dioxide at an observatory on Mauna Loa. <a href=\"http:\/\/en.wikipedia.org\/wiki\/Charles_David_Keeling\">Charles Keeling<\/a> started collecting this data in 1974. It's available online at the following URL.<\/p><pre class=\"codeinput\">s = urlread(<span class=\"string\">'ftp:\/\/aftp.cmdl.noaa.gov\/products\/trends\/co2\/co2_weekly_mlo.txt'<\/span>);\r\nc = strsplit(s,<span class=\"string\">'\\n'<\/span>);\r\n<\/pre><p>The file is a pretty simple text format. We just need to break it into individual lines and pull out the fields we want. We want the date (which is stored in the first 3 columns) and the \"CO2 molfrac\" which is stored in the 5th column.<\/p><pre class=\"codeinput\">nl = length(c);\r\ndates = datetime.empty;\r\nmolfrac = [];\r\n<span class=\"keyword\">for<\/span> i=1:nl\r\n    <span class=\"comment\">% Comment lines start with #<\/span>\r\n    <span class=\"keyword\">if<\/span> isempty(c{i}) || c{i}(1) == <span class=\"string\">'#'<\/span>\r\n        <span class=\"keyword\">continue<\/span>\r\n    <span class=\"keyword\">end<\/span>\r\n    <span class=\"comment\">% Get all the fields<\/span>\r\n    lab = strsplit(c{i});\r\n    value_str = lab{6};\r\n    dates(end+1) = datetime(str2double(lab{2}), <span class=\"keyword\">...<\/span>\r\n                            str2double(lab{3}), <span class=\"keyword\">...<\/span>\r\n                            str2double(lab{4}));\r\n    <span class=\"comment\">% -999.99 means missing data<\/span>\r\n    <span class=\"keyword\">if<\/span> strcmp(value_str,<span class=\"string\">'-999.99'<\/span>)\r\n        molfrac(end+1) = nan;\r\n    <span class=\"keyword\">else<\/span>\r\n        molfrac(end+1) = str2double(value_str);\r\n    <span class=\"keyword\">end<\/span>\r\n<span class=\"keyword\">end<\/span>\r\n<\/pre><p>At this point, we have two arrays. * dates is an array of datetime values when the measurements were taken. * molfrac is a numeric array with the measurements in parts per million.<\/p><p>Lets start out with a simple plot.<\/p><pre class=\"codeinput\">plot(dates,molfrac)\r\nxlabel(<span class=\"string\">'Date'<\/span>)\r\nylabel(<span class=\"string\">'parts per million'<\/span>)\r\ntitle(<span class=\"string\">'Atmospheric CO_2 at Mauna Loa'<\/span>);\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/graphics\/2015\/keelingspiral_01.png\" alt=\"\"> <p>As you can see, the CO2 level keeps going up every year, but it also bounces up and down quite a bit. That's because there's a seasonal variation in the level of CO2 that's related to things like the rate at which plants are growing. We can see this if we zoom in on a single year.<\/p><pre class=\"codeinput\">mask = year(dates)==1987;\r\nplot(dates(mask),molfrac(mask))\r\ntitle(<span class=\"string\">'Atmospheric CO_2 at Mauna Loa, 1987'<\/span>);\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/graphics\/2015\/keelingspiral_02.png\" alt=\"\"> <p>One simple way to deal with this seasonality is to break the dataset into individual years, and plot them all together. We do this by looping over the years, pulling out the values where \"year(dates)\" matches our loop index, and then computing the number of days since January 1st of that year.<\/p><pre class=\"codeinput\">cla <span class=\"string\">reset<\/span>\r\nhold <span class=\"string\">on<\/span>\r\n<span class=\"keyword\">for<\/span> yidx=1974:2015\r\n    mask = year(dates)==yidx;\r\n    plot(days(dates(mask)-datetime(yidx,1,1)),molfrac(mask));\r\n<span class=\"keyword\">end<\/span>\r\nxlim([0 365])\r\nxlabel(<span class=\"string\">'Day of Year'<\/span>)\r\nylabel(<span class=\"string\">'parts per million'<\/span>)\r\ntitle(<span class=\"string\">'Atmospheric CO_2 at Mauna Loa'<\/span>);\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/graphics\/2015\/keelingspiral_03.png\" alt=\"\"> <p>But we really need to improve the tick labels with this view. We can use the <a href=\"https:\/\/www.mathworks.com\/help\/matlab\/ref\/datestr.html\">datestr<\/a> function to get a nicer label for the days.<\/p><pre class=\"codeinput\">xlabel(<span class=\"string\">''<\/span>)\r\nax = gca;\r\nx = ax.XTick;\r\nlab = cell(size(x));\r\n<span class=\"keyword\">for<\/span> i=1:length(x)\r\n    lab{i} = datestr(datetime(2015,1,1) + days(x(i)), <span class=\"string\">'mmm dd'<\/span>);\r\n<span class=\"keyword\">end<\/span>\r\nax.XTickLabel = lab;\r\nax.XTickLabelRotation = 45;\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/graphics\/2015\/keelingspiral_04.png\" alt=\"\"> <p>Stacking 2D lines shows us how the CO2 level moves up each year, but what would it look like if we plotted it in 3D? That's pretty easily done by creating an array of years and an array of \"day of the year\".<\/p><pre class=\"codeinput\">yr = zeros(size(molfrac));\r\ndofy = zeros(size(molfrac));\r\n<span class=\"keyword\">for<\/span> yidx=1974:2015\r\n    mask = year(dates) == yidx;\r\n    yr(mask) = yidx;\r\n    dofy(mask) = days(dates(mask) - datetime(yidx,1,1));\r\n<span class=\"keyword\">end<\/span>\r\n\r\nhold <span class=\"string\">off<\/span>\r\ncla <span class=\"string\">reset<\/span>\r\n<span class=\"keyword\">for<\/span> yidx=1974:2015\r\n    mask = year(dates) == yidx;\r\n    line(dofy(mask), yr(mask), molfrac(mask));\r\n<span class=\"keyword\">end<\/span>\r\nbox <span class=\"string\">on<\/span>\r\ngrid <span class=\"string\">on<\/span>\r\nview(3)\r\nxlim([0 365])\r\nylim([1974 2015])\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/graphics\/2015\/keelingspiral_05.png\" alt=\"\"> <p>And again, we'll want to pretty up the labels.<\/p><pre class=\"codeinput\">ax = gca;\r\nx = ax.XTick;\r\nlab = cell(size(x));\r\n<span class=\"keyword\">for<\/span> i=1:length(x)\r\n    lab{i} = datestr(datetime(2015,1,1) + days(x(i)), <span class=\"string\">'mmm dd'<\/span>);\r\n<span class=\"keyword\">end<\/span>\r\nax.XTickLabel = lab;\r\nxlabel(<span class=\"string\">'Day'<\/span>)\r\nylabel(<span class=\"string\">'Year'<\/span>)\r\nzlabel(<span class=\"string\">'parts per million'<\/span>)\r\ntitle(<span class=\"string\">'Atmospheric CO_2 at Mauna Loa'<\/span>);\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/graphics\/2015\/keelingspiral_06.png\" alt=\"\"> <p>We could also use <a href=\"https:\/\/www.mathworks.com\/help\/matlab\/ref\/scatter3.html\">scatter3<\/a> to plot molfrac against the yr and dofy arrays. That gives us something that looks a little like a surface.<\/p><pre class=\"codeinput\">scatter3(dofy,yr,molfrac)\r\nxlim([0 365])\r\nylim([1974 2015])\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/graphics\/2015\/keelingspiral_07.png\" alt=\"\"> <p>But it'd be nice if that really was a surface plot. We can't use the surface command because this isn't a regular grid. We could pad the first year and last year out to 52 weeks by adding nans, but there are also years that have 53 weeks, such as 1978.<\/p><p>We can skip the gridding problem by using <a href=\"https:\/\/www.mathworks.com\/help\/matlab\/ref\/trisurf.html\">trisurf<\/a>, but that means we'll need to triangulate the data. Lets try using <a href=\"https:\/\/www.mathworks.com\/help\/matlab\/ref\/delaunay.html\">Delaunay triangulation<\/a>. BTW, the divide by 7 is required because Delaunay triangulation works best when the X &amp; Y scaling is close to uniform.<\/p><pre class=\"codeinput\">tri = delaunay(yr,dofy\/7);\r\nh = trisurf(tri,dofy,yr,molfrac);\r\nh.EdgeColor = <span class=\"string\">'none'<\/span>;\r\nh.SpecularStrength = .25;\r\nylim([1974 2015])\r\ncamlight\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/graphics\/2015\/keelingspiral_08.png\" alt=\"\"> <p>That looks pretty interesting. We can now rotate it around and see some details about how the different years relate to each other. But as you can see there is something funny going on at the edges. If we look straight down and zoom in, we can figure out what's happening:<\/p><pre class=\"codeinput\">xlim([0 200])\r\nylim([1970 1980])\r\nh.FaceLighting = <span class=\"string\">'none'<\/span>;\r\nh.EdgeColor = <span class=\"string\">'black'<\/span>;\r\nview(2)\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/graphics\/2015\/keelingspiral_09.png\" alt=\"\"> <p>The call to delaunay created triangles between the first day (19-May-1974) and lots of different days in 1975. The Delaunay triangulation includes these triangles because they're part of the convex hull of the points, but they're not really meaningful for our purposes. We can get rid of those by walking through the array of triangles and throwing away any with long edges.<\/p><pre class=\"codeinput\">max_x = 1;\r\nmax_y = 15;\r\nlong_x = (abs(yr(tri(:,1))-yr(tri(:,2))) &gt; max_x) <span class=\"keyword\">...<\/span>\r\n       | (abs(yr(tri(:,2))-yr(tri(:,3))) &gt; max_x) <span class=\"keyword\">...<\/span>\r\n       | (abs(yr(tri(:,3))-yr(tri(:,1))) &gt; max_x);\r\nlong_y = (abs(dofy(tri(:,1))-dofy(tri(:,2))) &gt; max_y) <span class=\"keyword\">...<\/span>\r\n       | (abs(dofy(tri(:,2))-dofy(tri(:,3))) &gt; max_y) <span class=\"keyword\">...<\/span>\r\n       | (abs(dofy(tri(:,3))-dofy(tri(:,1))) &gt; max_y);\r\ntri = tri(~(long_x | long_y),:);\r\nh = trisurf(tri,dofy,yr,molfrac);\r\nh.EdgeColor = <span class=\"string\">'none'<\/span>;\r\nh.SpecularStrength = .25;\r\ncamlight\r\nview(2)\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/graphics\/2015\/keelingspiral_10.png\" alt=\"\"> <p>Now we'll go back to our 3D view.<\/p><pre class=\"codeinput\">view(3)\r\nxlim([0 365])\r\nylim([1974 2015])\r\nzlim([-inf inf])\r\n\r\nax = gca;\r\nt = ax.XTick;\r\nlab = cell(size(t));\r\n<span class=\"keyword\">for<\/span> i=1:length(t)\r\n    lab{i} = datestr(datetime(2015,1,1) + days(t(i)), <span class=\"string\">'mmm dd'<\/span>);\r\n<span class=\"keyword\">end<\/span>\r\nax.XTickLabel = lab;\r\nxlabel(<span class=\"string\">'Day'<\/span>)\r\nylabel(<span class=\"string\">'Year'<\/span>)\r\nzlabel(<span class=\"string\">'parts per million'<\/span>)\r\nbox <span class=\"string\">on<\/span>\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/graphics\/2015\/keelingspiral_11.png\" alt=\"\"> <p>But there's a problem with this visualization. The last day of one year is actually adjacent to the first day of the next year, but in this visualization they're on opposite sides of the chart. What if we rolled this around a cylinder? We'll make a cylindrical chart where the angle is the day of the year, the Z is the year, and the radius is the measurement of the CO2 level.<\/p><pre class=\"codeinput\">[x,y,z] = pol2cart(2*pi*dofy\/365, molfrac, yr+dofy\/365);\r\nh = trisurf(tri,x,y,z);\r\nh.FaceVertexCData = molfrac';\r\nh.FaceColor = <span class=\"string\">'interp'<\/span>;\r\nh.SpecularStrength = .25;\r\nline([0 0],[0 0],[1970 2020],<span class=\"string\">'Color'<\/span>,[.75 .75 .75],<span class=\"string\">'LineWidth'<\/span>,3)\r\ncamlight\r\nxlim([-405 405])\r\nylim([-405 405])\r\nzlim([1974 2015])\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/graphics\/2015\/keelingspiral_12.png\" alt=\"\"> <p>That's kind of interesting, but we can see that there's a rip down the back. That's because the Delaunay triangulation didn't know that those samples were near each other. It was working in a 2D plane which had a cut rather than in the cyclic surface of the cylinder. We'll need to do our own triangulation. This can be kind of tedious, so I'll just pull out one I prepared earlier like <a href=\"http:\/\/en.wikipedia.org\/wiki\/Julia_Child\">Julia Child<\/a> used to do on her TV show. You can <a href=\"https:\/\/blogs.mathworks.com\/images\/graphics\/2015\/stripyears.m\">download it here<\/a>.<\/p><pre class=\"codeinput\">tri = stripyears(yr,dofy);\r\n[x,y,z] = pol2cart(2*pi*dofy\/365, molfrac, yr+dofy\/365);\r\nh = trisurf(tri,x,y,z);\r\nh.FaceVertexCData = molfrac';\r\nh.FaceColor = <span class=\"string\">'interp'<\/span>;\r\nh.SpecularStrength = .25;\r\nline([0 0],[0 0],[1970 2020],<span class=\"string\">'Color'<\/span>,[.75 .75 .75],<span class=\"string\">'LineWidth'<\/span>,3)\r\nlight(<span class=\"string\">'Position'<\/span>,[-1 -.5 -.25]);\r\nlight(<span class=\"string\">'Position'<\/span>,[-1.5 .5 .25]);\r\nxlim([-405 405])\r\nylim([-405 405])\r\nzlim([1974 2015])\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/graphics\/2015\/keelingspiral_13.png\" alt=\"\"> <p>This results in a fairly unusual visualization that I saw used a few times back in the 80's, but I don't think that it's been used much since. That was really the dawn of 3D in data visualization, so we were probably just getting carried away with our new toy. It certainly has some limitations. But lets see if there's anything we can do to make it more useful.<\/p><p>The first thing we should do is fix the datatips so that they display the date and value from the dataset, rather than the X, Y, &amp; Z coordinates. That's actually pretty easy. We just need this simple function:<\/p><pre class=\"codeinput\">type <span class=\"string\">keeling_datatip.m<\/span>\r\ndcm = datacursormode(gcf);\r\ndcm.Enable = <span class=\"string\">'on'<\/span>;\r\ndcm.UpdateFcn = @keeling_datatip;\r\n<\/pre><pre class=\"codeoutput\">\r\nfunction txt = keeling_datatip(~,evd)\r\n    [th,r,z] = cart2pol(evd.Position(1),evd.Position(2),evd.Position(3));\r\n    th = mod(th,2*pi);\r\n    d = datetime(floor(z),1,1) + days(365*th\/(2*pi));\r\n    txt = {['Date: ', datestr(d)], ...\r\n           ['Value: ', num2str(r,4)]};\r\nend\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/graphics\/2015\/datatip.png\" alt=\"\"> <p>Another thing we can do is to exagerate the radius. That will make the variations in the radius more visible. We could also get rid of the edges and switch to Gouraud lighting.<\/p><pre class=\"codeinput\">roffset = 300;\r\n[x,y,z] = pol2cart(2*pi*dofy\/365, molfrac-roffset, yr+dofy\/365);\r\nh = trisurf(tri,x,y,z);\r\nh.FaceVertexCData = molfrac';\r\nh.FaceColor = <span class=\"string\">'interp'<\/span>;\r\nh.EdgeColor = <span class=\"string\">'none'<\/span>;\r\nh.FaceLighting = <span class=\"string\">'gouraud'<\/span>;\r\nh.SpecularStrength = .25;\r\nline([0 0],[0 0],[1970 2020],<span class=\"string\">'Color'<\/span>,[.75 .75 .75],<span class=\"string\">'LineWidth'<\/span>,3)\r\nl1 = light(<span class=\"string\">'Position'<\/span>,[-1 -.5 -.25]);\r\nl2 = light(<span class=\"string\">'Position'<\/span>,[-1.5 .5 .25]);\r\nxlim([-105 105])\r\nylim([-105 105])\r\nzlim([1974 2015])\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/graphics\/2015\/keelingspiral_15.png\" alt=\"\"> <p>But when with the new radius scale, we need to fix the X &amp; Y ticks to account for the offset.<\/p><pre class=\"codeinput\">ax = gca;\r\nt = ax.XTick;\r\nlab = cell(size(t));\r\n<span class=\"keyword\">for<\/span> i=1:length(t)\r\n    lab{i} = num2str(abs(t(i))+roffset);\r\n<span class=\"keyword\">end<\/span>\r\nax.XTickLabel = lab;\r\nax.YTick = t;\r\nax.YTickLabel = lab;\r\nxlabel(<span class=\"string\">'parts per million'<\/span>)\r\nylabel(<span class=\"string\">'parts per million'<\/span>)\r\nzlabel(<span class=\"string\">'Year'<\/span>)\r\ntitle(<span class=\"string\">'Atmospheric CO_2 at Mauna Loa'<\/span>);\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/graphics\/2015\/keelingspiral_16.png\" alt=\"\"> <p>What do you think? Can you think of cases where this visualization might be useful? Can you think of any additional tweaks which might make it more useful?<\/p><p class=\"footer\"><br><a href=\"https:\/\/www.mathworks.com\/products\/matlab\/\">Published with MATLAB&reg; R2015a<\/a><br><\/p><\/div><!--\r\n##### SOURCE BEGIN #####\r\n%% The Keeling Spiral\r\n%\r\n% Today we're going to take a look at what is probably the most famous dataset \r\n% in atmospheric science, the <https:\/\/scripps.ucsd.edu\/programs\/keelingcurve\/\r\n% Keeling Curve>. This is a timeseries dataset of the concentration of\r\n% carbon dioxide at an observatory on Mauna Loa.\r\n% <http:\/\/en.wikipedia.org\/wiki\/Charles_David_Keeling Charles Keeling>\r\n% started collecting this data in 1974. It's available online at the\r\n% following URL.\r\n%\r\ns = urlread('ftp:\/\/aftp.cmdl.noaa.gov\/products\/trends\/co2\/co2_weekly_mlo.txt');\r\nc = strsplit(s,'\\n');\r\n\r\n%%\r\n% The file is a pretty simple text format. We just need to break it into \r\n% individual lines and pull out the fields we want. We want the date (which\r\n% is stored in the first 3 columns) and the \"CO2 molfrac\" which is stored\r\n% in the 5th column.\r\n%\r\nnl = length(c);\r\ndates = datetime.empty;\r\nmolfrac = [];\r\nfor i=1:nl\r\n    % Comment lines start with #\r\n    if isempty(c{i}) || c{i}(1) == '#'\r\n        continue\r\n    end\r\n    % Get all the fields\r\n    lab = strsplit(c{i});\r\n    value_str = lab{6};\r\n    dates(end+1) = datetime(str2double(lab{2}), ...\r\n                            str2double(lab{3}), ...\r\n                            str2double(lab{4}));\r\n    % -999.99 means missing data\r\n    if strcmp(value_str,'-999.99')\r\n        molfrac(end+1) = nan;\r\n    else\r\n        molfrac(end+1) = str2double(value_str);\r\n    end\r\nend\r\n\r\n%%\r\n% At this point, we have two arrays.\r\n% * dates is an array of datetime values when the measurements were taken.\r\n% * molfrac is a numeric array with the measurements in parts per million.\r\n%\r\n% Lets start out with a simple plot.\r\nplot(dates,molfrac)\r\nxlabel('Date')\r\nylabel('parts per million')\r\ntitle('Atmospheric CO_2 at Mauna Loa');\r\n\r\n%%\r\n% As you can see, the CO2 level keeps going up every year, but it also \r\n% bounces up and down quite a bit. That's because\r\n% there's a seasonal variation in the level of CO2 that's related to things\r\n% like the rate at which plants are growing. We can see this if we zoom in \r\n% on a single year.\r\nmask = year(dates)==1987;\r\nplot(dates(mask),molfrac(mask))\r\ntitle('Atmospheric CO_2 at Mauna Loa, 1987');\r\n\r\n%% \r\n% One simple way to deal with this seasonality is to break the dataset into\r\n% individual years, and plot them all together. We do this by looping over\r\n% the years, pulling out the values where \"year(dates)\" matches our loop \r\n% index, and then computing the number of days since January 1st of that\r\n% year.\r\ncla reset\r\nhold on\r\nfor yidx=1974:2015\r\n    mask = year(dates)==yidx;\r\n    plot(days(dates(mask)-datetime(yidx,1,1)),molfrac(mask));\r\nend\r\nxlim([0 365])\r\nxlabel('Day of Year')\r\nylabel('parts per million')\r\ntitle('Atmospheric CO_2 at Mauna Loa');\r\n\r\n%%\r\n% But we really need to improve the tick labels with this view. We can use\r\n% the <https:\/\/www.mathworks.com\/help\/matlab\/ref\/datestr.html datestr> function to get a nicer label for the days.\r\nxlabel('')\r\nax = gca;\r\nx = ax.XTick;\r\nlab = cell(size(x));\r\nfor i=1:length(x)\r\n    lab{i} = datestr(datetime(2015,1,1) + days(x(i)), 'mmm dd');\r\nend\r\nax.XTickLabel = lab;\r\nax.XTickLabelRotation = 45;\r\n\r\n%%\r\n% Stacking 2D lines shows us how the CO2 level moves up each year, but what\r\n% would it look like if we plotted it in 3D? That's pretty easily done by\r\n% creating an array of years and an array of \"day of the year\". \r\nyr = zeros(size(molfrac));\r\ndofy = zeros(size(molfrac));\r\nfor yidx=1974:2015\r\n    mask = year(dates) == yidx;\r\n    yr(mask) = yidx;\r\n    dofy(mask) = days(dates(mask) - datetime(yidx,1,1));\r\nend\r\n\r\nhold off\r\ncla reset\r\nfor yidx=1974:2015\r\n    mask = year(dates) == yidx;\r\n    line(dofy(mask), yr(mask), molfrac(mask));\r\nend\r\nbox on\r\ngrid on\r\nview(3)\r\nxlim([0 365])\r\nylim([1974 2015])\r\n\r\n%%\r\n% And again, we'll want to pretty up the labels.\r\nax = gca;\r\nx = ax.XTick;\r\nlab = cell(size(x));\r\nfor i=1:length(x)\r\n    lab{i} = datestr(datetime(2015,1,1) + days(x(i)), 'mmm dd');\r\nend\r\nax.XTickLabel = lab;\r\nxlabel('Day')\r\nylabel('Year')\r\nzlabel('parts per million')\r\ntitle('Atmospheric CO_2 at Mauna Loa');\r\n\r\n\r\n%%\r\n% We could also \r\n% use <https:\/\/www.mathworks.com\/help\/matlab\/ref\/scatter3.html scatter3> to\r\n% plot molfrac against the yr and dofy arrays. That gives us something that\r\n% looks a little like a surface.\r\nscatter3(dofy,yr,molfrac)\r\nxlim([0 365])\r\nylim([1974 2015])\r\n\r\n%%\r\n% But it'd be nice if that really was a surface plot. We can't use the surface\r\n% command because this isn't a regular grid. We could pad the first year\r\n% and last year out to 52 weeks by adding nans, but there are also years\r\n% that have 53 weeks, such as 1978.\r\n%\r\n% We can skip the gridding problem by using\r\n% <https:\/\/www.mathworks.com\/help\/matlab\/ref\/trisurf.html trisurf>, but \r\n% that means we'll need to triangulate the data. Lets try using <https:\/\/www.mathworks.com\/help\/matlab\/ref\/delaunay.html Delaunay\r\n% triangulation>. BTW, the divide by 7 is required because Delaunay triangulation \r\n% works best when the X & Y scaling is close to uniform.\r\ntri = delaunay(yr,dofy\/7);\r\nh = trisurf(tri,dofy,yr,molfrac);\r\nh.EdgeColor = 'none';\r\nh.SpecularStrength = .25;\r\nylim([1974 2015])\r\ncamlight\r\n\r\n%%\r\n% That looks pretty interesting. We can now rotate it around and see some \r\n% details about how the different years relate to each other. But as you can \r\n% see there is something funny going on at the edges. If we look straight \r\n% down and zoom in, we can figure out what's happening:\r\nxlim([0 200])\r\nylim([1970 1980])\r\nh.FaceLighting = 'none';\r\nh.EdgeColor = 'black';\r\nview(2)\r\n\r\n%%\r\n% The call to delaunay created triangles between the first day (19-May-1974)\r\n% and lots of different days in 1975. The Delaunay triangulation includes\r\n% these triangles because they're part of the convex hull of the points, but \r\n% they're not really meaningful for our purposes. We can get rid of those \r\n% by walking through the array of triangles and throwing away any with long edges.\r\nmax_x = 1;\r\nmax_y = 15;\r\nlong_x = (abs(yr(tri(:,1))-yr(tri(:,2))) > max_x) ...\r\n       | (abs(yr(tri(:,2))-yr(tri(:,3))) > max_x) ...\r\n       | (abs(yr(tri(:,3))-yr(tri(:,1))) > max_x);\r\nlong_y = (abs(dofy(tri(:,1))-dofy(tri(:,2))) > max_y) ...\r\n       | (abs(dofy(tri(:,2))-dofy(tri(:,3))) > max_y) ...\r\n       | (abs(dofy(tri(:,3))-dofy(tri(:,1))) > max_y);\r\ntri = tri(~(long_x | long_y),:);\r\nh = trisurf(tri,dofy,yr,molfrac);\r\nh.EdgeColor = 'none';\r\nh.SpecularStrength = .25;\r\ncamlight\r\nview(2)\r\n\r\n%%\r\n% Now we'll go back to our 3D view.\r\nview(3)\r\nxlim([0 365])\r\nylim([1974 2015])\r\nzlim([-inf inf])\r\n\r\nax = gca;\r\nt = ax.XTick;\r\nlab = cell(size(t));\r\nfor i=1:length(t)\r\n    lab{i} = datestr(datetime(2015,1,1) + days(t(i)), 'mmm dd');\r\nend\r\nax.XTickLabel = lab;\r\nxlabel('Day')\r\nylabel('Year')\r\nzlabel('parts per million')\r\nbox on\r\n\r\n%%\r\n% But there's a problem with this visualization. The last day of one year\r\n% is actually adjacent to the first day of the next year, but in this\r\n% visualization they're on opposite sides of the chart. What if we rolled\r\n% this around a cylinder? We'll make a cylindrical chart where the angle is\r\n% the day of the year, the Z is the year, and the radius is the measurement\r\n% of the CO2 level.\r\n[x,y,z] = pol2cart(2*pi*dofy\/365, molfrac, yr+dofy\/365);\r\nh = trisurf(tri,x,y,z);\r\nh.FaceVertexCData = molfrac';\r\nh.FaceColor = 'interp';\r\nh.SpecularStrength = .25;\r\nline([0 0],[0 0],[1970 2020],'Color',[.75 .75 .75],'LineWidth',3)\r\ncamlight\r\nxlim([-405 405])\r\nylim([-405 405])\r\nzlim([1974 2015])\r\n\r\n%%\r\n% That's kind of interesting, but we can see that there's a rip down the\r\n% back. That's because the Delaunay triangulation didn't know that those\r\n% samples were near each other. It was working in a 2D plane which had a cut \r\n% rather than in the cyclic surface of the cylinder. We'll need to do our \r\n% own triangulation. This can be kind of tedious, so I'll just pull out one\r\n% I prepared earlier like <http:\/\/en.wikipedia.org\/wiki\/Julia_Child Julia Child>\r\n% used to do on her TV show. You can <https:\/\/blogs.mathworks.com\/images\/graphics\/2015\/stripyears.m download it here>.\r\ntri = stripyears(yr,dofy);\r\n[x,y,z] = pol2cart(2*pi*dofy\/365, molfrac, yr+dofy\/365);\r\nh = trisurf(tri,x,y,z);\r\nh.FaceVertexCData = molfrac';\r\nh.FaceColor = 'interp';\r\nh.SpecularStrength = .25;\r\nline([0 0],[0 0],[1970 2020],'Color',[.75 .75 .75],'LineWidth',3)\r\nlight('Position',[-1 -.5 -.25]);\r\nlight('Position',[-1.5 .5 .25]);\r\nxlim([-405 405])\r\nylim([-405 405])\r\nzlim([1974 2015])\r\n\r\n%%\r\n% This results in a fairly unusual visualization that I saw used a few times \r\n% back in the 80's, \r\n% but I don't think that it's been used much since. That was really \r\n% the dawn of 3D in data visualization, so we were probably just getting \r\n% carried away with our new toy. It certainly has some limitations. But\r\n% lets see if there's anything we can do to make it more useful.\r\n%\r\n% The first thing we should do is fix the datatips so that they display \r\n% the date and value from the dataset, rather than the X, Y, & Z\r\n% coordinates. That's actually pretty easy. We just need this simple\r\n% function:\r\ntype keeling_datatip.m\r\ndcm = datacursormode(gcf);\r\ndcm.Enable = 'on';\r\ndcm.UpdateFcn = @keeling_datatip;\r\n\r\n%%\r\n% Another thing we can do is to exagerate the radius. That will make the\r\n% variations in the radius more visible. We could also get rid of the\r\n% edges and switch to Gouraud lighting.\r\nroffset = 300;\r\n[x,y,z] = pol2cart(2*pi*dofy\/365, molfrac-roffset, yr+dofy\/365);\r\nh = trisurf(tri,x,y,z);\r\nh.FaceVertexCData = molfrac';\r\nh.FaceColor = 'interp';\r\nh.EdgeColor = 'none';\r\nh.FaceLighting = 'gouraud';\r\nh.SpecularStrength = .25;\r\nline([0 0],[0 0],[1970 2020],'Color',[.75 .75 .75],'LineWidth',3)\r\nl1 = light('Position',[-1 -.5 -.25]);\r\nl2 = light('Position',[-1.5 .5 .25]);\r\nxlim([-105 105])\r\nylim([-105 105])\r\nzlim([1974 2015])\r\n%%\r\n% But when with the new radius scale, we need to fix the X & Y ticks to account for the\r\n% offset.\r\nax = gca;\r\nt = ax.XTick;\r\nlab = cell(size(t));\r\nfor i=1:length(t)\r\n    lab{i} = num2str(abs(t(i))+roffset);\r\nend\r\nax.XTickLabel = lab;\r\nax.YTick = t;\r\nax.YTickLabel = lab;\r\nxlabel('parts per million')\r\nylabel('parts per million')\r\nzlabel('Year')\r\ntitle('Atmospheric CO_2 at Mauna Loa');\r\n\r\n%%\r\n% What do you think? Can you think of cases where this visualization might\r\n% be useful? Can you think of any additional tweaks which might make it more useful?\r\n\r\n##### SOURCE END #####\r\n--><\/body><\/html>","protected":false},"excerpt":{"rendered":"<div class=\"overview-image\"><img src=\"https:\/\/blogs.mathworks.com\/graphics\/files\/feature_image\/keelingthumbnail.png\" class=\"img-responsive attachment-post-thumbnail size-post-thumbnail wp-post-image\" alt=\"\" decoding=\"async\" loading=\"lazy\" \/><\/div><p>\r\n\r\n      \r\n   The Keeling... <a class=\"read-more\" href=\"https:\/\/blogs.mathworks.com\/graphics\/2015\/03\/31\/keeling-spiral\/\">read more >><\/a><\/p>","protected":false},"author":89,"featured_media":225,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":[],"categories":[7],"tags":[],"_links":{"self":[{"href":"https:\/\/blogs.mathworks.com\/graphics\/wp-json\/wp\/v2\/posts\/222"}],"collection":[{"href":"https:\/\/blogs.mathworks.com\/graphics\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blogs.mathworks.com\/graphics\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blogs.mathworks.com\/graphics\/wp-json\/wp\/v2\/users\/89"}],"replies":[{"embeddable":true,"href":"https:\/\/blogs.mathworks.com\/graphics\/wp-json\/wp\/v2\/comments?post=222"}],"version-history":[{"count":5,"href":"https:\/\/blogs.mathworks.com\/graphics\/wp-json\/wp\/v2\/posts\/222\/revisions"}],"predecessor-version":[{"id":831,"href":"https:\/\/blogs.mathworks.com\/graphics\/wp-json\/wp\/v2\/posts\/222\/revisions\/831"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blogs.mathworks.com\/graphics\/wp-json\/wp\/v2\/media\/225"}],"wp:attachment":[{"href":"https:\/\/blogs.mathworks.com\/graphics\/wp-json\/wp\/v2\/media?parent=222"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blogs.mathworks.com\/graphics\/wp-json\/wp\/v2\/categories?post=222"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blogs.mathworks.com\/graphics\/wp-json\/wp\/v2\/tags?post=222"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}