pynarrative

1from .story import Story, story
2
3
4__all__ = ['Story', 'story']
class Story:
  5class Story:
  6    """
  7    Story class: Implements a structure for creating narrative views of data.
  8    
  9    This class extends the functionality of Altair to include narrative elements
 10    such as titles, contexts, call-to-action and annotations. It is designed to facilitate
 11    the creation of more engaging and informative data visualisations.
 12    """
 13
 14    def __init__(self, data=None, width=600, height=400, font='Arial', base_font_size=16, **kwargs):
 15        """
 16        Initialise a Story object.
 17
 18        Parameters:
 19        - data: DataFrame or URL for graph data  (default: None)
 20        - width: Graph width in pixels (default: 600)
 21        - height: Height of the graph in pixels (default: 400)
 22        - font: Font to be used for all text elements (default: 'Arial')
 23        - base_font_size: Basic font size in pixels (default: 16)
 24        - **kwargs: Additional parameters to be passed to the constructor of alt.Chart
 25        """
 26        # Initialising the Altair Chart object with basic parameters
 27        self.chart = alt.Chart(data, width=width, height=height, **kwargs)
 28        self.font = font
 29        self.base_font_size = base_font_size
 30        self.story_layers = []  # List for storing history layers
 31        
 32        # Dictionaries for the sizes and colours of various text elements
 33        # These values are multipliers for base_font_size
 34        self.font_sizes = {
 35            'title': 2,        # The title will be twice as big as the basic font
 36            'subtitle': 1.5,   # The subtitle will be 1.5 times bigger
 37            'context': 1.2,    # The context text will be 1.2 times larger
 38            'nextstep': 1.3,   # Next-Step text will be 1.3 times larger
 39            'source': 1        # The source text will have the basic size
 40        }
 41        # Predefined colours for various text elements
 42        self.colors = {
 43            'title': 'black',
 44            'subtitle': 'gray',
 45            'context': 'black',
 46            'nextstep': 'black',
 47            'source': 'gray'
 48        }
 49        self.config = {}
 50
 51    def __getattr__(self, name):
 52        """
 53        Special method to delegate attributes not found to the Altair Chart object.
 54        
 55        This method is crucial for maintaining compatibility with Altair, allowing
 56        to call Altair methods directly on the Story object.
 57
 58        Parameters:
 59        - name: Name of the required attribute
 60
 61        Returns:
 62        - Altair attribute if it exists, otherwise raises an AttributeError
 63        """
 64        # Search for the attribute in Altair's Chart object
 65        attr = getattr(self.chart, name)
 66        
 67        # If the attribute is callable (i.e. it is a method), we create a wrapper
 68        if callable(attr):
 69            def wrapped(*args, **kwargs):
 70                # This wrapped function is created dynamically for each Altair method
 71                # It is used to intercept Altair method calls and handle them correctly
 72                
 73                # Let us call Altair's original method
 74                result = attr(*args, **kwargs)
 75                
 76                # If the result is a new Altair Chart object
 77                if isinstance(result, alt.Chart):
 78                    # We update the chart attribute of our Story instance 
 79                    self.chart = result
 80                    # We return self (the Story instance) to allow method chaining
 81                    return self
 82                # If the result is not a Chart, we return it as it is
 83                return result
 84            
 85            # We return the wrapped function instead of the original method
 86            return wrapped
 87        
 88        # If the attribute is not callable (it is a property), we return it directly
 89        return attr
 90
 91    def add_title(self, title, subtitle=None, title_color=None, subtitle_color=None, title_font_size=None, subtitle_font_size=None, dx=None, dy=None, s_dx=None, s_dy=None):
 92        """
 93        Adds a title layer (and optional subtitle) to the story.
 94
 95        Parameters:
 96        - title: Main title text
 97        - subtitle: Subtitle text (optional)
 98        - title_color: Custom colour for the title (optional)
 99        - subtitle_color: Custom colour for the subtitle (optional)
100        - title_font_size: Custom font size for title in pixels (optional)
101        - subtitle_font_size: Custom font size for subtitle in pixels (optional)
102        - dx: Horizontal offset of the title in pixels (optional)
103        - dy: Vertical offset of the title in pixels (optional)
104        - s_dx: Horizontal offset of the subtitle in pixels (optional)
105        - s_dy: Vertical offset of the subtitle in pixels (optional)
106
107        returns:
108        - self, to allow method chaining
109        """
110        self.story_layers.append({
111            'type': 'title', 
112            'title': title, 
113            'subtitle': subtitle,
114            'title_color': title_color or self.colors['title'],
115            'subtitle_color': subtitle_color or self.colors['subtitle'],
116            'title_font_size': title_font_size or self.em_to_px(self.font_sizes['title']),
117            'subtitle_font_size': subtitle_font_size or self.em_to_px(self.font_sizes['subtitle']),
118            'dx': dx or 0,
119            'dy': dy or 0,
120            's_dx': s_dx or 0,
121            's_dy': s_dy or 0
122        })
123        return self
124
125    def add_context(self, text, position='left', color=None, dx=0, dy=0, font_size=None):
126        """
127        Adds a context layer to the story.
128
129        Parameters:
130        - text: The context text to be added
131        - position: The position of the text (default: ‘left’)
132        - colour: Custom text colour (optional)
133        - dx: Horizontal offset in pixels from the base position (default: 0)
134        - dy: Vertical offset in pixels from the base position (default: 0)
135        - font_size: Custom font size in pixels (optional)
136
137        returns:
138        - self, to allow method chaining
139        """
140        self.story_layers.append({
141            'type': 'context', 
142            'text': text, 
143            'position': position,
144            'color': color or self.colors['context'],
145            'dx': dx,
146            'dy': dy,
147            'font_size' : font_size or self.em_to_px(self.font_sizes['context'])
148            
149        })
150        return self
151
152
153    def add_next_steps(self, 
154        #Basic parameters
155        text=None,
156        position='bottom',
157        mode=None,
158        title="What can we do next?",        
159    
160        # General styling parameters (used as fallback)
161        color=None,             # general color of elements
162        font_family='Arial',
163        font_size=14,
164        text_color=None,
165        opacity=None,
166        width=None,
167        height=None,
168        chart_width=None,
169        chart_height=None,
170        space=None,
171        
172        # List of texts for steps
173        texts=None,
174        
175        # Parameters per button
176        url=None,
177        button_width=120,
178        button_height=40,
179        button_color='#80C11E',
180        button_opacity=0.2,
181        button_corner_radius=5,
182        button_text_color='black',
183        button_font_family=None,    # If None, use font_family
184        button_font_size=None,      # If None, use font_size
185        
186        # Parameters for line_steps
187        line_steps_rect_width=10,
188        line_steps_rect_height=10,
189        line_steps_space=5,
190        line_steps_chart_width=700,
191        line_steps_chart_height=100,
192        line_steps_color='#80C11E',
193        line_steps_opacity=0.2,
194        line_steps_text_color='black',
195        line_steps_font_family=None,  # If None, use font_family
196        line_steps_font_size=None,    # If None, use font_size
197        
198        # Parameters for stair_steps
199        stair_steps_rect_width=10,
200        stair_steps_rect_height=3,
201        stair_steps_chart_width=700,
202        stair_steps_chart_height=300,
203        stair_steps_color='#80C11E',
204        stair_steps_opacity=0.2,
205        stair_steps_text_color='black',
206        stair_steps_font_family=None,  # If None, use font_family
207        stair_steps_font_size=None,    # If None, use font_size
208        
209        # Title Parameters
210        title_color='black',
211        title_font_family=None,     # If None, use font_family
212        title_font_size=None,       # If None, use font_size * 1.4
213        **kwargs):
214
215
216        """
217        Adds a next-step element to the visualisation with multiple customisation options.
218        
219        Parameters
220        ---------
221        text : str                                              Text for the basic version or for the button
222        position : str, default=‘bottom’                        Position of the element (‘bottom’, ‘top’, ‘left’, ‘right’)
223        type : str, optional                                    Display type (‘line_steps’, ‘button’, ‘stair_steps’)
224        title : str, default="What can we do next?’             Title for special visualisations
225        colour : str, optional                                  Text colour for the basic version
226        font_family : str, default=‘Arial’                      Default font
227        font_size : int, default=14                             Default font size
228        texts : list of str                                     List of texts for line_steps and stair_steps
229        url : str                                               URL for the button
230        
231        Parameters for Button
232        ------------------
233        button_width : int, default=120                         Button width
234        button_height : int, default=40                         Height of the button
235        button_color : str, default=‘#80C11E’                   Background colour of the button
236        button_opacity : float, default=0.2                     Opacity of the button
237        button_corner_radius : int, default=5                   Corner radius of the button
238        button_text_color : str, default=‘black’                Button text colour
239        button_font_family : str, optional                      Font specific to the button
240        button_font_size : int, optional                        Font size for the button
241        
242        Parameters for Line Steps
243        ----------------------
244        line_steps_rect_width : int, default=10                 Rectangle width
245        line_steps_rect_height : int, default=10                Height of rectangles
246        line_steps_space : int, default=5                       Space between rectangles
247        line_steps_chart_width : int, default=700               Total width of the chart
248        line_steps_chart_height : int, default=100              Total chart height
249        line_steps_color : str, default=‘#80C11E’               Colour of rectangles
250        line_steps_opacity : float, default=0.2                 Opacity of rectangles
251        line_steps_text_colour : str, default=‘black’           Text colour
252        line_steps_font_family : str, optional                  Font specific to line steps
253        line_steps_font_size : int, optional                    Font size for line steps
254        
255        Parameters for Stair Steps
256        -----------------------
257        stair_steps_rect_width : int, default=10                Width of steps
258        stair_steps_rect_height : int, default=3                Height of steps
259        stair_steps_chart_width : int, default=700              Total width of the chart
260        stair_steps_chart_height : int, default=300             Total height of the chart
261        stair_steps_color : str, default=‘#80C11E’              Colour of the steps
262        stair_steps_opacity : float, default=0.2                Opacity of the steps
263        stair_steps_text_colour : str, default=‘black’          Text colour
264        stair_steps_font_family : str,                          Font specific to stair steps
265        stair_steps_font_size : int, optional                   Font size for stair steps
266        
267        Title parameters
268        ----------------------
269        title_color : str, default=‘black’                      Title colour
270        title_font_family : str, optional                       Specific font for the title
271        title_font_size : int, optional                         Font size for the title
272        
273        Returns
274        -------
275        self : Story object                                     The current instance for method chaining
276        """
277 
278 
279        # Apply general parameters to specific parameters if not set
280        # Colors and styling
281        button_color = button_color or color
282        button_opacity = button_opacity or opacity
283        button_text_color = button_text_color or text_color
284        button_width = button_width or width
285        button_height = button_height or height
286        
287        line_steps_color = line_steps_color or color
288        line_steps_opacity = line_steps_opacity or opacity
289        line_steps_text_color = line_steps_text_color or text_color
290        line_steps_rect_width = line_steps_rect_width or width
291        line_steps_rect_height = line_steps_rect_height or height
292        line_steps_space = line_steps_space or space
293        line_steps_chart_width = line_steps_chart_width or chart_width
294        line_steps_chart_height = line_steps_chart_height or chart_height
295        
296        stair_steps_color = stair_steps_color or color
297        stair_steps_opacity = stair_steps_opacity or opacity
298        stair_steps_text_color = stair_steps_text_color or text_color
299        stair_steps_rect_width = stair_steps_rect_width or width
300        stair_steps_rect_height = stair_steps_rect_height or height
301        stair_steps_chart_width = stair_steps_chart_width or chart_width
302        stair_steps_chart_height = stair_steps_chart_height or chart_height
303        
304        title_color = title_color or text_color       
305
306    
307      
308        # Basic version (text only)
309        if mode is None:
310            if text is None:
311                raise ValueError("The parameter ‘text’ is required for the basic version")
312            self.story_layers.append({
313                'type': 'cta', 
314                'text': text, 
315                'position': position,
316                'color': color or self.colors['cta']
317            })
318            return self
319
320        # Mode validation
321        valid_types = ['line_steps', 'button', 'stair_steps']
322        if mode not in valid_types:
323            raise ValueError(f"Invalid type. Use one of: {', '.join(valid_types)}")
324
325        # Chart creation by mode
326        if mode == 'button':
327            if text is None:
328                raise ValueError("The parameter ‘text’ is required for the button type")
329            if url is None:
330                raise ValueError("The ‘url’ parameter is required for the button type")
331            
332            # Button chart creation
333            df = pd.DataFrame([{
334                'text': text,
335                'url': url,
336                'x': 0,
337                'y': 0
338            }])
339            
340            base = alt.Chart(df).encode(
341                x=alt.X('x:Q', axis=None),
342                y=alt.Y('y:Q', axis=None)
343            ).properties(
344                width=button_width,
345                height=button_height
346            )
347            
348            button_bg = base.mark_rect(
349                color=button_color,
350                opacity=button_opacity,
351                cornerRadius=button_corner_radius,
352                width=button_width,
353                height=button_height
354            ).encode(
355                href='url:N'
356            )
357            
358            button_text = base.mark_text(
359                fontSize=button_font_size or font_size,
360                font=button_font_family or font_family,
361                align='center',
362                baseline='middle',
363                color=button_text_color
364            ).encode(
365                text='text'
366            )
367            
368            chart = alt.layer(button_bg, button_text)
369            
370        elif mode == 'line_steps':
371            if not texts or not isinstance(texts, list):
372                raise ValueError("It is necessary to provide a list of texts for line_steps")
373            if len(texts) > 5:
374                raise ValueError("Maximum number of steps is 5")
375            if len(texts) < 1:
376                raise ValueError("Must provide at least one step")
377                
378            # Create DataFrame for rectangles and text
379            N = len(texts)
380            x = [i*(line_steps_rect_width+line_steps_space) for i in range(N)]
381            y = [0 for _ in range(N)]
382            x2 = [(i+1)*line_steps_rect_width+i*line_steps_space for i in range(N)]
383            y2 = [line_steps_rect_height for _ in range(N)]
384            
385            df_rect = pd.DataFrame({   
386                'x': x, 'y': y, 'x2': x2, 'y2': y2, 'text': texts
387            })
388            
389            # Create rectangles
390            rect = alt.Chart(df_rect).mark_rect(
391                color=line_steps_color,
392                opacity=line_steps_opacity
393            ).encode(
394                x=alt.X('x:Q', axis=None),
395                y=alt.Y('y:Q', axis=None),
396                x2='x2:Q',
397                y2='y2:Q'
398            ).properties(
399                width=line_steps_chart_width,
400                height=line_steps_chart_height
401            )
402            
403            # Add text labels
404            text = alt.Chart(df_rect).mark_text(
405                fontSize=line_steps_font_size or font_size,
406                font=line_steps_font_family or font_family,
407                align='left',
408                dx=10,
409                lineHeight=18,
410                color=line_steps_text_color
411            ).encode(
412                text='text:N',
413                x=alt.X('x:Q', axis=None),
414                y=alt.Y('y_half:Q', axis=None),
415            ).transform_calculate(
416                y_half='datum.y2/2'
417            )
418            
419            if N > 1:
420                df_line = pd.DataFrame({   
421                    'x': [line_steps_rect_width*i+line_steps_space*(i-1) for i in range(1,N)],
422                    'y': [line_steps_rect_height/2 for _ in range(N-1)],
423                    'x2': [(line_steps_rect_width+line_steps_space)*i for i in range(1,N)],
424                    'y2': [line_steps_rect_height/2 for _ in range(N-1)]
425                })
426                
427                line = alt.Chart(df_line).mark_line(
428                    point=True,
429                    strokeWidth=2
430                ).encode(
431                    x=alt.X('x:Q', axis=None),
432                    y=alt.Y('y:Q', axis=None),
433                    x2='x2:Q',
434                    y2='y2:Q'
435                )
436                
437                chart = alt.layer(rect, line, text)
438            else:
439                chart = alt.layer(rect, text)
440                
441        elif mode == 'stair_steps':
442            if not texts or not isinstance(texts, list):
443                raise ValueError("You must provide a list of texts for stair_steps")
444            if len(texts) > 5:
445                raise ValueError("Maximum number of steps is 5")
446            if len(texts) < 1:
447                raise ValueError("Must provide at least one step")
448                
449            # Create DataFrame for rectangles and text
450            N = len(texts)
451            x = [i*stair_steps_rect_width for i in range(N)]
452            y = [i*stair_steps_rect_height for i in range(N)]
453            x2 = [(i+1)*stair_steps_rect_width for i in range(N)]
454            y2 = [(i+1)*stair_steps_rect_height for i in range(N)]
455            
456            df_rect = pd.DataFrame({   
457                'x': x, 'y': y, 'x2': x2, 'y2': y2, 'text': texts
458            })
459            
460            # Create rectangles
461            rect = alt.Chart(df_rect).mark_rect(
462                color=stair_steps_color,
463                opacity=stair_steps_opacity
464            ).encode(
465                x=alt.X('x:Q', axis=None),
466                y=alt.Y('y:Q', axis=None, scale=alt.Scale(domain=[0, N*stair_steps_rect_height])),
467                x2='x2:Q',
468                y2='y2:Q'
469            ).properties(
470                width=stair_steps_chart_width,
471                height=stair_steps_chart_height
472            )
473            
474            # Add text labels
475            text = alt.Chart(df_rect).mark_text(
476                fontSize=stair_steps_font_size or font_size,
477                font=stair_steps_font_family or font_family,
478                align='left',
479                dx=10,
480                dy=0,
481                color=stair_steps_text_color
482            ).encode(
483                text=alt.Text('text'),
484                x=alt.X('x:Q', axis=None),
485                y=alt.Y('y_mid:Q', axis=None),
486            ).transform_calculate(
487                y_mid='(datum.y + datum.y2)/2'
488            )
489            
490            if N > 1:
491                line_data = []
492                for i in range(N-1):
493                    line_data.append({
494                        'x': x2[i],
495                        'y': y2[i],
496                        'x2': x[i+1],
497                        'y2': y[i+1]
498                    })
499                
500                df_line = pd.DataFrame(line_data)
501                
502                line = alt.Chart(df_line).mark_line(
503                    point=True,
504                    strokeWidth=2
505                ).encode(
506                    x=alt.X('x:Q', axis=None),
507                    y=alt.Y('y:Q', axis=None),
508                    x2='x2:Q',
509                    y2='y2:Q'
510                )
511                
512                chart = alt.layer(rect, line, text)
513            else:
514                chart = alt.layer(rect, text)
515
516        # Addition of title with customisable font
517        if title:
518            chart = chart.properties(
519                title=alt.TitleParams(
520                    text=[title],
521                    fontSize=title_font_size or (font_size * 1.4),
522                    font=title_font_family or font_family,
523                    color=title_color,
524                    offset=10
525                )
526            )
527
528        # Addition to layer
529        self.story_layers.append({
530            'type': 'special_cta',
531            'chart': chart,
532            'position': position
533        })
534        
535        return self
536    
537    def add_source(self, text, position='bottom', vertical=False, color=None, dx=None, dy=None, font_size=None):
538        """
539        Add a source layer to the story.
540
541        Parameters:
542        - text: The source text
543        - position: The position of the text (default: 'bottom')
544        - vertical: If True, the text will be rotated 90 degrees (default: False)
545        - color: Custom text color (optional)
546        - dx: Horizontal movement of the text (optional)
547        - dy: Vertical movement of the text (optional)
548        - font_size: Custom font size (optional)
549
550        Ritorna:
551        - self, to allow the method chaining
552        """
553        self.story_layers.append({
554            'type': 'source', 
555            'text': text, 
556            'position': position, 
557            'vertical': vertical,
558            'color': color or self.colors['source'],
559            'dx': dx or 0,
560            'dy': dy or 0,
561            'font_size': font_size or self.em_to_px(self.font_sizes['source'])
562            
563        })
564        return self
565
566    def add_annotation(self, x_point, y_point, annotation_text="Point of interest", 
567                                     arrow_direction='right', arrow_color='blue', arrow_size=40,
568                                     label_color='black', label_size=12,
569                                     show_point=True, point_color='red', point_size=60,
570                                     arrow_dx=0, arrow_dy=-45,
571                                     label_dx=37, label_dy=-37):
572        """
573        Create an arrow annotation on the graph.
574        
575        This method is essential for highlighting specific points in the graph and adding
576        contextual explanations. It is particularly useful for data narration, allowing
577        to guide the observer's attention to relevant aspects of the visualisation.
578
579        Parameters:
580        - x_point, y_point: Coordinates of the point to be annotated
581        - annotation_text: Text of the annotation (default: ‘Point of interest’)
582        - arrow_direction: Direction of the arrow (default: ‘right’)
583        - arrow_color, arrow_size: Arrow colour and size
584        - label_colour, label_size: Colour and size of the annotation text
585        - show_point: If True, shows a point at the annotation location
586        - point_color, point_size: Colour and size of the point
587        - arrow_dx, arrow_dy: Distances in pixels to be added to the arrow position (default:0, -45)
588        - label_dx, label_dy: Distances in pixels to be added to the label position (default:37, -37)
589
590        returns:
591        - self, to allow method chaining
592        """
593        
594        # Dictionary that maps arrow directions to corresponding Unicode symbols
595        arrow_symbols = {
596            'left': '←', 'right': '→', 'up': '↑', 'down': '↓',
597            'upleft': '↖', 'upright': '↗', 'downleft': '↙', 'downright': '↘',
598            'leftup': '↰', 'leftdown': '↲', 'rightup': '↱', 'rightdown': '↳',
599            'upleftcurve': '↺', 'uprightcurve': '↻'
600        }
601
602        # Check that the direction of the arrow is valid
603        if arrow_direction not in arrow_symbols:
604            raise ValueError(f"Invalid arrow direction. Use one of: {', '.join(arrow_symbols.keys())}")
605
606        # Select the appropriate arrow symbol
607        arrow_symbol = arrow_symbols[arrow_direction]
608        
609        # checks whether the encoding has already been defined
610        if not hasattr(self.chart, 'encoding')or not hasattr(self.chart.encoding, 'x'):
611            # If encoding is not defined use of default values
612            x_type = 'Q'
613            y_type = 'Q'
614        else:
615            # If encoding is defined, we extract the data types for the x and y axes
616            x_type = self.chart.encoding.x.shorthand.split(':')[-1]
617            y_type = self.chart.encoding.y.shorthand.split(':')[-1]
618
619        # Explanation of data type extraction:
620        # 1. We first check whether the encoding has been defined. This is important because
621        # the user may call this method before defining the encoding of the graph.
622        # (The encoding in Altair defines how the data is mapped to the visual properties of the graph).
623        # 2. If encoding is not defined, we use ‘Q’ (Quantitative) as the default data type
624        # for both axes. This allows the method to work even without a defined encoding.
625        # 3. If the encoding is defined, we proceed with the data type extraction as before:
626        # - self.chart.encoding.x.shorthand: accesses the shorthand of the x-axis
627        # .shorthand: In Altair, ‘shorthand’ is a concise notation for specifying the encoding.
628        # Example of shorthand: ‘column:Q’ where ‘column’ is the name of the data column
629        # and ‘Q’ is the data type (in this case, Quantitative).
630        # - split(‘:’)[-1]: splits the shorthand at ‘:’ and takes the last element (the data type)
631        # Example: If shorthand is ‘price:Q’, split(‘:’) will return [‘price’, ‘Q’].
632        # 4. The extracted data type will be one of:
633        # - ‘Q’: Quantitative (continuous numeric)
634        # - ‘O’: Ordinal (ordered categories)
635        # N': Nominal (unordered categories)
636        # T': Temporal (dates or times)
637        
638        # Important: This operation is essential to ensure that the annotations
639        # added to the graph are consistent with the original axis data types.
640        # Without this match, annotations may be positioned
641        # incorrectly or cause rendering errors.
642
643        # Create a DataFrame with a single point for the annotation
644        annotation_data = pd.DataFrame({'x': [x_point], 'y': [y_point]})
645        
646        # Internal function to create the correct encoding based on the data type
647        def create_encoding(field, dtype):
648            """
649            Creates the appropriate encoding for Altair based on the data type.
650            This function is crucial to ensure that the annotation is
651            consistent with the original chart data type.
652            """
653            
654            if dtype == 'O':  # Ordinal
655                return alt.X(field + ':O', title='') if field == 'x' else alt.Y(field + ':O', title='')
656            elif dtype == 'N':  # Nominal
657                return alt.X(field + ':N', title='') if field == 'x' else alt.Y(field + ':N', title='')
658            elif dtype == 'T':  # Temporal
659                return alt.X(field + ':T', title='') if field == 'x' else alt.Y(field + ':T', title='')
660            else:  # Quantity (default)
661                return alt.X(field + ':Q', title='') if field == 'x' else alt.Y(field + ':Q', title='')
662
663        # Initialises the list of annotation layers
664        layers = []
665
666        # Adds full stop if required
667        if show_point:
668            point_layer = alt.Chart(annotation_data).mark_point(
669                color=point_color,
670                size=point_size
671            ).encode(
672                x=create_encoding('x', x_type),
673                y=create_encoding('y', y_type)
674            )
675            layers.append(point_layer)
676
677        # Adds the arrow
678        # We use mark_text to draw the arrow using a Unicode character
679        arrow_layer = alt.Chart(annotation_data).mark_text(
680            text=arrow_symbol, 
681            fontSize=arrow_size,
682            dx=arrow_dx,  # Horizontal offset to position the arrow                   was 22
683            dy=arrow_dy,  # Vertical offset to position the arrow                     was -22
684            color=arrow_color
685        ).encode(
686            x=create_encoding('x', x_type),
687            y=create_encoding('y', y_type)
688        )
689        layers.append(arrow_layer)
690
691        # Adds text label
692        label_layer = alt.Chart(annotation_data).mark_text(
693            align='left',
694            baseline='top',
695            dx= label_dx,  # Horizontal offset to position text                     was 37
696            dy= label_dy,  # Vertical offset to position text                       was -37
697            fontSize=label_size,
698            color=label_color,
699            text=annotation_text
700        ).encode(
701            x=create_encoding('x', x_type),
702            y=create_encoding('y', y_type)
703        )
704        layers.append(label_layer)
705
706        # Combine all layers into a single annotation
707        annotation = alt.layer(*layers)
708
709        # Adds annotation to history layers
710        self.story_layers.append({
711            'type': 'annotation',
712            'chart': annotation
713        })
714
715        # Note on flexibility and robustness:
716        # This approach makes the add_annotation method more flexible,
717        # as it can automatically adapt to different types of graphs without
718        # requiring manual input on the axis data type. In addition, using
719        # the Altair shorthand, the code automatically adapts even if the user
720        # has specified the encoding in different ways (e.g., using alt.X(‘column:Q’)
721        # or alt.X(‘column’, type=‘quantitative’)).
722
723
724        return self  # Return self to allow method chaining
725    
726    
727    def add_line(self, value, orientation='horizontal', color='red', stroke_width=2, stroke_dash=[]):
728        """
729        Adds a reference line to the story.
730
731        Parameters:
732        - value: The position of the line (x or y value)
733        - orientation: 'horizontal' or 'vertical' (default: 'horizontal')
734        - color: Color of the line (default: 'red')
735        - stroke_width: Width of the line in pixels (default: 2)
736        - stroke_dash: Pattern for dashed line, e.g. [5,5] (default: [] for solid line)
737
738        Returns:
739        - self, to allow method chaining
740        """
741        
742        if orientation not in ['horizontal', 'vertical']:
743            raise ValueError("orientation must be either 'horizontal' or 'vertical'")
744
745        # Crea un DataFrame con un singolo valore
746        if orientation == 'horizontal':
747            data = pd.DataFrame({'y': [value]})
748            encoding = {'y': 'y:Q'}
749        else:  # vertical
750            data = pd.DataFrame({'x': [value]})
751            encoding = {'x': 'x:Q'}
752        
753        # definizione dei parametri della line
754        mark_params = {
755            'color': color,
756            'strokeWidth': stroke_width,
757        }
758        
759        # Viene aggiunto stokeDash solo se è specificato un pattern
760        if stroke_dash:
761            mark_params['strokeDash'] = stroke_dash
762
763        # Crea la linea
764        line = alt.Chart(data).mark_rule(
765            color=color,
766            strokeWidth=stroke_width,
767            strokeDash=stroke_dash
768        ).encode(
769            **encoding
770        )
771
772        # Aggiunge la linea ai layer della storia
773        self.story_layers.append({
774            'type': 'line',
775            'chart': line
776        })
777
778        return self
779    
780        
781
782    def em_to_px(self, em):
783        """
784        Converts a dimension from em to pixels.
785
786        This function is essential for maintaining visual consistency
787        between different textual elements and devices.
788
789        Parameters:
790        - em: Size in em
791
792        Returns:
793        - Equivalent size in pixels (integer)
794        """
795        return int(em * self.base_font_size)
796
797    def _get_position(self, position):
798        """
799        Calculates the x and y co-ordinates for positioning text elements in the graph.
800
801        This method is crucial for the correct positioning of various elements
802        narrative elements such as context, call-to-action and sources.
803
804        Parameters:
805        - position: String indicating the desired position (e.g. ‘left’, ‘right’, ‘top’, etc.).
806        
807        Returns:
808        - Tuple (x, y) representing the coordinates in pixels
809        """
810        # Dictionary mapping positions to coordinates (x, y)
811        # Co-ordinates are calculated from the size of the graph
812        positions = {
813            'left': (-100, self.chart.height / 2),  # 10 pixel from the left edge, centred vertically
814            'right': (self.chart.width + 20, self.chart.height / 2),  # 10 pixel from the right edge, centred vertically
815            'top': (self.chart.width / 2, 80),  # Centred horizontally, 80 pixels from above
816            'bottom': (self.chart.width / 2, self.chart.height + 40),  # Horizontally centred, 40 pixels from bottom
817            'center': (self.chart.width / 2, self.chart.height / 2),  # Graph Centre
818            'side-left': (-150, self.chart.height / 2),  # 10 pixels to the left of the border, centred vertically
819            'side-right': (self.chart.width + 50, self.chart.height / 2),  # 10 pixels to the right of the border, centred vertically
820        }
821        
822        # If the required position is not in the dictionary, use a default position
823        # In this case, horizontally centred and 20 pixels from the bottom
824        return positions.get(position, (self.chart.width / 2, self.chart.height - 20))
825
826    def create_title_layer(self, layer):
827        """
828        Creates the title layer (and subtitle if present).
829
830        This method is responsible for visually creating the main title
831        and the optional subtitle of the graphic.
832
833        Parameters:
834        - Layer: Dictionary containing the title information
835
836        Returns:
837        - Altair Chart object representing the title layer
838        """
839        title_chart = alt.Chart(self.chart.data).mark_text(
840            text=layer['title'],
841            fontSize=layer['title_font_size'],
842            fontWeight='bold',
843            align='center',
844            font=self.font,
845            color=layer['title_color']
846        ).encode(
847            x=alt.value(self.chart.width / 2 + layer.get('dx', 0)),  # Centre horizontally
848            y=alt.value(-50 + layer.get('dy', 0))  # Position 20 pixels from above
849        )
850        
851        if layer['subtitle']:
852            subtitle_chart = alt.Chart(self.chart.data).mark_text(
853                text=layer['subtitle'],
854                fontSize=layer['subtitle_font_size'],
855                align='center',
856                font=self.font,
857                color=layer['subtitle_color']
858            ).encode(
859                x=alt.value(self.chart.width / 2 + layer.get('s_dx', 0)),  # Centre horizontally
860                y=alt.value(-20 + layer.get('s_dy', 0))  # Position 50 pixels from the top (below the title)
861            )
862            return title_chart + subtitle_chart
863        return title_chart
864
865    def create_text_layer(self, layer):
866        """
867        Creates a generic text layer (context, next-steps, source).
868
869        This method is used to create text layers for various narrative purposes,
870        such as adding context, next-steps or data source information.
871
872        Parameters:
873        - Layer: Dictionary containing the text information to be added
874
875        Returns:
876        - Altair Chart object representing the text layer
877        """
878        x, y = self._get_position(layer['position'])
879        
880        # Apply offsets if they exist (for context layers)
881        if layer['type'] in ['context', 'source']:
882            x += layer.get('dx', 0)
883            y += layer.get('dy', 0)
884        
885        return alt.Chart(self.chart.data).mark_text(
886            text=layer['text'],
887            fontSize=layer.get('font_size', self.em_to_px(self.font_sizes[layer['type']])),
888            align='center',
889            baseline='middle',
890            font=self.font,
891            angle=270 if layer.get('vertical', False) else 0,  #  Rotate text if ‘vertical’ is True
892            color=layer['color']
893        ).encode(
894            x=alt.value(x),
895            y=alt.value(y)
896        )
897
898    def configure_view(self, *args, **kwargs):
899        """
900        Configure aspects of the graph view using Altair's configure_view method.
901
902        This method allows you to configure various aspects of the chart view, such as
903        the background colour, border style, internal spacing, etc.
904
905        Parameters:
906        *args, **kwargs: Arguments to pass to the Altair configure_view method.
907
908        stores the view configuration for application during rendering.
909        """
910        self.config['view'] = kwargs
911        return self
912
913
914    def render(self):
915        """
916        It renders all layers of the story in a single graphic.       
917        """
918        # Let's start with the basic graph
919        main_chart = self.chart
920        
921        # Create separate lists to place special graphics
922        top_charts = []
923        bottom_charts = []
924        left_charts = []
925        right_charts = []
926        overlay_charts = []
927        
928        # Organise the layers according to their position
929        for layer in self.story_layers:
930            if layer['type'] == 'special_cta':
931                # We take the position from the layer
932                if layer.get('position') == 'top':
933                    top_charts.append(layer['chart'])
934                elif layer.get('position') == 'bottom':
935                    bottom_charts.append(layer['chart'])
936                elif layer.get('position') == 'left':
937                    left_charts.append(layer['chart'])
938                elif layer.get('position') == 'right':
939                    right_charts.append(layer['chart'])
940            elif layer['type'] == 'title':
941                overlay_charts.append(self.create_title_layer(layer))
942            elif layer['type'] in ['context', 'cta', 'source']:
943                overlay_charts.append(self.create_text_layer(layer))
944            elif layer['type'] in ['shape', 'shape_label', 'annotation']:
945                overlay_charts.append(layer['chart'])
946            elif layer['type'] == 'line':
947                overlay_charts.append(layer['chart'])
948
949        # Overlaying the layers on the main graph
950        for overlay in overlay_charts:
951            main_chart += overlay
952
953        # Build the final layout
954        if left_charts:
955            main_chart = alt.hconcat(alt.vconcat(*left_charts), main_chart)
956        if right_charts:
957            main_chart = alt.hconcat(main_chart, alt.vconcat(*right_charts))
958        if top_charts:
959            main_chart = alt.vconcat(alt.hconcat(*top_charts), main_chart)
960        if bottom_charts:
961            main_chart = alt.vconcat(main_chart, alt.hconcat(*bottom_charts))
962
963        # Apply configurations
964        if 'view' in self.config:
965            main_chart = main_chart.configure_view(**self.config['view'])
966
967        return main_chart.resolve_axis(x='independent', y='independent')

