Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Bar Charts

single-view-bar-plots.livemd

Bar Charts

Mix.install([
  {:vega_lite, "~> 0.1.5"},
  {:kino_vega_lite, "~> 0.1.1"},
  :jason
])

alias VegaLite, as: Vl

Simple bar chart

A bar chart encodes quantitative values as the extent of rectangular bars.

data = [
  %{"a" => "A", "b" => 28},
  %{"a" => "B", "b" => 55},
  %{"a" => "C", "b" => 43},
  %{"a" => "D", "b" => 91},
  %{"a" => "E", "b" => 81},
  %{"a" => "F", "b" => 53},
  %{"a" => "G", "b" => 19},
  %{"a" => "H", "b" => 87},
  %{"a" => "I", "b" => 52}
]

Vl.new()
|> Vl.data_from_values(data)
|> Vl.mark(:bar)
|> Vl.encode(:x, field: :a, type: :nominal, axis: [label_angle: 40])
|> Vl.encode(:y, field: :b, type: :quantitative)

Responsive Bar Chart

The bar gets automatically resized based on container size. To see how the bar gets automatically resized try this in the editor.

I could´t use “width”: “container”. Is this possible in livebook?

Vl.new(width: 300, heigh: 250)
|> Vl.data_from_url("https://vega.github.io/vega-lite/examples/data/cars.json")
|> Vl.mark(:bar)
|> Vl.encode(:x, field: "Origin")
|> Vl.encode(:y, aggregate: :count, title: "Number of Cars")

Section

Aggregate Bar Chart

A bar chart showing the US population distribution of age groups in 2000.

Vl.new(
  height: [step: 17],
  description: "A bar chart showing the US population distribution of age groups in 2000."
)
|> Vl.data_from_url("https://vega.github.io/vega-lite/examples/data/population.json")
|> Vl.transform(filter: "datum.year == 2000")
|> Vl.mark(:bar)
|> Vl.encode(:y, field: "age")
|> Vl.encode(:x, field: "people", aggregate: :sum, title: "population")

Aggregate Bar Chart (Sorted)

A bar chart that sorts the y-values by the x-values.

Vl.new(
  height: [step: 17],
  description: "A bar chart that sorts the y-values by the x-values."
)
|> Vl.data_from_url("https://vega.github.io/vega-lite/examples/data/population.json")
|> Vl.transform(filter: "datum.year == 2000")
|> Vl.mark(:bar)
|> Vl.encode(:y, field: "age", type: :ordinal, sort: "-x")
|> Vl.encode(:x, field: "people", aggregate: :sum, title: "population")

Grouped Bar Chart

Read here for more details about how to set step size for grouped bar.

data = [
  %{"category" => "A", "group" => "x", "value" => 0.1},
  %{"category" => "A", "group" => "y", "value" => 0.6},
  %{"category" => "A", "group" => "z", "value" => 0.9},
  %{"category" => "B", "group" => "x", "value" => 0.7},
  %{"category" => "B", "group" => "y", "value" => 0.2},
  %{"category" => "B", "group" => "z", "value" => 1.1},
  %{"category" => "C", "group" => "x", "value" => 0.6},
  %{"category" => "C", "group" => "y", "value" => 0.1},
  %{"category" => "C", "group" => "z", "value" => 0.2}
]

Vl.new()
|> Vl.data_from_values(data)
|> Vl.mark(:bar)
|> Vl.encode(:x, field: "category")
|> Vl.encode(:y, field: "value", type: :quantitative)
|> Vl.encode(:x_offset, field: "group")
|> Vl.encode(:color, field: "group")

Grouped Bar Chart (Multiple Measure with Repeat)

Read here for more details about how to set step size for grouped bar.

Vl.new()
|> Vl.data_from_url("https://vega.github.io/vega-lite/examples/data/movies.json")
|> Vl.repeat(
  [layer: ["Worldwide Gross", "US Gross"]],
  Vl.new()
  |> Vl.mark(:bar)
  |> Vl.encode(:x, field: "Major Genre", type: :nominal)
  |> Vl.encode_repeat(:y, :layer, aggregate: :sum, type: :quantitative, title: "Total Gross")
  |> Vl.encode(:color, datum: [repeat: :layer], type: :nominal, title: "Gross")
  |> Vl.encode(:x_offset, datum: [repeat: :layer])
)

