Composition of layer

Layer composition can be used to superpose multiple layers to achieve compound plots, annotations and many more. This example shows a composed-layered chart built in HTML / JavaScript with source code that you can edit to see changes in visualization

main
run-button
run-button
reset-button
// Data is fetched from https://www.charts.com/static/weather/data.csv and assigned to the variable data.
// Schema is fetched from https://www.charts.com/static/weather/schema.json and assigned to the variable schema.
// An element with id `chart-container` is available in the DOM to house the visualization
// Retrieves the DataModel from muze namespace. Muze recognizes DataModel as a first class source of data.
let env = muze();
const DataModel = muze.DataModel;
const share = muze.Operators.share;
const DateTimeFormatter = muze.utils.DateTimeFormatter;

const rootData = new DataModel(data, schema);

env = env.data(rootData).minUnitHeight(40).minUnitWidth(40);
const rows = [[], [share('minDays', 'days at or above 32 deg', 'maxDays')]];
let canvas = env.canvas();

canvas = canvas
	.rows(rows)
    .columns(['time'])
    .width(850)
    .height(500)
    .color({
        value: '#f07520'
    })
    .config({
        gridLines: {
            x: {
                show: true
            }
        },
        axes: {
            y: {
                domain: [180, 320],
                name: 'Average number of days at or above 32˚C',
                numberOfTicks: 8
            }
        },
        interaction: {
            tooltip: {
                formatter: (dm) => {
                    const dataArr = dm.getData().data;
                    const fieldsConfig = dm.getFieldsConfig();
                    const maxDayIndex = fieldsConfig.maxDays.index;
                    const minDayIndex = fieldsConfig.minDays.index;
                    return [
                        ['Year: ', {
                            value: DateTimeFormatter.formatAs(dataArr[0][fieldsConfig.time.index], '%Y'),
                            className: 'muze-tooltip-value'
                        }],
                        ['Days at or above 32 deg: ', {
                            value: Math.floor(dataArr[0][fieldsConfig['days at or above 32 deg'].index]),
                            className: 'muze-tooltip-value'
                        }],
                        ['Range: ', {
                            value: `${Math.floor(dataArr[0][minDayIndex])} - ${Math.floor(dataArr[0][maxDayIndex])}`,
                            className: 'muze-tooltip-value'
                        }]
                    ];
                }
            }
        }
    })
    .layers([{
        mark: 'line',
        className: 'line-plot-item',
        encoding: {
            y: 'days at or above 32 deg'
        },
        interpolate: 'catmullRom'
    }, {
        mark: 'area',
        className: 'area-layer',
        encoding: {
            y: 'minDays',
            y0: 'maxDays',
            color: {
                value: () => '#fdb92b'
            }
        },
        transition: {
            duration: 0
        },
        interpolate: 'catmullRom'
    }, {
        mark: 'text',
        className: 'text-layer',
        encoding: {
            y: 'days at or above 32 deg',
            text: {
                field: 'time',
                formatter: val => DateTimeFormatter.formatAs(val, '%Y')
            },
            color: {
                value: () => '#000'
            }
        },
        source: dt => dt.select(fields =>
            [1992, 2018, 2072].indexOf(new Date(fields.time.value).getFullYear()) !== -1),
        encodingTransform: (points) => {
            for (let i = 0, len = points.length; i < len; i++) {
                points[i].update.y -= 10;
                if (i === 0) {
                    points[i].text += '  Born';
                    points[i].update.x -= 30;
                }
                if (i === len - 1) {
                    points[i].text += ' Age 80';
                    points[i].update.x -= 30;
                }
            }
            return points;
        }
    }, {
        mark: 'point',
        className: 'anchor-indicator',
        encoding: {
            y: 'days at or above 32 deg',
            size: {
                value: 100
            },
            color: {
                value: () => '#ff0000'
            }
        },
        source: dt => dt.select(fields =>
            [1992, 2018, 2072].indexOf(new Date(fields.time.value).getFullYear()) !== -1)
    }, {
        mark: 'tick',
        className: 'tick-layer',
        encoding: {
            y: 'maxDays',
            y0: 'minDays'
        },
        source: dt => dt.select(fields =>
            [1992, 2018, 2072].indexOf(new Date(fields.time.value).getFullYear()) !== -1)
    }])
    .subtitle('Days at or above 32°C per year from the time you were born')
    .title('Composition of layers')
    .mount('#chart-container');

What is Composition of layers?

Custom visualizations can be created by composing atomic constructs which Muze provides. The above visualization is a combination of line, range area, point and text marks (plot type) which is achieved by composing multiple layers. With Muze you are not limited by the chart type Muze provides, you can use those to create your own chart. A detailed explanation of composition of layers can be found here.

Data

The above visualization is created from this data and this schema.

Steps to create a concurrent tooltip

Following are the steps we need to do to create the above visualization

  • Create an instance of DataModel from data and schema
  • Provide the rows and columns and details to the canvas for layouting
  • Formatting axes and gridlines
  • Add tooltip configuration
  • Draw layer on the canvas
  • Add title and subtitle to the canvases
  • Mount the charts to the DOM.

Formatting axes and gridlines

Axes