Story class: Implements a structure for creating narrative views of data.

This class extends the functionality of Altair to include narrative elements such as titles, contexts, call-to-action and annotations. It is designed to facilitate the creation of more engaging and informative data visualisations.

Story( data=None, width=600, height=400, font='Arial', base_font_size=16, **kwargs)
14    def __init__(self, data=None, width=600, height=400, font='Arial', base_font_size=16, **kwargs):
15        """
16        Initialise a Story object.
17
18        Parameters:
19        - data: DataFrame or URL for graph data  (default: None)
20        - width: Graph width in pixels (default: 600)
21        - height: Height of the graph in pixels (default: 400)
22        - font: Font to be used for all text elements (default: 'Arial')
23        - base_font_size: Basic font size in pixels (default: 16)
24        - **kwargs: Additional parameters to be passed to the constructor of alt.Chart
25        """
26        # Initialising the Altair Chart object with basic parameters
27        self.chart = alt.Chart(data, width=width, height=height, **kwargs)
28        self.font = font
29        self.base_font_size = base_font_size
30        self.story_layers = []  # List for storing history layers
31        
32        # Dictionaries for the sizes and colours of various text elements
33        # These values are multipliers for base_font_size
34        self.font_sizes = {
35            'title': 2,        # The title will be twice as big as the basic font
36            'subtitle': 1.5,   # The subtitle will be 1.5 times bigger
37            'context': 1.2,    # The context text will be 1.2 times larger
38            'nextstep': 1.3,   # Next-Step text will be 1.3 times larger
39            'source': 1        # The source text will have the basic size
40        }
41        # Predefined colours for various text elements
42        self.colors = {
43            'title': 'black',
44            'subtitle': 'gray',
45            'context': 'black',
46            'nextstep': 'black',
47            'source': 'gray'
48        }
49        self.config = {}

