block by curran 9e04ccfebeb84bcdc76c

Scatter Plot

Full Screen

This program makes a scatter plot from Iris data set, showcasing how a single reusable visualization module can gracefully support optional visualization configuration properties like size and color.

Based on Data Canvas Part 3 - Bar Chart.

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">

    <!-- Use RequireJS for module loading. -->
    <script src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.14/require.js"></script>

    <!-- Configure RequireJS paths for third party libraries. -->
    <script>
      requirejs.config({
        paths: {
          model: "//curran.github.io/cdn/model-v0.2.1/dist/model-min",
          d3: "//d3js.org/d3.v3.min",
          jquery: "//code.jquery.com/jquery-2.1.1.min"
        }
      });
    </script>

    <!-- Include CSS that styles the visualization. -->
    <link rel="stylesheet" href="styles.css">

    <title>Scatter Plot</title>
  </head>
  <body>

    <!-- The visualization will be injected into this div. -->
    <div id="container"></div>
    
    <!-- Run the main program. -->
    <script src="main.js"></script>

  </body>
</html>

generateColors.js

// Generates n distinct colors.
// l is the fixed lightness, the L in LAB color space.
// r is the radius of the circle in AB space along which points are taken.
// Documented at http://bl.ocks.org/curran/dd73d3d8925cdf50df86
define([], function (){
  var lDefault = 50,
      rDefault = 100;
  return function generateColors(n, l, r){
    var colors = [], a, b, θ;
    l = l || lDefault;
    r = r || rDefault;
    for(var i = 0; i < n; i++){
      θ = (i / n) * Math.PI * 2;
      a = Math.sin(θ) * r;
      b = Math.cos(θ) * r;
      colors.push(d3.lab(l, a, b).toString());
    }
    return colors;
  }
});

iris-metadata.json

{
  "sepal_length":{
    "type": "Q",
    "label": "sepal length (cm)"
  },
  "sepal_width": {
    "type": "Q",
    "label": "sepal width (cm)"
  },
  "petal_length": {
    "type": "Q",
    "label": "petal length (cm)"
  },
  "petal_width": {
    "type": "Q",
    "label": "petal width (cm)"
  },
  "class": {
    "type": "N",
    "label": "species"
  }
}

iris.csv

