Search code examples
pharoroassal

How to add horizontal line labels at end of line when using multiple plots in Roassal3


I'm trying to use Roassal3, in Pharo 10, to visualise multiple series of data. I've managed to draw the chart fine but want to label the lines. I tried using RSLegend to display the text with corresponding colour boxes (corresponding to the line colours) but they come out far too large and end up shrinking the graph to fit the canvas. If I could manipulate the legends to display differently, that would help, but I recall seeing an example, using an earlier version of Roassal, which displays a label for each line, at the end of the line. This would be ideal, so I tried adding an RSLabel as the markerEnd for the plot. This worked except that the labels all came out at various angles (presumably the angle of the plot lines, at the end). How can I get the end marker labels to display horizontally? Documentation on Roassal3 is still a work in progress, so I can't find any examples.

I've updated the sample code to show what I've tried with end markers, legends and decorators (none are ideal but I may be able to work with decorators):

| chart plot dates values firstDate labels legend offset plotLabel renderedLabel canvasCopy |
chart := RSChart new.
canvasCopy := chart canvas copy.
dates := (20 to: 1 by: -1) collect: [ :i | Date today subtractDays: i ]. 
firstDate := dates first.
offset := 20 @ -50.
values := #(
    #(4.29 4.01 3.82 3.91 4.01 3.73 4.47 4.28 4.18 4.00 3.72 4.27 3.99 4.91 5.09 4.91 5.09 4.91 4.44 4.91)
    #(2.0 1.98 1.98 1.98 1.99 1.96 2.07 1.96 1.90 1.95 1.98 2.04 2.12 2.12 2.21 2.27 2.27 2.10 2.19 1.95)
    ).
labels := #('series 1' 'series 2').
values with: labels do: [ :series :label | 
   plot := RSLinePlot new markerEnd: (RSLabel new text: label).
   plot 
       x: (dates collect: [ :date | date julianDayNumber - firstDate julianDayNumber ])
       y: series.
   chart addPlot: plot.
    plotLabel := RSYLabelDecoration new right; 
        title: '~' , label;
        fontSize: 12;
        rotationAngle: 90;
        color: (chart colorFor: plot);
        offset: offset;
        yourself.
    chart addDecoration: plotLabel.
    renderedLabel := (plotLabel copy renderIn: canvasCopy) label.
    offset := (0 - renderedLabel textWidth) @ (offset y + renderedLabel textHeight + 4). 
 ].
canvasCopy shapes copy do: [ :shape | canvasCopy removeShape: shape ].
chart addDecoration: (RSHorizontalTick new labelConversion: [ :value | 
    Date julianDayNumber: firstDate julianDayNumber + value ]; useDiagonalLabel; yourself).
chart addDecoration: RSVerticalTick new.
chart ylabel: 'The values'.
chart build.
legend := RSLegend new.
legend container: chart canvas.
labels with: chart plots do: [ :c : p |
    legend text: c withBoxColor: (chart colorFor: p) ].
legend layout horizontalCompactTree .
legend build.
^chart canvas open