Initialise a Story object.

Parameters:

  • data: DataFrame or URL for graph data (default: None)
  • width: Graph width in pixels (default: 600)
  • height: Height of the graph in pixels (default: 400)
  • font: Font to be used for all text elements (default: 'Arial')
  • base_font_size: Basic font size in pixels (default: 16)
  • **kwargs: Additional parameters to be passed to the constructor of alt.Chart
chart
font
base_font_size
story_layers
font_sizes
colors
config
def add_title( self, title, subtitle=None, title_color=None, subtitle_color=None, title_font_size=None, subtitle_font_size=None, dx=None, dy=None, s_dx=None, s_dy=None):
 91    def add_title(self, title, subtitle=None, title_color=None, subtitle_color=None, title_font_size=None, subtitle_font_size=None, dx=None, dy=None, s_dx=None, s_dy=None):
 92        """
 93        Adds a title layer (and optional subtitle) to the story.
 94
 95        Parameters:
 96        - title: Main title text
 97        - subtitle: Subtitle text (optional)
 98        - title_color: Custom colour for the title (optional)
 99        - subtitle_color: Custom colour for the subtitle (optional)
100        - title_font_size: Custom font size for title in pixels (optional)
101        - subtitle_font_size: Custom font size for subtitle in pixels (optional)
102        - dx: Horizontal offset of the title in pixels (optional)
103        - dy: Vertical offset of the title in pixels (optional)
104        - s_dx: Horizontal offset of the subtitle in pixels (optional)
105        - s_dy: Vertical offset of the subtitle in pixels (optional)
106
107        returns:
108        - self, to allow method chaining
109        """
110        self.story_layers.append({
111            'type': 'title', 
112            'title': title, 
113            'subtitle': subtitle,
114            'title_color': title_color or self.colors['title'],
115            'subtitle_color': subtitle_color or self.colors['subtitle'],
116            'title_font_size': title_font_size or self.em_to_px(self.font_sizes['title']),
117            'subtitle_font_size': subtitle_font_size or self.em_to_px(self.font_sizes['subtitle']),
118            'dx': dx or 0,
119            'dy': dy or 0,
120            's_dx': s_dx or 0,
121            's_dy': s_dy or 0
122        })
123        return self

