-
Notifications
You must be signed in to change notification settings - Fork 9
/
draw_storygraph.m
278 lines (257 loc) · 11.6 KB
/
draw_storygraph.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
function draw_storygraph(indices, block_times, varargin)
%DRAW_STORYGRAPH Creates the story-graph given which line goes where (indices)
%
% indices: One character per row, value indicating position in y-axis
% An index value of 0 means the person is not visible at that time.
% block_times: [start, end]
%
% varargin:
% names: specify name (string) for each line/character
% colors: specify color (r,g,b) for each line/character
% presence: bool matrix, size(indices), indicates character visible or not
% offset: offset, when character appears in block, the line is drawn
% from x - offset : x + offset (default, 0.2)
%
% fig.title: VideoStruct.name for example (string)
% fig.xlabel: XLabel (minutes, 30seconds, etc.)
%
% appear.first: first appearance (a row of numbers with column index)
% appear.last: last appearance (a row of numbers with column index)
%
% graphics.backlines: (true|false) should i draw gray background lines?
% graphics.xkcdify: (true|false) whether to output in XKCD mode
% graphics.linewidth: (integer) default 3
% graphics.fontsize: (integer) default 16
%
%
% Author: Makarand Tapaswi
% Created: 13-09-2013
% close all;
%% Generate defaults, parse arguments
if ~exist('indices', 'var')
% sample data to test the drawing skills
indices = [1 1 1 1 1 1 1 2 2 2 2 1 1 1 1 1
2 2 2 2 2 2 2 1 1 1 1 2 2 2 2 2
3 3 3 3 3 3 3 3 3 3 3 1 1 1 1 1
5 5 5 5 5 5 5 4 4 4 4 3 3 3 3 3
6 6 6 6 6 6 6 5 5 5 5 5 6 6 6 6
4 4 4 4 4 4 4 6 6 6 6 6 5 5 5 5];
presence = [0 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 1
1 1 1 1 0 0 1 1 1 1 0 0 1 1 1 1
0 0 0 0 0 1 1 1 1 0 0 1 1 1 1 1
0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1];
% block_times = [000 030; 030 060; 060 090; 090 120; 120 150; 150 180; 180 210; 210 240;
% 240 270; 270 300; 300 330; 330 360; 360 390; 390 420; 420 450; 450 480];
% create fake block_times to test drawing
times = 60*(rand(1, size(indices, 2))+0.1);
block_times = [cumsum([0, times(1:end-1)])', cumsum(times)'];
varargin = [varargin, 'presence', presence];
% a random example
% indices = rand(10, 25);
end
numcharacter = size(indices, 1);
numcols = size(indices, 2);
%%% Generate colors (uses the golden number color generation scheme for now)
% colors will repeat for the same saturation value, so confine it somehow later
colors = colormap(jet(numcharacter));
%%% Generate names in case they don't exist
names = cell(numcharacter, 1);
for k = 1:numcharacter
names{k} = sprintf('name%02d', k);
end
%%% Varargin
dopts.colors = colors;
dopts.names = names;
dopts.presence = ones(size(indices));
dopts.offset = 0.25;
% Default figure labels
dopts.fig.title = 'Narrative Chart';
dopts.fig.xlabel = 'Time in Minutes';
dopts.fig.xtickspace = 5;
dopts.fig.gui = false; % is the drawing part of a gui?
% Default graphics
dopts.graphics.backlines = true;
dopts.graphics.xkcdify = false;
dopts.graphics.linewidth = 3;
dopts.graphics.fontsize = 16;
% Default 0 gradients
dopts.gradients = zeros(size(indices));
opts = cvhci_process_options(varargin, dopts);
colors = opts.colors;
% Default first/last appearance
opts.se_presence = zeros(size(opts.presence));
for k = 1:numcharacter
if all(~opts.presence(k, :))
opts.appear.first(k) = -1;
opts.appear.last(k) = -1;
opts.se_presence(k, :) = 0;
else
opts.appear.first(k) = find(opts.presence(k, :) == 1, 1, 'first');
opts.appear.last(k) = find(opts.presence(k, :) == 1, 1, 'last');
opts.se_presence(k, opts.appear.first(k):opts.appear.last(k)) = 1;
end
end
%% Pre-drawing Graphics Options
hold off; cla; hold on;
ylim_whitespace = (max(indices(:)) - min(indices(:))) * 0.05;
minylim = min(min(nonzeros(opts.se_presence .* indices)))-ylim_whitespace;
maxylim = max(max(nonzeros(opts.se_presence .* indices)))+ylim_whitespace;
%%% Vertical lines option
x_coordinates = [block_times(:, 1)', block_times(end, end)];
if opts.graphics.backlines
for jj = x_coordinates
line([jj, jj], [minylim, maxylim], 'Color', [0.8, 0.8, 0.8]);
end
end
%% Prepare line segments (1 for each character, draw later!)
% y-coordinate is indicated by the value in the indices column
% x-coordinate column spacing is now related to timing (in seconds)
off = opts.offset;
%%% Convert block_times to column indices
index_duration = block_times(:, 2) - block_times(:, 1);
xcoord_startend = bsxfun(@plus, [off * index_duration, (1 - off) * index_duration], block_times(:, 1));
xcoord_forward_fillers = [(1-off) * index_duration(1:end-1) + block_times(1:end-1, 1), ...
off * index_duration(2:end) + block_times(1:end-1, 2), ...
block_times(1:end-1, 2)];
xcoord_forward_fillers(end+1, :) = [xcoord_startend(end, end), block_times(end, end), NaN];
%%% Create line structures with x, y points mapped for each identity
NewLine = struct('x', [], 'y', [], 'id', [], 'type', []);
for ii = 1:numcharacter % ii is character color code
% Lines for each character extend only from the first appearance to last.
% Everything else is empty and will not be plotted anyways.
NewLine(end+1).id = ii;
% skip if the character never appears
if all(~opts.presence(ii, :)), continue; end
NewLine(end).x = block_times(opts.appear.first(ii), 1);
NewLine(end).y = indices(ii, opts.appear.first(ii));
for jj = opts.appear.first(ii):opts.appear.last(ii) % jj is x-axis location
% indices(ii, jj) is y-axis position
NewLine(end).x = [NewLine(end).x, xcoord_startend(jj, :)];
NewLine(end).y = [NewLine(end).y, indices(ii, jj), indices(ii, jj)];
NewLine = add_transition_segment(NewLine, indices, ii, jj, xcoord_forward_fillers, opts);
end
NewLine(end).x = [NewLine(end).x, block_times(opts.appear.last(ii), 2)];
NewLine(end).y = [NewLine(end).y, NewLine(end).y(end)];
end
NewLine = NewLine(2:end);
%% Draw lines, first-last appearance, names, etc.
%%% Actually draw the lines
for ii = 1:numcharacter
% skip if the character never appears
if all(~opts.presence(ii, :)), continue; end
for jj = 1:numcols
if ~opts.se_presence(ii, jj)
continue; % just skip over. Out of first/last appearance bounds
end
[~, start_draw_idx] = min(abs(NewLine(ii).x - block_times(jj, 1)));
[~, end_draw_idx] = min(abs(NewLine(ii).x - block_times(jj, 2)));
draw_idx = start_draw_idx:end_draw_idx;
% choose marker type based on whether person is visible
switch opts.presence(ii, jj)
case 0, marker = ':'; linewidth = opts.graphics.linewidth - 2;
case 1, marker = '-'; linewidth = opts.graphics.linewidth;
end
% draw the line
line(NewLine(ii).x(draw_idx), NewLine(ii).y(draw_idx), 'Color', colors(NewLine(ii).id, :), ...
'LineStyle', marker, 'LineWidth', linewidth);
end
end
%%% First / Last Appearance drawings
for k = 1:numcharacter
% skip if the character never appears
if all(~opts.presence(k, :)), continue; end
% first appearance
plot(block_times(opts.appear.first(k), 1), indices(k, opts.appear.first(k)), 'Marker', '>', ...
'MarkerSize', 10, 'MarkerFaceColor', colors(k, :), 'MarkerEdgeColor', colors(k, :));
% last appearance
plot(block_times(opts.appear.last(k), 2), indices(k, opts.appear.last(k)), 'Marker', 'o', ...
'MarkerSize', 10, 'MarkerFaceColor', colors(k, :), 'MarkerEdgeColor', colors(k, :));
end
%%% Names
for ii = 1:numcharacter
% skip if the character never appears
if all(~opts.presence(ii, :)), continue; end
format_name = strrep(opts.names{ii}, '_', ' ');
idx = [1, strfind(format_name, ' ')+1];
format_name(idx) = upper(format_name(idx));
% Text at the first appearance
text(block_times(opts.appear.first(ii), 1)-30, indices(ii, opts.appear.first(ii)), format_name, ...
'Color', colors(ii, :), 'FontSize', opts.graphics.fontsize, ...
'HorizontalAlignment', 'right', 'VerticalAlignment', 'middle');
% Text at the last appearance
text(block_times(opts.appear.last(ii), 2)+30, indices(ii, opts.appear.last(ii)), format_name, ...
'Color', colors(ii, :), 'FontSize', opts.graphics.fontsize, ...
'HorizontalAlignment', 'left', 'VerticalAlignment', 'middle');
end
%% Draw the Gradients as tiny arrows on the lines
if any(any(opts.gradients))
% if any gradient is nonzero, basically given some gradient draw
for ii = 1:numcharacter
for jj = 1:numcols
if opts.se_presence(ii, jj) % if line is present, draw. else ignore
block_center = mean(block_times(jj, :));
% arrow([block_center, indices(ii, jj), 10], [block_center, indices(ii, jj) + opts.gradients(ii, jj), 10], ...
% 'EdgeColor', [0.8, 0.8, 0.8], 'FaceColor', [0.8, 0.8, 0.8]);
line([block_center, block_center], [indices(ii, jj), indices(ii, jj) + opts.gradients(ii, jj)], ...
'Color', colors(ii, :), 'LineWidth', 2);
end
end
end
end
%% Decorate
%%% Axes setup
set(gca, 'YLim', [minylim, maxylim]);
set(gca, 'XLim', [x_coordinates(1)-5, x_coordinates(end)+5]); % 5 seconds +/- x-coordinates
set(gca, 'YDir', 'reverse');
set(gca, 'YTick', [], 'YColor', [1 1 1]);
set(gca, 'FontSize', opts.graphics.fontsize-2);
% title(opts.fig.title, 'FontSize', opts.graphics.fontsize);
maxtime = block_times(end,end);
set(gca, 'XTick', 0:opts.fig.xtickspace*60:maxtime, 'XTickLabel', (0:opts.fig.xtickspace*60:maxtime)/60);
xlabel(opts.fig.xlabel, 'FontSize', opts.graphics.fontsize);
if ~opts.fig.gui
set(gcf, 'Color', 'w');
set(gcf, 'MenuBar', 'none');
set(gcf, 'Position', [100, 100, maxtime, 200+(maxylim-minylim)*50]);
end
%%% XKCD effect
if opts.graphics.xkcdify
warning off;
xkcdify(gca);
warning on;
end
end
function [NewLine, draw_balance] = add_transition_segment(NewLine, indices, ii, jj, xcoord_forward_fillers, opts)
% Creates the intermediate transition region between the lines
% If the transition is not flat, inserts a sigmoidal transition
sigmoid_npoints = 20;
if jj < size(indices, 2) && jj < opts.appear.last(ii) % add transition segment
xc = xcoord_forward_fillers(jj, :);
if opts.presence(ii, jj) == opts.presence(ii, jj+1)
%%% character presence is same in current and next scene
% spread sigmoid transition evenly
draw_balance = 0;
xcoords = linspace(xc(1), xc(2), sigmoid_npoints);
ycoords = mySigmoid(xcoords, [], [], xcoords(1), xcoords(end));
ycoords = (indices(ii, jj+1) - indices(ii, jj)) * ycoords;
elseif opts.presence(ii, jj) && ~opts.presence(ii, jj+1)
%%% character is present in current scene, but not in next scene
% spread sigmoid transition in next scene
draw_balance = -1;
xcoords = [xc(1), linspace(xc(3), xc(2), sigmoid_npoints)];
ycoords = [0, mySigmoid(xcoords(2:end), [], [], xcoords(2), xcoords(end))];
ycoords = (indices(ii, jj+1) - indices(ii, jj)) * ycoords;
elseif opts.presence(ii, jj+1)
%%% character is present in next scene, but not in current scene
% spread sigmoid transition in current scene
draw_balance = 1;
xcoords = [linspace(xc(1), xc(3), sigmoid_npoints), xc(2)];
ycoords = [mySigmoid(xcoords(1:end-1), [], [], xcoords(1), xcoords(end-1)), 1];
ycoords = (indices(ii, jj+1) - indices(ii, jj)) * ycoords;
end
NewLine(end).x = [NewLine(end).x, xcoords];
NewLine(end).y = [NewLine(end).y, ycoords+indices(ii, jj)];
end
end