Stacked Bar Chart

Vl.new()
|> Vl.data_from_url("https://vega.github.io/vega-lite/examples/data/seattle-weather.csv")
|> Vl.mark(:bar)
|> Vl.encode_field(:x, "date", type: :ordinal, time_unit: :month, title: "Month of the year")
|> Vl.encode(:y, aggregate: :count, type: :quantitative)
|> Vl.encode_field(:color, "weather",
  type: :nominal,
  scale: [
    domain: ["sun", "fog", "drizzle", "rain", "snow"],
    range: ["#e7ba52", "#c7c7c7", "#aec7e8", "#1f77b4", "#9467bd"]
  ]
)

Stacked Bar Chart with Rounded Corners

Vl.new()
|> Vl.data_from_url("https://vega.github.io/vega-lite/examples/data/seattle-weather.csv")
|> Vl.mark(:bar, corner_radius_top_left: 10, corner_radius_top_right: 10)
|> Vl.encode_field(:x, "date", type: :ordinal, time_unit: :month, title: "Month of the year")
|> Vl.encode(:y, aggregate: :count, type: :quantitative)
|> Vl.encode_field(:color, "weather")

Horizontal Stacked Bar Chart

Vl.new()
|> Vl.data_from_url("https://vega.github.io/vega-lite/examples/data/barley.json")
|> Vl.mark(:bar)
|> Vl.encode_field(:y, "variety")
|> Vl.encode_field(:x, "yield", aggregate: :sum)
|> Vl.encode_field(:color, "site")

Normalized (Percentage) Stacked Bar Chart

Vl.new(width: [step: 13])
|> Vl.data_from_url("https://vega.github.io/vega-lite/examples/data/population.json")
|> Vl.transform(filter: "datum.year == 2000")
|> Vl.transform(calculate: "datum.sex == 2 ? 'Female' : 'Male'", as: "gender")
|> Vl.mark(:bar)
|> Vl.encode_field(:x, "age")
|> Vl.encode_field(:y, "people", aggregate: :sum, stack: :normalize)
|> Vl.encode_field(:color, "gender", scale: [range: ["#675193", "#ca8861"]])

Normalized (Percentage) Stacked Bar Chart With Labels

Vl.new()
|> Vl.data_from_url("https://vega.github.io/vega-lite/examples/data/population.json")
|> Vl.transform(filter: "datum.year == 2000")
|> Vl.transform(calculate: "datum.sex == 2 ? 'Female' : 'Male'", as: "gender")
|> Vl.encode_field(:y, "age")
|> Vl.layers([
  Vl.new()
  |> Vl.mark(:bar)
  |> Vl.encode_field(:x, "people", aggregate: :sum, stack: :normalize, title: "population")
  |> Vl.encode_field(:color, "gender", scale: [range: ["#675193", "#ca8861"]]),
  Vl.new()
  |> Vl.mark(:text, opacity: 0.9, color: :white)
  |> Vl.encode_field(:x, "people", aggregate: :sum, stack: :normalize, band_position: 0.5)
  |> Vl.encode_field(:text, "people", aggregate: :sum, title: "population")
  |> Vl.encode_field(:detail, "gender")
])

Gantt Chart (Ranged Bar Marks)

A simple bar chart with ranged data (aka Gantt Chart).

data = [
  %{"task" => "A", "start" => 1, "end" => 3},
  %{"task" => "B", "start" => 3, "end" => 8},
  %{"task" => "C", "start" => 8, "end" => 10}
]

Vl.new()
|> Vl.data_from_values(data)
|> Vl.mark(:bar)
|> Vl.encode_field(:y, "task", type: :ordinal)
|> Vl.encode_field(:x, "start", type: :quantitative)
|> Vl.encode_field(:x2, "end")