Solution

  • Following a tip from Alexandre Bergel, I now have a reasonable way to draw labels at the end of plot lines. For interest, I've included, in the playground code below, three ways to identify the plot lines, using the end of plot labels, using right side decorations and using a more standard legend. It's messy, in places but can be cleaned up a lot by moving code to more appropriate objects.

    | chart plot dates values firstDate labels legend offset plotLabel renderedLabel canvasCopy maxOffset |
    chart := RSChart new.
    canvasCopy := RSCanvas new.
    dates := (10 to: 1 by: -1) collect: [ :i | Date today subtractDays: i ]. 
    firstDate := dates first.
    offset := 20 @ -50.
    maxOffset := 0.
    values := #(
        #(3.72 4.27 3.99 4.91 5.09 4.91 5.09 4.91 4.44 4.91)
        #(4.29 4.01 3.82 3.91 4.01 3.73 4.47 4.28 4.18 4.00)
        #(1.98 2.04 2.12 2.12 2.21 2.27 2.27 2.10 2.19 1.95)
        #(2.5 2.3 2.7 2.73 2.15 2.6 2.63 2.57 2.4 2.8)
        #(2.0 1.98 1.98 1.98 1.99 1.96 2.07 1.96 1.90 1.95)
        ).
    labels := #('first series' 'second series' 'series number 4' 'the third series' 'a fifth series').
    values with: labels do: [ :series :label | 
       plot := RSLinePlot new.
       plot 
           x: (dates collect: [ :date | date julianDayNumber - firstDate julianDayNumber ])
           y: series.
       chart addPlot: plot.
        "If adding a legend to the right, RSYLabel decoration is needed but with modified offsets to get them to lay out vertically."
        plotLabel := RSYLabelDecoration new right; 
            title: '~' , label;
            fontSize: 12;
            rotationAngle: 90;
            color: (chart colorFor: plot);
            offset: offset;
            yourself.
        chart addDecoration: plotLabel.
        renderedLabel := (plotLabel renderIn: canvasCopy) label.
        maxOffset := maxOffset max: renderedLabel textWidth.
        offset := (0 - maxOffset) @ (offset y + renderedLabel textHeight + 4).
     ].
    canvasCopy := nil.
    chart addDecoration: (RSHorizontalTick new labelConversion: [ :value | 
        Date julianDayNumber: firstDate julianDayNumber + value ]; useDiagonalLabel; yourself).
    chart addDecoration: RSVerticalTick new.
    "When adding labels at the end of the plot lines, the right hand line of the chart box will overlay the labels so alter the
    spine to a polygon which only draws the x and y axes. "
    spine := chart decorations
            detect: [ :d | d class == RSChartSpineDecoration ].
    spine shape: (RSPolygon new points: {(0 @ 0). (0 @ 0). (0 @ (chart extent y)). (chart extent x @ (chart extent y)). (chart extent x @ (chart extent y)). (0 @ (chart extent y)) }; noPaint; withBorder; yourself).
    chart ylabel: 'The values'.
    chart build.
    "To use standard legend under the chart"
    legend := RSLegend new defaultLabel: (RSLabel new fontSize: 8; yourself).
    legend container: chart canvas.
    labels with: chart plots do: [ :c : p |
        legend text: c withShape: (RSPolygon new points: { 0 @ -2. 10 @ -2. 10 @ 2. 0 @ 2 }; color: (chart colorFor: p))
        ].
    legend layout grid.
    legend build.
    
    rsLabels := Dictionary new: chart plots size.
    chart plots with: labels do: [ :plot :label | rsLabels at: plot put: ((RSLabel text: label) fontSize: 5) ].
    lastys := chart plots collect: [ :pl | 
        Association key: pl value: (pl yScale scale: pl yValues last) ].
    lastys sort: [ :el1 :el2 | 
        el1 value = el2 value 
            "If the last y coordinate is the same for both plots, order by the second last y point (reverse ordering)"
            ifTrue: [ (el1 key yValues at: el1 key yValues size - 1) > (el2 key yValues at: el2 key yValues size - 1) ] 
            ifFalse: [ el1 value < el2 value ] ].
    lastys withIndexDo: [ :lasty :index |
            numPlots := lastys size.
            plot := lasty key.
            label := rsLabels at: plot.
            label color: plot color.
            chart canvas add: label.
            yPoint := lasty value.
            textHeight := label textHeight.
            index < numPlots ifTrue: [ 
                "Reset the y point to be about textHeight away from the next one, if they are close"
                (diff := (yPoint - (lastys at: index + 1) value) abs) < textHeight 
                    ifTrue: [ yPoint := yPoint - (textHeight / 2) + (diff / 2) ] ].
            index > 1 ifTrue: [ 
                "Reset the y point to be about textHeight away from the last one, if they are close"
                (diff := ((lastys at: index - 1) value - yPoint ) abs) < textHeight 
                    ifTrue: [ yPoint := yPoint + (textHeight / 2) - (diff / 2) ] ].
            label translateTo: ((plot xScale scale: plot xValues last) + (label textWidth / 2) + 1) @ yPoint.
        ].
    ^chart canvas