sepal_length,sepal_width,petal_length,petal_width,class
5.1,3.5,1.4,0.2,Iris-setosa
4.9,3.0,1.4,0.2,Iris-setosa
4.7,3.2,1.3,0.2,Iris-setosa
4.6,3.1,1.5,0.2,Iris-setosa
5.0,3.6,1.4,0.2,Iris-setosa
5.4,3.9,1.7,0.4,Iris-setosa
4.6,3.4,1.4,0.3,Iris-setosa
5.0,3.4,1.5,0.2,Iris-setosa
4.4,2.9,1.4,0.2,Iris-setosa
4.9,3.1,1.5,0.1,Iris-setosa
5.4,3.7,1.5,0.2,Iris-setosa
4.8,3.4,1.6,0.2,Iris-setosa
4.8,3.0,1.4,0.1,Iris-setosa
4.3,3.0,1.1,0.1,Iris-setosa
5.8,4.0,1.2,0.2,Iris-setosa
5.7,4.4,1.5,0.4,Iris-setosa
5.4,3.9,1.3,0.4,Iris-setosa
5.1,3.5,1.4,0.3,Iris-setosa
5.7,3.8,1.7,0.3,Iris-setosa
5.1,3.8,1.5,0.3,Iris-setosa
5.4,3.4,1.7,0.2,Iris-setosa
5.1,3.7,1.5,0.4,Iris-setosa
4.6,3.6,1.0,0.2,Iris-setosa
5.1,3.3,1.7,0.5,Iris-setosa
4.8,3.4,1.9,0.2,Iris-setosa
5.0,3.0,1.6,0.2,Iris-setosa
5.0,3.4,1.6,0.4,Iris-setosa
5.2,3.5,1.5,0.2,Iris-setosa
5.2,3.4,1.4,0.2,Iris-setosa
4.7,3.2,1.6,0.2,Iris-setosa
4.8,3.1,1.6,0.2,Iris-setosa
5.4,3.4,1.5,0.4,Iris-setosa
5.2,4.1,1.5,0.1,Iris-setosa
5.5,4.2,1.4,0.2,Iris-setosa
4.9,3.1,1.5,0.1,Iris-setosa
5.0,3.2,1.2,0.2,Iris-setosa
5.5,3.5,1.3,0.2,Iris-setosa
4.9,3.1,1.5,0.1,Iris-setosa
4.4,3.0,1.3,0.2,Iris-setosa
5.1,3.4,1.5,0.2,Iris-setosa
5.0,3.5,1.3,0.3,Iris-setosa
4.5,2.3,1.3,0.3,Iris-setosa
4.4,3.2,1.3,0.2,Iris-setosa
5.0,3.5,1.6,0.6,Iris-setosa
5.1,3.8,1.9,0.4,Iris-setosa
4.8,3.0,1.4,0.3,Iris-setosa
5.1,3.8,1.6,0.2,Iris-setosa
4.6,3.2,1.4,0.2,Iris-setosa
5.3,3.7,1.5,0.2,Iris-setosa
5.0,3.3,1.4,0.2,Iris-setosa
7.0,3.2,4.7,1.4,Iris-versicolor
6.4,3.2,4.5,1.5,Iris-versicolor
6.9,3.1,4.9,1.5,Iris-versicolor
5.5,2.3,4.0,1.3,Iris-versicolor
6.5,2.8,4.6,1.5,Iris-versicolor
5.7,2.8,4.5,1.3,Iris-versicolor
6.3,3.3,4.7,1.6,Iris-versicolor
4.9,2.4,3.3,1.0,Iris-versicolor
6.6,2.9,4.6,1.3,Iris-versicolor
5.2,2.7,3.9,1.4,Iris-versicolor
5.0,2.0,3.5,1.0,Iris-versicolor
5.9,3.0,4.2,1.5,Iris-versicolor
6.0,2.2,4.0,1.0,Iris-versicolor
6.1,2.9,4.7,1.4,Iris-versicolor
5.6,2.9,3.6,1.3,Iris-versicolor
6.7,3.1,4.4,1.4,Iris-versicolor
5.6,3.0,4.5,1.5,Iris-versicolor
5.8,2.7,4.1,1.0,Iris-versicolor
6.2,2.2,4.5,1.5,Iris-versicolor
5.6,2.5,3.9,1.1,Iris-versicolor
5.9,3.2,4.8,1.8,Iris-versicolor
6.1,2.8,4.0,1.3,Iris-versicolor
6.3,2.5,4.9,1.5,Iris-versicolor
6.1,2.8,4.7,1.2,Iris-versicolor
6.4,2.9,4.3,1.3,Iris-versicolor
6.6,3.0,4.4,1.4,Iris-versicolor
6.8,2.8,4.8,1.4,Iris-versicolor
6.7,3.0,5.0,1.7,Iris-versicolor
6.0,2.9,4.5,1.5,Iris-versicolor
5.7,2.6,3.5,1.0,Iris-versicolor
5.5,2.4,3.8,1.1,Iris-versicolor
5.5,2.4,3.7,1.0,Iris-versicolor
5.8,2.7,3.9,1.2,Iris-versicolor
6.0,2.7,5.1,1.6,Iris-versicolor
5.4,3.0,4.5,1.5,Iris-versicolor
6.0,3.4,4.5,1.6,Iris-versicolor
6.7,3.1,4.7,1.5,Iris-versicolor
6.3,2.3,4.4,1.3,Iris-versicolor
5.6,3.0,4.1,1.3,Iris-versicolor
5.5,2.5,4.0,1.3,Iris-versicolor
5.5,2.6,4.4,1.2,Iris-versicolor
6.1,3.0,4.6,1.4,Iris-versicolor
5.8,2.6,4.0,1.2,Iris-versicolor
5.0,2.3,3.3,1.0,Iris-versicolor
5.6,2.7,4.2,1.3,Iris-versicolor
5.7,3.0,4.2,1.2,Iris-versicolor
5.7,2.9,4.2,1.3,Iris-versicolor
6.2,2.9,4.3,1.3,Iris-versicolor
5.1,2.5,3.0,1.1,Iris-versicolor
5.7,2.8,4.1,1.3,Iris-versicolor
6.3,3.3,6.0,2.5,Iris-virginica
5.8,2.7,5.1,1.9,Iris-virginica
7.1,3.0,5.9,2.1,Iris-virginica
6.3,2.9,5.6,1.8,Iris-virginica
6.5,3.0,5.8,2.2,Iris-virginica
7.6,3.0,6.6,2.1,Iris-virginica
4.9,2.5,4.5,1.7,Iris-virginica
7.3,2.9,6.3,1.8,Iris-virginica
6.7,2.5,5.8,1.8,Iris-virginica
7.2,3.6,6.1,2.5,Iris-virginica
6.5,3.2,5.1,2.0,Iris-virginica
6.4,2.7,5.3,1.9,Iris-virginica
6.8,3.0,5.5,2.1,Iris-virginica
5.7,2.5,5.0,2.0,Iris-virginica
5.8,2.8,5.1,2.4,Iris-virginica
6.4,3.2,5.3,2.3,Iris-virginica
6.5,3.0,5.5,1.8,Iris-virginica
7.7,3.8,6.7,2.2,Iris-virginica
7.7,2.6,6.9,2.3,Iris-virginica
6.0,2.2,5.0,1.5,Iris-virginica
6.9,3.2,5.7,2.3,Iris-virginica
5.6,2.8,4.9,2.0,Iris-virginica
7.7,2.8,6.7,2.0,Iris-virginica
6.3,2.7,4.9,1.8,Iris-virginica
6.7,3.3,5.7,2.1,Iris-virginica
7.2,3.2,6.0,1.8,Iris-virginica
6.2,2.8,4.8,1.8,Iris-virginica
6.1,3.0,4.9,1.8,Iris-virginica
6.4,2.8,5.6,2.1,Iris-virginica
7.2,3.0,5.8,1.6,Iris-virginica
7.4,2.8,6.1,1.9,Iris-virginica
7.9,3.8,6.4,2.0,Iris-virginica
6.4,2.8,5.6,2.2,Iris-virginica
6.3,2.8,5.1,1.5,Iris-virginica
6.1,2.6,5.6,1.4,Iris-virginica
7.7,3.0,6.1,2.3,Iris-virginica
6.3,3.4,5.6,2.4,Iris-virginica
6.4,3.1,5.5,1.8,Iris-virginica
6.0,3.0,4.8,1.8,Iris-virginica
6.9,3.1,5.4,2.1,Iris-virginica
6.7,3.1,5.6,2.4,Iris-virginica
6.9,3.1,5.1,2.3,Iris-virginica
5.8,2.7,5.1,1.9,Iris-virginica
6.8,3.2,5.9,2.3,Iris-virginica
6.7,3.3,5.7,2.5,Iris-virginica
6.7,3.0,5.2,2.3,Iris-virginica
6.3,2.5,5.0,1.9,Iris-virginica
6.5,3.0,5.2,2.0,Iris-virginica
6.2,3.4,5.4,2.3,Iris-virginica
5.9,3.0,5.1,1.8,Iris-virginica