A Bar Chart Encoding Color Names in the Data

A bar chart that directly encodes color names in the data.

data = [
  %{"color" => "red", "b" => 28},
  %{"color" => "green", "b" => 55},
  %{"color" => "blue", "b" => 43}
]

Vl.new()
|> Vl.data_from_values(data)
|> Vl.mark(:bar)
|> Vl.encode_field(:x, "color", type: :nominal)
|> Vl.encode_field(:y, "b", type: :quantitative)
|> Vl.encode_field(:color, "color", type: :nominal, scale: false)

Layered Bar Chart

A bar chart showing the US population distribution of age groups and gender in 2000.

Vl.new(width: [step: 17])
|> Vl.data_from_url("https://vega.github.io/vega-lite/examples/data/population.json")
|> Vl.transform(filter: "datum.year == 2000")
|> Vl.transform(calculate: "datum.sex == 2 ? 'Female' : 'Male'", as: "gender")
|> Vl.mark(:bar)
|> Vl.encode_field(:x, "age", type: :ordinal)
|> Vl.encode_field(:y, "people", aggregate: "sum", title: "population", stack: false)
|> Vl.encode_field(:color, "gender", scale: [range: ["#675193", "#ca8861"]])
|> Vl.encode(:opacity, value: 0.6)

###

Diverging Stacked Bar Chart (Population Pyramid)

A population pyramid for the US in 2000, created using stack. See https://vega.github.io/vega-lite/examples/concat_population_pyramid.html for a variant of this created using concat.

Vl.new(width: 300, height: 200)
|> Vl.data_from_url("https://vega.github.io/vega-lite/examples/data/population.json")
|> Vl.transform(filter: "datum.year == 2000")
|> Vl.transform(calculate: "datum.sex == 2 ? 'Female' : 'Male'", as: "gender")
|> Vl.transform(calculate: "datum.sex == 2 ? -datum.people : datum.people", as: "signed_people")
|> Vl.mark(:bar)
|> Vl.encode_field(:y, "age", sort: :descending, axis: false)
|> Vl.encode_field(:x, "signed_people", aggregate: :sum, axis: [format: :s])
|> Vl.encode_field(
  :color,
  "gender",
  scale: [range: ["#675193", "#ca8861"]],
  legend: [orient: :top, title: false]
)
|> Vl.config(view: [stroke: false], axis: [grid: false])

Diverging Stacked Bar Chart (with Neutral Parts)

A diverging stacked bar chart for sentiments towards a set of eight questions, displayed as percentages with neutral responses straddling the 0% mark

TODO set correct x