Adds a title layer (and optional subtitle) to the story.

Parameters:

  • title: Main title text
  • subtitle: Subtitle text (optional)
  • title_color: Custom colour for the title (optional)
  • subtitle_color: Custom colour for the subtitle (optional)
  • title_font_size: Custom font size for title in pixels (optional)
  • subtitle_font_size: Custom font size for subtitle in pixels (optional)
  • dx: Horizontal offset of the title in pixels (optional)
  • dy: Vertical offset of the title in pixels (optional)
  • s_dx: Horizontal offset of the subtitle in pixels (optional)
  • s_dy: Vertical offset of the subtitle in pixels (optional)

returns:

  • self, to allow method chaining
def add_context(self, text, position='left', color=None, dx=0, dy=0, font_size=None):
125    def add_context(self, text, position='left', color=None, dx=0, dy=0, font_size=None):
126        """
127        Adds a context layer to the story.
128
129        Parameters:
130        - text: The context text to be added
131        - position: The position of the text (default: ‘left’)
132        - colour: Custom text colour (optional)
133        - dx: Horizontal offset in pixels from the base position (default: 0)
134        - dy: Vertical offset in pixels from the base position (default: 0)
135        - font_size: Custom font size in pixels (optional)
136
137        returns:
138        - self, to allow method chaining
139        """
140        self.story_layers.append({
141            'type': 'context', 
142            'text': text, 
143            'position': position,
144            'color': color or self.colors['context'],
145            'dx': dx,
146            'dy': dy,
147            'font_size' : font_size or self.em_to_px(self.font_sizes['context'])
148            
149        })
150        return self