main.js

// This is the main program that sets up a scatter plot to visualize the Iris data set.
// Curran Kelleher March 2015
require(["scatterPlot", "generateColors"], function (ScatterPlot, generateColors) {

  // Initialize the scatter plot.
  var options = {
    
    // Tell the visualization which DOM element to insert itself into.
    container: d3.select("#container").node(),

    // Specify the margin and text label offsets.
    margin: {
      top: 10,
      right: 10,
      bottom: 45,
      left: 55
    },
    yAxisLabelOffset: 1.8, // Unit is CSS "em"s
    xAxisLabelOffset: 1.9,
    titleOffset: 0.3
  };
  var scatterPlot1 = ScatterPlot(options);
  var scatterPlot2 = ScatterPlot(options);
  var scatterPlot3 = ScatterPlot(options);
  var scatterPlot4 = ScatterPlot(options);

  // Fetch the column metadata.
  d3.json("iris-metadata.json", function (metadata) {

    var xColumn = "sepal_length",
        yColumn = "petal_length",
        sizeColumn = "petal_width",
        colorColumn = "class",
        xyOptions = {
          xColumn: xColumn,
          xAxisLabel: metadata[xColumn].label,
          yColumn: yColumn,
          yAxisLabel: metadata[yColumn].label
        };

    // Use the same X and Y for all plots.
    scatterPlot1.set(xyOptions);
    scatterPlot2.set(xyOptions);
    scatterPlot3.set(xyOptions);
    scatterPlot4.set(xyOptions);

    // Use X, Y, and size for the second scatter plot.
    scatterPlot2.sizeColumn = sizeColumn;

    // Use X, Y, and color for the third scatter plot.
    scatterPlot3.colorColumn = colorColumn;
    scatterPlot3.colorRange = d3.scale.category10().range();

    // Use X, Y, size, and color for the fourth scatter plot.
    scatterPlot4.sizeColumn = sizeColumn;
    scatterPlot4.colorColumn = colorColumn;
    scatterPlot4.colorRange = scatterPlot3.colorRange;

    // Load the data from a CSV file.
    d3.csv("iris.csv", function (data){

      // Parse quantitative values from strings to numbers.
      var quantitativeColumns = Object.keys(metadata).filter(function (column){
        return metadata[column].type === "Q";
      });
      data.forEach(function (d){
        quantitativeColumns.forEach(function (column){
          d[column] = parseFloat(d[column]);
        });
      });

      // Pass the data into the plots.
      scatterPlot1.data = data;
      scatterPlot2.data = data;
      scatterPlot3.data = data;
      scatterPlot4.data = data;
    });
  });

  // Sets the `box` model property
  // based on the size of the container,
  function computeBoxes(){
    var width = container.clientWidth,
        height = container.clientHeight,
        padding = 10,
        plotWidth = (width - padding * 4) / 3,
        plotHeight = (height - padding * 3) / 2;
    scatterPlot1.box = {
      x: padding,
      y: height / 2 - plotHeight / 2,
      width: plotWidth,
      height: plotHeight
    };
    scatterPlot2.box = {
      x: plotWidth + padding * 2,
      y: padding,
      width: plotWidth,
      height: plotHeight
    };
    scatterPlot3.box = {
      x: plotWidth + padding * 2,
      y: plotHeight + padding * 2,
      width: plotWidth,
      height: plotHeight
    };
    scatterPlot4.box = {
      x: plotWidth * 2 + padding * 3,
      y: height / 2 - plotHeight / 2,
      width: plotWidth,
      height: plotHeight
    };
  }

  // once to initialize `model.box`, and
  computeBoxes();

  // whenever the browser window resizes in the future.
  window.addEventListener("resize", computeBoxes);
});