data = [
  %{
    "question" => "Question 1",
    "type" => "Strongly disagree",
    "value" => 24,
    "percentage" => 0.7
  },
  %{"question" => "Question 1", "type" => "Disagree", "value" => 294, "percentage" => 9.1},
  %{
    "question" => "Question 1",
    "type" => "Neither agree nor disagree",
    "value" => 594,
    "percentage" => 18.5
  },
  %{"question" => "Question 1", "type" => "Agree", "value" => 1927, "percentage" => 59.9},
  %{"question" => "Question 1", "type" => "Strongly agree", "value" => 376, "percentage" => 11.7},
  %{
    "question" => "Question 2",
    "type" => "Strongly disagree",
    "value" => 2,
    "percentage" => 18.2
  },
  %{"question" => "Question 2", "type" => "Disagree", "value" => 2, "percentage" => 18.2},
  %{
    "question" => "Question 2",
    "type" => "Neither agree nor disagree",
    "value" => 0,
    "percentage" => 0
  },
  %{"question" => "Question 2", "type" => "Agree", "value" => 7, "percentage" => 63.6},
  %{"question" => "Question 2", "type" => "Strongly agree", "value" => 11, "percentage" => 0},
  %{"question" => "Question 3", "type" => "Strongly disagree", "value" => 2, "percentage" => 20},
  %{"question" => "Question 3", "type" => "Disagree", "value" => 0, "percentage" => 0},
  %{
    "question" => "Question 3",
    "type" => "Neither agree nor disagree",
    "value" => 2,
    "percentage" => 20
  },
  %{"question" => "Question 3", "type" => "Agree", "value" => 4, "percentage" => 40},
  %{"question" => "Question 3", "type" => "Strongly agree", "value" => 2, "percentage" => 20},
  %{"question" => "Question 4", "type" => "Strongly disagree", "value" => 0, "percentage" => 0},
  %{"question" => "Question 4", "type" => "Disagree", "value" => 2, "percentage" => 12.5},
  %{
    "question" => "Question 4",
    "type" => "Neither agree nor disagree",
    "value" => 1,
    "percentage" => 6.3
  },
  %{"question" => "Question 4", "type" => "Agree", "value" => 7, "percentage" => 43.8},
  %{"question" => "Question 4", "type" => "Strongly agree", "value" => 6, "percentage" => 37.5},
  %{"question" => "Question 5", "type" => "Strongly disagree", "value" => 0, "percentage" => 0},
  %{"question" => "Question 5", "type" => "Disagree", "value" => 1, "percentage" => 4.2},
  %{
    "question" => "Question 5",
    "type" => "Neither agree nor disagree",
    "value" => 3,
    "percentage" => 12.5
  },
  %{"question" => "Question 5", "type" => "Agree", "value" => 16, "percentage" => 66.7},
  %{"question" => "Question 5", "type" => "Strongly agree", "value" => 4, "percentage" => 16.7},
  %{"question" => "Question 6", "type" => "Strongly disagree", "value" => 1, "percentage" => 6.3},
  %{"question" => "Question 6", "type" => "Disagree", "value" => 1, "percentage" => 6.3},
  %{
    "question" => "Question 6",
    "type" => "Neither agree nor disagree",
    "value" => 2,
    "percentage" => 12.5
  },
  %{"question" => "Question 6", "type" => "Agree", "value" => 9, "percentage" => 56.3},
  %{"question" => "Question 6", "type" => "Strongly agree", "value" => 3, "percentage" => 18.8},
  %{"question" => "Question 7", "type" => "Strongly disagree", "value" => 0, "percentage" => 0},
  %{"question" => "Question 7", "type" => "Disagree", "value" => 0, "percentage" => 0},
  %{
    "question" => "Question 7",
    "type" => "Neither agree nor disagree",
    "value" => 1,
    "percentage" => 20
  },
  %{"question" => "Question 7", "type" => "Agree", "value" => 4, "percentage" => 80},
  %{"question" => "Question 7", "type" => "Strongly agree", "value" => 0, "percentage" => 0},
  %{"question" => "Question 8", "type" => "Strongly disagree", "value" => 0, "percentage" => 0},
  %{"question" => "Question 8", "type" => "Disagree", "value" => 0, "percentage" => 0},
  %{
    "question" => "Question 8",
    "type" => "Neither agree nor disagree",
    "value" => 0,
    "percentage" => 0
  },
  %{"question" => "Question 8", "type" => "Agree", "value" => 0, "percentage" => 0},
  %{"question" => "Question 8", "type" => "Strongly agree", "value" => 2, "percentage" => 100}
]