Adds a context layer to the story.

Parameters:

  • text: The context text to be added
  • position: The position of the text (default: ‘left’)
  • colour: Custom text colour (optional)
  • dx: Horizontal offset in pixels from the base position (default: 0)
  • dy: Vertical offset in pixels from the base position (default: 0)
  • font_size: Custom font size in pixels (optional)

returns:

  • self, to allow method chaining
def add_next_steps( self, text=None, position='bottom', mode=None, title='What can we do next?', color=None, font_family='Arial', font_size=14, text_color=None, opacity=None, width=None, height=None, chart_width=None, chart_height=None, space=None, texts=None, url=None, button_width=120, button_height=40, button_color='#80C11E', button_opacity=0.2, button_corner_radius=5, button_text_color='black', button_font_family=None, button_font_size=None, line_steps_rect_width=10, line_steps_rect_height=10, line_steps_space=5, line_steps_chart_width=700, line_steps_chart_height=100, line_steps_color='#80C11E', line_steps_opacity=0.2, line_steps_text_color='black', line_steps_font_family=None, line_steps_font_size=None, stair_steps_rect_width=10, stair_steps_rect_height=3, stair_steps_chart_width=700, stair_steps_chart_height=300, stair_steps_color='#80C11E', stair_steps_opacity=0.2, stair_steps_text_color='black', stair_steps_font_family=None, stair_steps_font_size=None, title_color='black', title_font_family=None, title_font_size=None, **kwargs):
153    def add_next_steps(self, 
154        #Basic parameters
155        text=None,
156        position='bottom',
157        mode=None,
158        title="What can we do next?",        
159    
160        # General styling parameters (used as fallback)
161        color=None,             # general color of elements
162        font_family='Arial',
163        font_size=14,
164        text_color=None,
165        opacity=None,
166        width=None,
167        height=None,
168        chart_width=None,
169        chart_height=None,
170        space=None,
171        
172        # List of texts for steps
173        texts=None,
174        
175        # Parameters per button
176        url=None,
177        button_width=120,
178        button_height=40,
179        button_color='#80C11E',
180        button_opacity=0.2,
181        button_corner_radius=5,
182        button_text_color='black',
183        button_font_family=None,    # If None, use font_family
184        button_font_size=None,      # If None, use font_size
185        
186        # Parameters for line_steps
187        line_steps_rect_width=10,
188        line_steps_rect_height=10,
189        line_steps_space=5,
190        line_steps_chart_width=700,
191        line_steps_chart_height=100,
192        line_steps_color='#80C11E',
193        line_steps_opacity=0.2,
194        line_steps_text_color='black',
195        line_steps_font_family=None,  # If None, use font_family
196        line_steps_font_size=None,    # If None, use font_size
197        
198        # Parameters for stair_steps
199        stair_steps_rect_width=10,
200        stair_steps_rect_height=3,
201        stair_steps_chart_width=700,
202        stair_steps_chart_height=300,
203        stair_steps_color='#80C11E',
204        stair_steps_opacity=0.2,
205        stair_steps_text_color='black',
206        stair_steps_font_family=None,  # If None, use font_family
207        stair_steps_font_size=None,    # If None, use font_size
208        
209        # Title Parameters
210        title_color='black',
211        title_font_family=None,     # If None, use font_family
212        title_font_size=None,       # If None, use font_size * 1.4
213        **kwargs):
214
215
216        """
217        Adds a next-step element to the visualisation with multiple customisation options.
218        
219        Parameters
220        ---------
221        text : str                                              Text for the basic version or for the button
222        position : str, default=‘bottom’                        Position of the element (‘bottom’, ‘top’, ‘left’, ‘right’)
223        type : str, optional                                    Display type (‘line_steps’, ‘button’, ‘stair_steps’)
224        title : str, default="What can we do next?’             Title for special visualisations
225        colour : str, optional                                  Text colour for the basic version
226        font_family : str, default=‘Arial’                      Default font
227        font_size : int, default=14                             Default font size
228        texts : list of str                                     List of texts for line_steps and stair_steps
229        url : str                                               URL for the button
230        
231        Parameters for Button
232        ------------------
233        button_width : int, default=120                         Button width
234        button_height : int, default=40                         Height of the button
235        button_color : str, default=‘#80C11E’                   Background colour of the button
236        button_opacity : float, default=0.2                     Opacity of the button
237        button_corner_radius : int, default=5                   Corner radius of the button
238        button_text_color : str, default=‘black’                Button text colour
239        button_font_family : str, optional                      Font specific to the button
240        button_font_size : int, optional                        Font size for the button
241        
242        Parameters for Line Steps
243        ----------------------
244        line_steps_rect_width : int, default=10                 Rectangle width
245        line_steps_rect_height : int, default=10                Height of rectangles
246        line_steps_space : int, default=5                       Space between rectangles
247        line_steps_chart_width : int, default=700               Total width of the chart
248        line_steps_chart_height : int, default=100              Total chart height
249        line_steps_color : str, default=‘#80C11E’               Colour of rectangles
250        line_steps_opacity : float, default=0.2                 Opacity of rectangles
251        line_steps_text_colour : str, default=‘black’           Text colour
252        line_steps_font_family : str, optional                  Font specific to line steps
253        line_steps_font_size : int, optional                    Font size for line steps
254        
255        Parameters for Stair Steps
256        -----------------------
257        stair_steps_rect_width : int, default=10                Width of steps
258        stair_steps_rect_height : int, default=3                Height of steps
259        stair_steps_chart_width : int, default=700              Total width of the chart
260        stair_steps_chart_height : int, default=300             Total height of the chart
261        stair_steps_color : str, default=‘#80C11E’              Colour of the steps
262        stair_steps_opacity : float, default=0.2                Opacity of the steps
263        stair_steps_text_colour : str, default=‘black’          Text colour
264        stair_steps_font_family : str,                          Font specific to stair steps
265        stair_steps_font_size : int, optional                   Font size for stair steps
266        
267        Title parameters
268        ----------------------
269        title_color : str, default=‘black’                      Title colour
270        title_font_family : str, optional                       Specific font for the title
271        title_font_size : int, optional                         Font size for the title
272        
273        Returns
274        -------
275        self : Story object                                     The current instance for method chaining
276        """
277 
278 
279        # Apply general parameters to specific parameters if not set
280        # Colors and styling
281        button_color = button_color or color
282        button_opacity = button_opacity or opacity
283        button_text_color = button_text_color or text_color
284        button_width = button_width or width
285        button_height = button_height or height
286        
287        line_steps_color = line_steps_color or color
288        line_steps_opacity = line_steps_opacity or opacity
289        line_steps_text_color = line_steps_text_color or text_color
290        line_steps_rect_width = line_steps_rect_width or width
291        line_steps_rect_height = line_steps_rect_height or height
292        line_steps_space = line_steps_space or space
293        line_steps_chart_width = line_steps_chart_width or chart_width
294        line_steps_chart_height = line_steps_chart_height or chart_height
295        
296        stair_steps_color = stair_steps_color or color
297        stair_steps_opacity = stair_steps_opacity or opacity
298        stair_steps_text_color = stair_steps_text_color or text_color
299        stair_steps_rect_width = stair_steps_rect_width or width
300        stair_steps_rect_height = stair_steps_rect_height or height
301        stair_steps_chart_width = stair_steps_chart_width or chart_width
302        stair_steps_chart_height = stair_steps_chart_height or chart_height
303        
304        title_color = title_color or text_color       
305
306    
307      
308        # Basic version (text only)
309        if mode is None:
310            if text is None:
311                raise ValueError("The parameter ‘text’ is required for the basic version")
312            self.story_layers.append({
313                'type': 'cta', 
314                'text': text, 
315                'position': position,
316                'color': color or self.colors['cta']
317            })
318            return self
319
320        # Mode validation
321        valid_types = ['line_steps', 'button', 'stair_steps']
322        if mode not in valid_types:
323            raise ValueError(f"Invalid type. Use one of: {', '.join(valid_types)}")
324
325        # Chart creation by mode
326        if mode == 'button':
327            if text is None:
328                raise ValueError("The parameter ‘text’ is required for the button type")
329            if url is None:
330                raise ValueError("The ‘url’ parameter is required for the button type")
331            
332            # Button chart creation
333            df = pd.DataFrame([{
334                'text': text,
335                'url': url,
336                'x': 0,
337                'y': 0
338            }])
339            
340            base = alt.Chart(df).encode(
341                x=alt.X('x:Q', axis=None),
342                y=alt.Y('y:Q', axis=None)
343            ).properties(
344                width=button_width,
345                height=button_height
346            )
347            
348            button_bg = base.mark_rect(
349                color=button_color,
350                opacity=button_opacity,
351                cornerRadius=button_corner_radius,
352                width=button_width,
353                height=button_height
354            ).encode(
355                href='url:N'
356            )
357            
358            button_text = base.mark_text(
359                fontSize=button_font_size or font_size,
360                font=button_font_family or font_family,
361                align='center',
362                baseline='middle',
363                color=button_text_color
364            ).encode(
365                text='text'
366            )
367            
368            chart = alt.layer(button_bg, button_text)
369            
370        elif mode == 'line_steps':
371            if not texts or not isinstance(texts, list):
372                raise ValueError("It is necessary to provide a list of texts for line_steps")
373            if len(texts) > 5:
374                raise ValueError("Maximum number of steps is 5")
375            if len(texts) < 1:
376                raise ValueError("Must provide at least one step")
377                
378            # Create DataFrame for rectangles and text
379            N = len(texts)
380            x = [i*(line_steps_rect_width+line_steps_space) for i in range(N)]
381            y = [0 for _ in range(N)]
382            x2 = [(i+1)*line_steps_rect_width+i*line_steps_space for i in range(N)]
383            y2 = [line_steps_rect_height for _ in range(N)]
384            
385            df_rect = pd.DataFrame({   
386                'x': x, 'y': y, 'x2': x2, 'y2': y2, 'text': texts
387            })
388            
389            # Create rectangles
390            rect = alt.Chart(df_rect).mark_rect(
391                color=line_steps_color,
392                opacity=line_steps_opacity
393            ).encode(
394                x=alt.X('x:Q', axis=None),
395                y=alt.Y('y:Q', axis=None),
396                x2='x2:Q',
397                y2='y2:Q'
398            ).properties(
399                width=line_steps_chart_width,
400                height=line_steps_chart_height
401            )
402            
403            # Add text labels
404            text = alt.Chart(df_rect).mark_text(
405                fontSize=line_steps_font_size or font_size,
406                font=line_steps_font_family or font_family,
407                align='left',
408                dx=10,
409                lineHeight=18,
410                color=line_steps_text_color
411            ).encode(
412                text='text:N',
413                x=alt.X('x:Q', axis=None),
414                y=alt.Y('y_half:Q', axis=None),
415            ).transform_calculate(
416                y_half='datum.y2/2'
417            )
418            
419            if N > 1:
420                df_line = pd.DataFrame({   
421                    'x': [line_steps_rect_width*i+line_steps_space*(i-1) for i in range(1,N)],
422                    'y': [line_steps_rect_height/2 for _ in range(N-1)],
423                    'x2': [(line_steps_rect_width+line_steps_space)*i for i in range(1,N)],
424                    'y2': [line_steps_rect_height/2 for _ in range(N-1)]
425                })
426                
427                line = alt.Chart(df_line).mark_line(
428                    point=True,
429                    strokeWidth=2
430                ).encode(
431                    x=alt.X('x:Q', axis=None),
432                    y=alt.Y('y:Q', axis=None),
433                    x2='x2:Q',
434                    y2='y2:Q'
435                )
436                
437                chart = alt.layer(rect, line, text)
438            else:
439                chart = alt.layer(rect, text)
440                
441        elif mode == 'stair_steps':
442            if not texts or not isinstance(texts, list):
443                raise ValueError("You must provide a list of texts for stair_steps")
444            if len(texts) > 5:
445                raise ValueError("Maximum number of steps is 5")
446            if len(texts) < 1:
447                raise ValueError("Must provide at least one step")
448                
449            # Create DataFrame for rectangles and text
450            N = len(texts)
451            x = [i*stair_steps_rect_width for i in range(N)]
452            y = [i*stair_steps_rect_height for i in range(N)]
453            x2 = [(i+1)*stair_steps_rect_width for i in range(N)]
454            y2 = [(i+1)*stair_steps_rect_height for i in range(N)]
455            
456            df_rect = pd.DataFrame({   
457                'x': x, 'y': y, 'x2': x2, 'y2': y2, 'text': texts
458            })
459            
460            # Create rectangles
461            rect = alt.Chart(df_rect).mark_rect(
462                color=stair_steps_color,
463                opacity=stair_steps_opacity
464            ).encode(
465                x=alt.X('x:Q', axis=None),
466                y=alt.Y('y:Q', axis=None, scale=alt.Scale(domain=[0, N*stair_steps_rect_height])),
467                x2='x2:Q',
468                y2='y2:Q'
469            ).properties(
470                width=stair_steps_chart_width,
471                height=stair_steps_chart_height
472            )
473            
474            # Add text labels
475            text = alt.Chart(df_rect).mark_text(
476                fontSize=stair_steps_font_size or font_size,
477                font=stair_steps_font_family or font_family,
478                align='left',
479                dx=10,
480                dy=0,
481                color=stair_steps_text_color
482            ).encode(
483                text=alt.Text('text'),
484                x=alt.X('x:Q', axis=None),
485                y=alt.Y('y_mid:Q', axis=None),
486            ).transform_calculate(
487                y_mid='(datum.y + datum.y2)/2'
488            )
489            
490            if N > 1:
491                line_data = []
492                for i in range(N-1):
493                    line_data.append({
494                        'x': x2[i],
495                        'y': y2[i],
496                        'x2': x[i+1],
497                        'y2': y[i+1]
498                    })
499                
500                df_line = pd.DataFrame(line_data)
501                
502                line = alt.Chart(df_line).mark_line(
503                    point=True,
504                    strokeWidth=2
505                ).encode(
506                    x=alt.X('x:Q', axis=None),
507                    y=alt.Y('y:Q', axis=None),
508                    x2='x2:Q',
509                    y2='y2:Q'
510                )
511                
512                chart = alt.layer(rect, line, text)
513            else:
514                chart = alt.layer(rect, text)
515
516        # Addition of title with customisable font
517        if title:
518            chart = chart.properties(
519                title=alt.TitleParams(
520                    text=[title],
521                    fontSize=title_font_size or (font_size * 1.4),
522                    font=title_font_family or font_family,
523                    color=title_color,
524                    offset=10
525                )
526            )
527
528        # Addition to layer
529        self.story_layers.append({
530            'type': 'special_cta',
531            'chart': chart,
532            'position': position
533        })
534        
535        return self