scatterPlot.js

// A reusable scatter plot module.
// Curran Kelleher March 2015
define(["d3", "model"], function (d3, Model) {

  // A representation for an optional Model property that is not specified.
  // This allows the "when" approach to support optional properties.
  // Inspired by Scala's Option type.
  // See http://alvinalexander.com/scala/using-scala-option-some-none-idiom-function-java-null
  var None = Model.None;

  // The constructor function, accepting default values.
  return function ScatterPlot(defaults) {

    // Create a Model instance for the visualization.
    // This will serve as its public API.
    var model = Model();

    // Create an SVG element from the container DOM element.
    model.when("container", function (container) {
      model.svg = d3.select(container).append("svg")

        // Use CSS `position: absolute;`
        // so setting `left` and `top` later will
        // position the SVG relative to the container div.
        .style("position", "absolute");
    });

    // Adjust the size of the SVG based on the `box` property.
    model.when(["svg", "box"], function (svg, box) {

      // Set the CSS `left` and `top` properties
      // to move the SVG to `(box.x, box.y)`
      // relative to the container div.
      svg
        .style("left", box.x + "px")
        .style("top", box.y + "px")
        .attr("width", box.width)
        .attr("height", box.height);
    });

    // Create an SVG group that will contain the visualization.
    model.when("svg", function (svg) {
      model.g = svg.append("g");
    });

    // Adjust the SVG group translation based on the margin.
    model.when(["g", "margin"], function (g, margin) {
      g.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    });

    // Create the title text element.
    model.when("g", function (g){
      model.titleText = g.append("text").attr("class", "title-text");
    });

    // Center the title text when width changes.
    model.when(["titleText", "width"], function (titleText, width) {
      titleText.attr("x", width / 2);
    });

    // Update the title text based on the `title` property.
    model.when(["titleText", "title"], function (titleText, title){
      titleText.text(title);
    });

    // Update the title text offset.
    model.when(["titleText", "titleOffset"], function (titleText, titleOffset){
      titleText.attr("dy", titleOffset + "em");
    });

    // Compute the inner box from the outer box and margin.
    // See Margin Convention http://bl.ocks.org/mbostock/3019563
    model.when(["box", "margin"], function (box, margin) {
      model.width = box.width - margin.left - margin.right;
      model.height = box.height - margin.top - margin.bottom;
    });

    // Generate a function for getting the X value.
    model.when(["data", "xColumn"], function (data, xColumn) {
      model.getX = function (d) { return d[xColumn]; };
    });

    // Compute the domain of the X attribute.

    // Allow the API client to optionally specify fixed min and max values.
    model.xDomainMin = None;
    model.xDomainMax = None;
    model.when(["data", "getX", "xDomainMin", "xDomainMax"],
        function (data, getX, xDomainMin, xDomainMax) {

      if(xDomainMin === None && xDomainMax === None){
        model.xDomain = d3.extent(data, getX);
      } else {
        if(xDomainMin === None){
          xDomainMin = d3.min(data, getX);
        }
        if(xDomainMax === None){
          xDomainMax = d3.max(data, getX);
        }
        model.xDomain = [xDomainMin, xDomainMax]
      }
    });

    // Compute the X scale.
    model.when(["xDomain", "width"], function (xDomain, width) {
      model.xScale = d3.scale.linear().domain(xDomain).range([0, width]);
    });

    // Generate a function for getting the scaled X value.
    model.when(["data", "xScale", "getX"], function (data, xScale, getX) {
      model.getXScaled = function (d) { return xScale(getX(d)); };
    });

    // Set up the X axis.
    model.when("g", function (g) {
      model.xAxisG = g.append("g").attr("class", "x axis");
      model.xAxisText = model.xAxisG.append("text").style("text-anchor", "middle");
    });

    // Move the X axis label based on its specified offset.
    model.when(["xAxisText", "xAxisLabelOffset"], function (xAxisText, xAxisLabelOffset){
      xAxisText.attr("dy", xAxisLabelOffset + "em");
    });

    // Update the X axis transform when height changes.
    model.when(["xAxisG", "height"], function (xAxisG, height) {
      xAxisG.attr("transform", "translate(0," + height + ")");
    });

    // Center the X axis label when width changes.
    model.when(["xAxisText", "width"], function (xAxisText, width) {
      xAxisText.attr("x", width / 2);
    });

    // Update the X axis based on the X scale.
    model.when(["xAxisG", "xScale"], function (xAxisG, xScale) {
      xAxisG.call(d3.svg.axis().orient("bottom").scale(xScale));
    });

    // Update X axis label.
    model.when(["xAxisText", "xAxisLabel"], function (xAxisText, xAxisLabel) {
      xAxisText.text(xAxisLabel);
    });

    // Generate a function for getting the Y value.
    model.when(["data", "yColumn"], function (data, yColumn) {
      model.getY = function (d) { return d[yColumn]; };
    });

    // Compute the domain of the Y attribute.

    // Allow the API client to optionally specify fixed min and max values.
    model.yDomainMin = None;
    model.yDomainMax = None;
    model.when(["data", "getY", "yDomainMin", "yDomainMax"],
        function (data, getY, yDomainMin, yDomainMax) {

      if(yDomainMin === None && yDomainMax === None){
        model.yDomain = d3.extent(data, getY);
      } else {
        if(yDomainMin === None){
          yDomainMin = d3.min(data, getY);
        }
        if(yDomainMax === None){
          yDomainMax = d3.max(data, getY);
        }
        model.yDomain = [yDomainMin, yDomainMax]
      }
    });

    // Compute the Y scale.
    model.when(["data", "yDomain", "height"], function (data, yDomain, height) {
      model.yScale = d3.scale.linear().domain(yDomain).range([height, 0]);
    });

    // Generate a function for getting the scaled Y value.
    model.when(["data", "yScale", "getY"], function (data, yScale, getY) {
      model.getYScaled = function (d) { return yScale(getY(d)); };
    });

    // Set up the Y axis.
    model.when("g", function (g) {
      model.yAxisG = g.append("g").attr("class", "y axis");
      model.yAxisText = model.yAxisG.append("text")
        .style("text-anchor", "middle")
        .attr("transform", "rotate(-90)")
        .attr("y", 0);
    });
    
    // Move the Y axis label based on its specified offset.
    model.when(["yAxisText", "yAxisLabelOffset"], function (yAxisText, yAxisLabelOffset){
      yAxisText.attr("dy", "-" + yAxisLabelOffset + "em")
    });

    // Center the Y axis label when height changes.
    model.when(["yAxisText", "height"], function (yAxisText, height) {
      yAxisText.attr("x", -height / 2);
    });

    // Update Y axis label.
    model.when(["yAxisText", "yAxisLabel"], function (yAxisText, yAxisLabel) {
      yAxisText.text(yAxisLabel);
    });

    // Update the Y axis based on the Y scale.
    model.when(["yAxisG", "yScale"], function (yAxisG, yScale) {
      yAxisG.call(d3.svg.axis().orient("left").scale(yScale));
    });

    // Allow the API client to optionally specify a size column.
    model.sizeColumn = None;
    
    // The default radius of circles in pixels.
    model.sizeDefault = 2;

    // The min and max circle radius in pixels.
    model.sizeMin = 0.5;
    model.sizeMax = 6;

    // Set up the size scale.
    model.when(["sizeColumn", "data", "sizeDefault", "sizeMin", "sizeMax"],
        function (sizeColumn, data, sizeDefault, sizeMin, sizeMax){
      if(sizeColumn !== None){
        var getSize = function (d){ return d[sizeColumn] },
            sizeScale = d3.scale.linear()
              .domain(d3.extent(data, getSize))
              .range([sizeMin, sizeMax]);
        model.getSizeScaled = function (d){ return sizeScale(getSize(d)); };
      } else {
        model.getSizeScaled = function (d){ return sizeDefault; };
      }
    });

    // Allow the API client to optionally specify a color column.
    model.colorColumn = None;
    model.colorRange = None;
    
    // The default color of circles (CSS color string).
    model.colorDefault = "black";

    // Set up the color scale.
    model.when(["colorColumn", "data", "colorDefault", "colorRange"],
        function (colorColumn, data, colorDefault, colorRange){
      if(colorColumn !== None && colorRange !== None){
        var getColor = function (d){ return d[colorColumn] },
            colorScale = d3.scale.ordinal()
              .domain(data.map(getColor))
              .range(colorRange);
        model.getColorScaled = function (d){ return colorScale(getColor(d)); };
      } else {
        model.getColorScaled = function (d){ return colorDefault; };
      }
    });

    // Add an SVG group to contain the marks.
    model.when("g", function (g) {
      model.circlesG = g.append("g");
    });

    // Draw the circles of the scatter plot.
    model.when(["data", "circlesG", "getXScaled", "getYScaled", "getSizeScaled", "getColorScaled"],
        function (data, circlesG, getXScaled, getYScaled, getSizeScaled, getColorScaled){

      var circles = circlesG.selectAll("circle").data(data);
      circles.enter().append("circle");
      circles
        .attr("cx", getXScaled)
        .attr("cy", getYScaled)
        .attr("r", getSizeScaled)
        .attr("fill", getColorScaled);
      circles.exit().remove();

    });

    // Set defaults at the end so they override optional properties set to None.
    model.set(defaults);

    return model;
  };
});

styles.css

/* Remove the default margin. */
body {
  margin: 0px;
}

/* Make the visualization container fill the page. */
#container {

  /* Use the default size from bl.ocks.org */
  width: 960px;
  height: 500px;
}

/* Put a border around each plot. */
svg {
  border-style: solid;
  border-color: lightgray;
  border-width: 1px;
}

/* Style the visualization. Draws from http://bl.ocks.org/mbostock/3887118 */

/* Tick mark labels */
.axis .tick text {
  font: 8pt sans-serif;
}

/* Axis labels */
.axis text {
  font: 14pt sans-serif;
}

.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.line {
  fill: none;
  stroke: black;
  stroke-width: 1.5px;
}

.title-text {
  text-anchor: middle;
  font: 24pt sans-serif;
}