Vl.new()
|> Vl.data_from_values(data)
|> Vl.transform(
  calculate:
    "if(datum.type === 'Strongly disagree',-2,0) + if(datum.type==='Disagree',-1,0) + if(datum.type =='Neither agree nor disagree',0,0) + if(datum.type ==='Agree',1,0) + if(datum.type ==='Strongly agree',2,0)",
  as: "q_order"
)
|> Vl.transform(
  calculate:
    "if(datum.type === 'Disagree' || datum.type === 'Strongly disagree', datum.percentage,0) + if(datum.type === 'Neither agree nor disagree', datum.percentage / 2,0)",
  as: "signed_percentage"
)
|> Vl.transform(stack: "percentage", as: ["v1", "v2"], groupby: ["question"])
|> Vl.transform(joinaggregate: [[field: "signed_percentage", op: "sum", as: "offset"]])
|> Vl.transform(groupby: ["question"])
|> Vl.transform(calculate: "datum.v1 - datum.offset", as: "nx")
|> Vl.transform(calculate: "datum.v2 - datum.offset", as: "nx2")
|> Vl.mark(:bar)
|> Vl.encode_field(:x, "nx", type: :quantitative, title: "percentage")
|> Vl.encode_field(:x2, "nx2")
|> Vl.encode_field(:y, "question",
  type: :nominal,
  title: "Question",
  axis: [offset: 5, ticks: false, min_extent: 60, domain: false]
)
|> Vl.encode_field(:color, "type",
  title: "Response",
  scale: [
    domain: [
      "Strongly disagree",
      "Disagree",
      "Neither agree nor disagree",
      "Agree",
      "Strongly agree"
    ],
    range: ["#c30d24", "#f3a583", "#cccccc", "#94c6da", "#1770ab"],
    type: :ordinal
  ]
)

# |> Vl.to_spec()

Simple Bar Chart with Labels

Bar chart with text labels. Set domain to make the frame cover the labels.

data = [
  %{"a" => "A", "b" => 28},
  %{"a" => "B", "b" => 55},
  %{"a" => "C", "b" => 43}
]

Vl.new()
|> Vl.data_from_values(data)
|> Vl.encode_field(:y, "a", type: :nominal)
|> Vl.encode_field(:x, "b", type: :quantitative, scale: [domain: [0, 60]])
|> Vl.layers([
  Vl.new()
  |> Vl.mark(:bar),
  Vl.new()
  |> Vl.mark(:text, align: :left, baseline: :middle, dx: 3)
  |> Vl.encode_field(:text, "b", type: :quantitative)
])

Bar Chart with Label Overlays

TODO font looks like superposed

Vl.new(width: 300, height: [step: 20])
|> Vl.data_from_url("https://vega.github.io/vega-lite/examples/data/movies.json")
|> Vl.encode_field(:y, "Major Genre", type: :nominal, axis: false)
|> Vl.layers([
  Vl.new()
  |> Vl.mark(:bar, color: "#ddd")
  |> Vl.encode_field(:x, "IMDB Rating",
    aggregate: :mean,
    scale: [domain: [0, 10]],
    title: "Mean IMDB Ratings"
  ),
  Vl.new()
  |> Vl.mark(:text, align: :left, x: 5)
  |> Vl.encode_field(:text, "Major Genre", detail: [aggregate: :count])
])

Bar Chart showing Initials of Month Names

Using labelExpr to show only initial letters of month names.

Vl.new()
|> Vl.data_from_url("https://vega.github.io/vega-lite/examples/data/seattle-weather.csv")
|> Vl.mark(:bar)
|> Vl.encode_field(:x, "date",
  time_unit: "month",
  axis: [
    label_align: "left",
    label_expr: "datum.label[0]"
  ]
)
|> Vl.encode_field(:y, "precipitation", aggregate: :mean)

Bar Chart with Negative Values and a Zero-Baseline

A bar chart with negative values. We can hide the axis domain line, and instead use a conditional grid color to draw a zero baseline.

data = [
  %{"a" => "A", "b" => -28},
  %{"a" => "B", "b" => 55},
  %{"a" => "C", "b" => -33},
  %{"a" => "D", "b" => 91},
  %{"a" => "E", "b" => 81},
  %{"a" => "F", "b" => 53},
  %{"a" => "G", "b" => -19},
  %{"a" => "H", "b" => 87},
  %{"a" => "I", "b" => 52}
]

Vl.new()
|> Vl.data_from_values(data)
|> Vl.mark(:bar)
|> Vl.encode_field(:x, "a",
  type: :nominal,
  axis: [domain: false, ticks: false, label_angle: 0, label_padding: 4]
)
|> Vl.encode_field(:y, "b",
  type: :quantitative,
  axis: [
    grid_color: [
      condition: [test: "datum.value ===0", value: "black"],
      value: "#ddd"
    ]
  ]
)

Horizontal Bar Chart with Negative Values and Labels