Adds a next-step element to the visualisation with multiple customisation options.

Parameters

text : str Text for the basic version or for the button position : str, default=‘bottom’ Position of the element (‘bottom’, ‘top’, ‘left’, ‘right’) type : str, optional Display type (‘line_steps’, ‘button’, ‘stair_steps’) title : str, default="What can we do next?’ Title for special visualisations colour : str, optional Text colour for the basic version font_family : str, default=‘Arial’ Default font font_size : int, default=14 Default font size texts : list of str List of texts for line_steps and stair_steps url : str URL for the button

Parameters for Button

button_width : int, default=120 Button width button_height : int, default=40 Height of the button button_color : str, default=‘#80C11E’ Background colour of the button button_opacity : float, default=0.2 Opacity of the button button_corner_radius : int, default=5 Corner radius of the button button_text_color : str, default=‘black’ Button text colour button_font_family : str, optional Font specific to the button button_font_size : int, optional Font size for the button

Parameters for Line Steps

line_steps_rect_width : int, default=10 Rectangle width line_steps_rect_height : int, default=10 Height of rectangles line_steps_space : int, default=5 Space between rectangles line_steps_chart_width : int, default=700 Total width of the chart line_steps_chart_height : int, default=100 Total chart height line_steps_color : str, default=‘#80C11E’ Colour of rectangles line_steps_opacity : float, default=0.2 Opacity of rectangles line_steps_text_colour : str, default=‘black’ Text colour line_steps_font_family : str, optional Font specific to line steps line_steps_font_size : int, optional Font size for line steps

Parameters for Stair Steps

stair_steps_rect_width : int, default=10 Width of steps stair_steps_rect_height : int, default=3 Height of steps stair_steps_chart_width : int, default=700 Total width of the chart stair_steps_chart_height : int, default=300 Total height of the chart stair_steps_color : str, default=‘#80C11E’ Colour of the steps stair_steps_opacity : float, default=0.2 Opacity of the steps stair_steps_text_colour : str, default=‘black’ Text colour stair_steps_font_family : str, Font specific to stair steps stair_steps_font_size : int, optional Font size for stair steps