In the above chart, the Y-axis is shown from the starting value 180 upto 320, to make the chart readable to the user, and avoid painting only on a small portion of the canvas. This is done by mention the domain. The name of the axis is also attached. The number of ticks determine the number of values on the Y-axis to be considered.

canvas.config({
    axes: {
        y: {
            domain: [180, 320],
            name: 'Average number of days at or above 32˚C',
            numberOfTicks: 8
        }
    }
)
logo

Note

numberOfTicks can be considered as a hint value. The actual number of ticks can be greater than the given value. This is because the tick interval of the axis needs to be equal and human readable, so it gives the nearest readable visualisation.

Gridlines

The X-Axis grid lines are hidden by default, we can pass a config to make it visible

canvas.config({
    gridLines: {
        x: {
            show: true
        }
    }
)

Adding tooltip

The chart above has only one interaction, which is a tooltip. By default Muze shows a tooltip with a predefined decoration. Here we change the text and decoration of the content of tooltip using formatter.

canvas.config({
    interaction: {
        tooltip: {
            formatter: (dm) => {
                const dataArr = dm.getData().data;
                const fieldsConfig = dm.getFieldsConfig();
                const maxDayIndex = fieldsConfig.maxDays.index;
                const minDayIndex = fieldsConfig.minDays.index;
                return [
                    ['Year: ', {
                        value: DateTimeFormatter.formatAs(dataArr[0][fieldsConfig.time.index], '%Y'),
                        className: 'muze-tooltip-value'
                    }],
                    ['Days at or above 32 deg: ', {
                        value: Math.floor(dataArr[0][fieldsConfig['days at or above 32 deg'].index]),
                        className: 'muze-tooltip-value'
                    }],
                    ['Range: ', {
                        value: `${Math.floor(dataArr[0][minDayIndex])} - ${Math.floor(dataArr[0][maxDayIndex])}`,
                        className: 'muze-tooltip-value'
                    }]
                ];
            }
        }
    }
})

Click here to read more on tooltips.

Drawing layers

The chart above has 5 layers:

Line


line

A line is plotted as days at or above 32 deg v/s time.

{
    mark: 'line',
    className: 'line-plot-item',
    encoding: {
        y: 'days at or above 32 deg'
    },
    interpolate: 'catmullRom'
}
logo

Note

D3's catmullRom is used which generate a Catmull–Rom spline.

Area

area It is plotted as minDays and maxDays v/s time. The layer is colored and transitioned.

{
    mark: 'area',
    className: 'area-layer',
    encoding: {
        y: 'minDays',
        y0: 'maxDays',
        color: {
            value: () => '#fdb92b'
        }
    },
    transition: {
        duration: 0
    },
    interpolate: 'catmullRom'
}

Text

text It is plotted as days at or above 32 deg v/s time. We add color and text encodings. The three values to be displayed are feeded to the encodingTransform. Assuming that the person was born in the year 1992, apt labels for born and age 80 are attached to the canvas.

{
    mark: 'text',
    className: 'text-layer',
    encoding: {
        y: 'days at or above 32 deg',
        text: {
            field: 'time',
            formatter: val => DateTimeFormatter.formatAs(val, '%Y')
        },
        color: {
            value: () => '#000'
        }
    },
    source: dt => dt.select(fields =>
        [1992, 2018, 2072].indexOf(new Date(fields.time.value).getFullYear()) !== -1)
}
logo

Note

You can use Muze's encodingTransform to transform the points after they have been rendered.

 encodingTransform: (points) => {
    for (let i = 0, len = points.length; i < len; i++) {
        points[i].update.y -= 10;
        if (i === 0) {
            points[i].text += '  Born';
            points[i].update.x -= 30;
        }
        if (i === len - 1) {
            points[i].text += ' Age 80';
            points[i].update.x -= 30;
        }
    }
    return points;
}

Point

point It is plotted as days at or above 32 deg v/s time. We add color and text encodings. The three values to be displayed are feeded to the source, and the points are displayed on those data values.

{
    mark: 'point',
    className: 'anchor-indicator',
    encoding: {
        y: 'days at or above 32 deg',
        size: {
            value: 100
        },
        color: {
            value: () => '#ff0000'
        }
    },
    source: dt => dt.select(fields =>
        [1992, 2018, 2072].indexOf(new Date(fields.time.value).getFullYear()) !== -1)
}

Tick

tick It is plotted as minDays and maxDays v/s time. The three values to be displayed are feeded to the source, and if there is an area plot there it draws the tick.

{
    mark: 'tick',
    className: 'tick-layer',
    encoding: {
        y: 'maxDays',
        y0: 'minDays'
    },
    source: dt => dt.select(fields =>
        [1992, 2018, 2072].indexOf(new Date(fields.time.value).getFullYear()) !== -1)
}

Add title and subtitle to canvas

Add a title and subtitle to the canvas

canvas.title('Days at or above 32°C per year')
      .subtitle('A Range Area Plot')

Read more about title and subtitle here.

Mount the chart to the DOM

Finally attach the canvas instance to DOM which houses the visualization

canvas.mount('#chart-container');

An element with id chart-container is available in the DOM to house the visualization.