A bar chart with negative values. We can hide the axis domain line, and instead use a conditional grid color to draw a zero baseline.

data = [
  %{"a" => "A", "b" => -28},
  %{"a" => "B", "b" => 55},
  %{"a" => "C", "b" => -33},
  %{"a" => "D", "b" => 91},
  %{"a" => "E", "b" => 81},
  %{"a" => "F", "b" => 53},
  %{"a" => "G", "b" => -19},
  %{"a" => "H", "b" => 87},
  %{"a" => "I", "b" => 52}
]

Vl.new()
|> Vl.data_from_values(data)
|> Vl.encode_field(:y, "a",
  type: :nominal,
  axis: [domain: false, ticks: false, label_angle: 0, label_padding: 4]
)
|> Vl.encode_field(:x, "b",
  type: :quantitative,
  scale: [padding: 20],
  axis: [
    grid_color: [
      condition: [test: "datum.value ===0", value: "black"],
      value: "#ddd"
    ]
  ]
)
|> Vl.layers([
  Vl.new()
  |> Vl.mark(:bar),
  Vl.new()
  |> Vl.mark(:text,
    align: [expr: "datum.b < 0 ? 'right' : 'left'"],
    dx: [expr: "datum.b < 0 ? -2 : 2"]
  )
  |> Vl.encode_field(:text, "b", type: :quantitative)
])

Bar Chart with a Spacing-Saving Y-Axis

Bar Chart with a spacing-saving y-axis

Vl.new(height: [step: 50])
|> Vl.data_from_url("https://vega.github.io/vega-lite/examples/data/cars.json")
|> Vl.mark(:bar, y_offset: 5, corner_radius_end: 2, height: [band: 0.5])
|> Vl.encode_field(:y, "Origin",
  scale: [padding: 0],
  axis: [
    band_position: 0,
    grid: true,
    domain: false,
    ticks: false,
    label_align: "left",
    label_baseline: "middle",
    label_padding: -5,
    label_offset: -15,
    title_x: 5,
    title_y: -5,
    title_angle: 0,
    title_align: "left"
  ]
)
|> Vl.encode(:x, aggregate: :count, axis: [grid: false], title: "Number of cars")

Heat Lane Chart

Heat lane chart based on https://www.smashingmagazine.com/2022/07/accessibility-first-approach-chart-visual-design/

Vl.new(height: 150, width: 400)
|> Vl.data_from_url("https://vega.github.io/vega-lite/examples/data/cars.json")
|> Vl.transform(
  bin: true,
  field: "Horsepower",
  as: ["bin_Horsepower_start", "bin_Horsepower_end"]
)
|> Vl.transform(
  aggregate: [[op: :count, as: :count]],
  groupby: ["bin_Horsepower_start", "bin_Horsepower_end"]
)
|> Vl.transform(bin: true, field: :count, as: ["bin_count_start", "bin_count_end"])
|> Vl.transform(calculate: "-datum.bin_count_end/2", as: "y2")
|> Vl.transform(calculate: "datum.bin_count_end/2", as: "y")
|> Vl.transform(joinaggregate: [[field: "bin_count_end", op: "max", as: "max_bin_count_end"]])
|> Vl.encode_field(:x, "bin_Horsepower_start",
  type: :quantitative,
  title: "Horsepower",
  axis: [grid: false]
)
|> Vl.encode_field(:x2, "bin_Horsepower_end")
|> Vl.encode_field(:y, "y", axis: false)
|> Vl.encode_field(:y2, "y2")
|> Vl.layers([
  Vl.new()
  |> Vl.mark(:bar, x_offset: 2, x2_offset: -2, corner_radius: 3)
  |> Vl.encode_field(:color, "max_bin_count_end",
    type: :ordinal,
    title: "count",
    scale: [scheme: "lighttealblue"]
  ),
  Vl.new()
  |> Vl.mark(:bar, x_offset: 2, x2_offset: -2, y_offset: -3, y2_offset: 3)
  |> Vl.encode_field(:color, "bin_count_end", type: :ordinal, title: "count")
])