Title parameters

title_color : str, default=‘black’ Title colour title_font_family : str, optional Specific font for the title title_font_size : int, optional Font size for the title

Returns

self : Story object The current instance for method chaining

def add_source( self, text, position='bottom', vertical=False, color=None, dx=None, dy=None, font_size=None):
537    def add_source(self, text, position='bottom', vertical=False, color=None, dx=None, dy=None, font_size=None):
538        """
539        Add a source layer to the story.
540
541        Parameters:
542        - text: The source text
543        - position: The position of the text (default: 'bottom')
544        - vertical: If True, the text will be rotated 90 degrees (default: False)
545        - color: Custom text color (optional)
546        - dx: Horizontal movement of the text (optional)
547        - dy: Vertical movement of the text (optional)
548        - font_size: Custom font size (optional)
549
550        Ritorna:
551        - self, to allow the method chaining
552        """
553        self.story_layers.append({
554            'type': 'source', 
555            'text': text, 
556            'position': position, 
557            'vertical': vertical,
558            'color': color or self.colors['source'],
559            'dx': dx or 0,
560            'dy': dy or 0,
561            'font_size': font_size or self.em_to_px(self.font_sizes['source'])
562            
563        })
564        return self

Add a source layer to the story.

Parameters:

  • text: The source text
  • position: The position of the text (default: 'bottom')
  • vertical: If True, the text will be rotated 90 degrees (default: False)
  • color: Custom text color (optional)
  • dx: Horizontal movement of the text (optional)
  • dy: Vertical movement of the text (optional)
  • font_size: Custom font size (optional)

Ritorna:

  • self, to allow the method chaining
def add_annotation( self, x_point, y_point, annotation_text='Point of interest', arrow_direction='right', arrow_color='blue', arrow_size=40, label_color='black', label_size=12, show_point=True, point_color='red', point_size=60, arrow_dx=0, arrow_dy=-45, label_dx=37, label_dy=-37):
566    def add_annotation(self, x_point, y_point, annotation_text="Point of interest", 
567                                     arrow_direction='right', arrow_color='blue', arrow_size=40,
568                                     label_color='black', label_size=12,
569                                     show_point=True, point_color='red', point_size=60,
570                                     arrow_dx=0, arrow_dy=-45,
571                                     label_dx=37, label_dy=-37):
572        """
573        Create an arrow annotation on the graph.
574        
575        This method is essential for highlighting specific points in the graph and adding
576        contextual explanations. It is particularly useful for data narration, allowing
577        to guide the observer's attention to relevant aspects of the visualisation.
578
579        Parameters:
580        - x_point, y_point: Coordinates of the point to be annotated
581        - annotation_text: Text of the annotation (default: ‘Point of interest’)
582        - arrow_direction: Direction of the arrow (default: ‘right’)
583        - arrow_color, arrow_size: Arrow colour and size
584        - label_colour, label_size: Colour and size of the annotation text
585        - show_point: If True, shows a point at the annotation location
586        - point_color, point_size: Colour and size of the point
587        - arrow_dx, arrow_dy: Distances in pixels to be added to the arrow position (default:0, -45)
588        - label_dx, label_dy: Distances in pixels to be added to the label position (default:37, -37)
589
590        returns:
591        - self, to allow method chaining
592        """
593        
594        # Dictionary that maps arrow directions to corresponding Unicode symbols
595        arrow_symbols = {
596            'left': '←', 'right': '→', 'up': '↑', 'down': '↓',
597            'upleft': '↖', 'upright': '↗', 'downleft': '↙', 'downright': '↘',
598            'leftup': '↰', 'leftdown': '↲', 'rightup': '↱', 'rightdown': '↳',
599            'upleftcurve': '↺', 'uprightcurve': '↻'
600        }
601
602        # Check that the direction of the arrow is valid
603        if arrow_direction not in arrow_symbols:
604            raise ValueError(f"Invalid arrow direction. Use one of: {', '.join(arrow_symbols.keys())}")
605
606        # Select the appropriate arrow symbol
607        arrow_symbol = arrow_symbols[arrow_direction]
608        
609        # checks whether the encoding has already been defined
610        if not hasattr(self.chart, 'encoding')or not hasattr(self.chart.encoding, 'x'):
611            # If encoding is not defined use of default values
612            x_type = 'Q'
613            y_type = 'Q'
614        else:
615            # If encoding is defined, we extract the data types for the x and y axes
616            x_type = self.chart.encoding.x.shorthand.split(':')[-1]
617            y_type = self.chart.encoding.y.shorthand.split(':')[-1]
618
619        # Explanation of data type extraction:
620        # 1. We first check whether the encoding has been defined. This is important because
621        # the user may call this method before defining the encoding of the graph.
622        # (The encoding in Altair defines how the data is mapped to the visual properties of the graph).
623        # 2. If encoding is not defined, we use ‘Q’ (Quantitative) as the default data type
624        # for both axes. This allows the method to work even without a defined encoding.
625        # 3. If the encoding is defined, we proceed with the data type extraction as before:
626        # - self.chart.encoding.x.shorthand: accesses the shorthand of the x-axis
627        # .shorthand: In Altair, ‘shorthand’ is a concise notation for specifying the encoding.
628        # Example of shorthand: ‘column:Q’ where ‘column’ is the name of the data column
629        # and ‘Q’ is the data type (in this case, Quantitative).
630        # - split(‘:’)[-1]: splits the shorthand at ‘:’ and takes the last element (the data type)
631        # Example: If shorthand is ‘price:Q’, split(‘:’) will return [‘price’, ‘Q’].
632        # 4. The extracted data type will be one of:
633        # - ‘Q’: Quantitative (continuous numeric)
634        # - ‘O’: Ordinal (ordered categories)
635        # N': Nominal (unordered categories)
636        # T': Temporal (dates or times)
637        
638        # Important: This operation is essential to ensure that the annotations
639        # added to the graph are consistent with the original axis data types.
640        # Without this match, annotations may be positioned
641        # incorrectly or cause rendering errors.
642
643        # Create a DataFrame with a single point for the annotation
644        annotation_data = pd.DataFrame({'x': [x_point], 'y': [y_point]})
645        
646        # Internal function to create the correct encoding based on the data type
647        def create_encoding(field, dtype):
648            """
649            Creates the appropriate encoding for Altair based on the data type.
650            This function is crucial to ensure that the annotation is
651            consistent with the original chart data type.
652            """
653            
654            if dtype == 'O':  # Ordinal
655                return alt.X(field + ':O', title='') if field == 'x' else alt.Y(field + ':O', title='')
656            elif dtype == 'N':  # Nominal
657                return alt.X(field + ':N', title='') if field == 'x' else alt.Y(field + ':N', title='')
658            elif dtype == 'T':  # Temporal
659                return alt.X(field + ':T', title='') if field == 'x' else alt.Y(field + ':T', title='')
660            else:  # Quantity (default)
661                return alt.X(field + ':Q', title='') if field == 'x' else alt.Y(field + ':Q', title='')
662
663        # Initialises the list of annotation layers
664        layers = []
665
666        # Adds full stop if required
667        if show_point:
668            point_layer = alt.Chart(annotation_data).mark_point(
669                color=point_color,
670                size=point_size
671            ).encode(
672                x=create_encoding('x', x_type),
673                y=create_encoding('y', y_type)
674            )
675            layers.append(point_layer)
676
677        # Adds the arrow
678        # We use mark_text to draw the arrow using a Unicode character
679        arrow_layer = alt.Chart(annotation_data).mark_text(
680            text=arrow_symbol, 
681            fontSize=arrow_size,
682            dx=arrow_dx,  # Horizontal offset to position the arrow                   was 22
683            dy=arrow_dy,  # Vertical offset to position the arrow                     was -22
684            color=arrow_color
685        ).encode(
686            x=create_encoding('x', x_type),
687            y=create_encoding('y', y_type)
688        )
689        layers.append(arrow_layer)
690
691        # Adds text label
692        label_layer = alt.Chart(annotation_data).mark_text(
693            align='left',
694            baseline='top',
695            dx= label_dx,  # Horizontal offset to position text                     was 37
696            dy= label_dy,  # Vertical offset to position text                       was -37
697            fontSize=label_size,
698            color=label_color,
699            text=annotation_text
700        ).encode(
701            x=create_encoding('x', x_type),
702            y=create_encoding('y', y_type)
703        )
704        layers.append(label_layer)
705
706        # Combine all layers into a single annotation
707        annotation = alt.layer(*layers)
708
709        # Adds annotation to history layers
710        self.story_layers.append({
711            'type': 'annotation',
712            'chart': annotation
713        })
714
715        # Note on flexibility and robustness:
716        # This approach makes the add_annotation method more flexible,
717        # as it can automatically adapt to different types of graphs without
718        # requiring manual input on the axis data type. In addition, using
719        # the Altair shorthand, the code automatically adapts even if the user
720        # has specified the encoding in different ways (e.g., using alt.X(‘column:Q’)
721        # or alt.X(‘column’, type=‘quantitative’)).
722
723
724        return self  # Return self to allow method chaining

Create an arrow annotation on the graph.

This method is essential for highlighting specific points in the graph and adding contextual explanations. It is particularly useful for data narration, allowing to guide the observer's attention to relevant aspects of the visualisation.

Parameters:

  • x_point, y_point: Coordinates of the point to be annotated
  • annotation_text: Text of the annotation (default: ‘Point of interest’)
  • arrow_direction: Direction of the arrow (default: ‘right’)
  • arrow_color, arrow_size: Arrow colour and size
  • label_colour, label_size: Colour and size of the annotation text
  • show_point: If True, shows a point at the annotation location
  • point_color, point_size: Colour and size of the point
  • arrow_dx, arrow_dy: Distances in pixels to be added to the arrow position (default:0, -45)
  • label_dx, label_dy: Distances in pixels to be added to the label position (default:37, -37)

returns:

  • self, to allow method chaining
def add_line( self, value, orientation='horizontal', color='red', stroke_width=2, stroke_dash=[]):
727    def add_line(self, value, orientation='horizontal', color='red', stroke_width=2, stroke_dash=[]):
728        """
729        Adds a reference line to the story.
730
731        Parameters:
732        - value: The position of the line (x or y value)
733        - orientation: 'horizontal' or 'vertical' (default: 'horizontal')
734        - color: Color of the line (default: 'red')
735        - stroke_width: Width of the line in pixels (default: 2)
736        - stroke_dash: Pattern for dashed line, e.g. [5,5] (default: [] for solid line)
737
738        Returns:
739        - self, to allow method chaining
740        """
741        
742        if orientation not in ['horizontal', 'vertical']:
743            raise ValueError("orientation must be either 'horizontal' or 'vertical'")
744
745        # Crea un DataFrame con un singolo valore
746        if orientation == 'horizontal':
747            data = pd.DataFrame({'y': [value]})
748            encoding = {'y': 'y:Q'}
749        else:  # vertical
750            data = pd.DataFrame({'x': [value]})
751            encoding = {'x': 'x:Q'}
752        
753        # definizione dei parametri della line
754        mark_params = {
755            'color': color,
756            'strokeWidth': stroke_width,
757        }
758        
759        # Viene aggiunto stokeDash solo se è specificato un pattern
760        if stroke_dash:
761            mark_params['strokeDash'] = stroke_dash
762
763        # Crea la linea
764        line = alt.Chart(data).mark_rule(
765            color=color,
766            strokeWidth=stroke_width,
767            strokeDash=stroke_dash
768        ).encode(
769            **encoding
770        )
771
772        # Aggiunge la linea ai layer della storia
773        self.story_layers.append({
774            'type': 'line',
775            'chart': line
776        })
777
778        return self

Adds a reference line to the story.

Parameters:

  • value: The position of the line (x or y value)
  • orientation: 'horizontal' or 'vertical' (default: 'horizontal')
  • color: Color of the line (default: 'red')
  • stroke_width: Width of the line in pixels (default: 2)
  • stroke_dash: Pattern for dashed line, e.g. [5,5] (default: [] for solid line)

Returns:

  • self, to allow method chaining
def em_to_px(self, em):
782    def em_to_px(self, em):
783        """
784        Converts a dimension from em to pixels.
785
786        This function is essential for maintaining visual consistency
787        between different textual elements and devices.
788
789        Parameters:
790        - em: Size in em
791
792        Returns:
793        - Equivalent size in pixels (integer)
794        """
795        return int(em * self.base_font_size)

Converts a dimension from em to pixels.

This function is essential for maintaining visual consistency between different textual elements and devices.

Parameters:

  • em: Size in em

Returns:

  • Equivalent size in pixels (integer)
def create_title_layer(self, layer):
826    def create_title_layer(self, layer):
827        """
828        Creates the title layer (and subtitle if present).
829
830        This method is responsible for visually creating the main title
831        and the optional subtitle of the graphic.
832
833        Parameters:
834        - Layer: Dictionary containing the title information
835
836        Returns:
837        - Altair Chart object representing the title layer
838        """
839        title_chart = alt.Chart(self.chart.data).mark_text(
840            text=layer['title'],
841            fontSize=layer['title_font_size'],
842            fontWeight='bold',
843            align='center',
844            font=self.font,
845            color=layer['title_color']
846        ).encode(
847            x=alt.value(self.chart.width / 2 + layer.get('dx', 0)),  # Centre horizontally
848            y=alt.value(-50 + layer.get('dy', 0))  # Position 20 pixels from above
849        )
850        
851        if layer['subtitle']:
852            subtitle_chart = alt.Chart(self.chart.data).mark_text(
853                text=layer['subtitle'],
854                fontSize=layer['subtitle_font_size'],
855                align='center',
856                font=self.font,
857                color=layer['subtitle_color']
858            ).encode(
859                x=alt.value(self.chart.width / 2 + layer.get('s_dx', 0)),  # Centre horizontally
860                y=alt.value(-20 + layer.get('s_dy', 0))  # Position 50 pixels from the top (below the title)
861            )
862            return title_chart + subtitle_chart
863        return title_chart

Creates the title layer (and subtitle if present).

This method is responsible for visually creating the main title and the optional subtitle of the graphic.

Parameters:

  • Layer: Dictionary containing the title information

Returns:

  • Altair Chart object representing the title layer
def create_text_layer(self, layer):
865    def create_text_layer(self, layer):
866        """
867        Creates a generic text layer (context, next-steps, source).
868
869        This method is used to create text layers for various narrative purposes,
870        such as adding context, next-steps or data source information.
871
872        Parameters:
873        - Layer: Dictionary containing the text information to be added
874
875        Returns:
876        - Altair Chart object representing the text layer
877        """
878        x, y = self._get_position(layer['position'])
879        
880        # Apply offsets if they exist (for context layers)
881        if layer['type'] in ['context', 'source']:
882            x += layer.get('dx', 0)
883            y += layer.get('dy', 0)
884        
885        return alt.Chart(self.chart.data).mark_text(
886            text=layer['text'],
887            fontSize=layer.get('font_size', self.em_to_px(self.font_sizes[layer['type']])),
888            align='center',
889            baseline='middle',
890            font=self.font,
891            angle=270 if layer.get('vertical', False) else 0,  #  Rotate text if ‘vertical’ is True
892            color=layer['color']
893        ).encode(
894            x=alt.value(x),
895            y=alt.value(y)
896        )

Creates a generic text layer (context, next-steps, source).

This method is used to create text layers for various narrative purposes, such as adding context, next-steps or data source information.

Parameters:

  • Layer: Dictionary containing the text information to be added

Returns:

  • Altair Chart object representing the text layer
def configure_view(self, *args, **kwargs):
898    def configure_view(self, *args, **kwargs):
899        """
900        Configure aspects of the graph view using Altair's configure_view method.
901
902        This method allows you to configure various aspects of the chart view, such as
903        the background colour, border style, internal spacing, etc.
904
905        Parameters:
906        *args, **kwargs: Arguments to pass to the Altair configure_view method.
907
908        stores the view configuration for application during rendering.
909        """
910        self.config['view'] = kwargs
911        return self

Configure aspects of the graph view using Altair's configure_view method.

This method allows you to configure various aspects of the chart view, such as the background colour, border style, internal spacing, etc.

Parameters: args, *kwargs: Arguments to pass to the Altair configure_view method.

stores the view configuration for application during rendering.

def render(self):
914    def render(self):
915        """
916        It renders all layers of the story in a single graphic.       
917        """
918        # Let's start with the basic graph
919        main_chart = self.chart
920        
921        # Create separate lists to place special graphics
922        top_charts = []
923        bottom_charts = []
924        left_charts = []
925        right_charts = []
926        overlay_charts = []
927        
928        # Organise the layers according to their position
929        for layer in self.story_layers:
930            if layer['type'] == 'special_cta':
931                # We take the position from the layer
932                if layer.get('position') == 'top':
933                    top_charts.append(layer['chart'])
934                elif layer.get('position') == 'bottom':
935                    bottom_charts.append(layer['chart'])
936                elif layer.get('position') == 'left':
937                    left_charts.append(layer['chart'])
938                elif layer.get('position') == 'right':
939                    right_charts.append(layer['chart'])
940            elif layer['type'] == 'title':
941                overlay_charts.append(self.create_title_layer(layer))
942            elif layer['type'] in ['context', 'cta', 'source']:
943                overlay_charts.append(self.create_text_layer(layer))
944            elif layer['type'] in ['shape', 'shape_label', 'annotation']:
945                overlay_charts.append(layer['chart'])
946            elif layer['type'] == 'line':
947                overlay_charts.append(layer['chart'])
948
949        # Overlaying the layers on the main graph
950        for overlay in overlay_charts:
951            main_chart += overlay
952
953        # Build the final layout
954        if left_charts:
955            main_chart = alt.hconcat(alt.vconcat(*left_charts), main_chart)
956        if right_charts:
957            main_chart = alt.hconcat(main_chart, alt.vconcat(*right_charts))
958        if top_charts:
959            main_chart = alt.vconcat(alt.hconcat(*top_charts), main_chart)
960        if bottom_charts:
961            main_chart = alt.vconcat(main_chart, alt.hconcat(*bottom_charts))
962
963        # Apply configurations
964        if 'view' in self.config:
965            main_chart = main_chart.configure_view(**self.config['view'])
966
967        return main_chart.resolve_axis(x='independent', y='independent')

It renders all layers of the story in a single graphic.

def story(data=None, **kwargs):
973def story(data=None, **kwargs):
974    """
975    Utility function for creating a Story instance.
976
977    This function simplifies the creation of a Story object, allowing
978    to initialise it in a more concise and intuitive way.
979
980    Parameters:
981    - data: DataFrame or URL for chart data (default: None)
982    - **kwargs: Additional parameters to be passed to the Story constructor
983
984    Returns:
985    - An instance of the Story class
986    """
987    return Story(data, **kwargs)

Utility function for creating a Story instance.

This function simplifies the creation of a Story object, allowing to initialise it in a more concise and intuitive way.

Parameters:

  • data: DataFrame or URL for chart data (default: None)
  • **kwargs: Additional parameters to be passed to the Story constructor

Returns:

  • An instance of the Story class