Qiskit Beginner Free 31/61 in series 11 min read

Visualizing Quantum Circuits and States in Qiskit

How to visualize quantum circuits, measurement histograms, statevectors, and Bloch sphere representations using Qiskit's built-in tools.

What you'll learn

  • Qiskit
  • visualization
  • circuit diagram
  • Bloch sphere
  • statevector
  • histograms

Prerequisites

  • Basic Python (variables, functions, loops)
  • No quantum physics background needed

Quantum circuits are easy to write and surprisingly hard to debug. A gate applied to the wrong qubit, a missing barrier, or an incorrect phase will not throw an exception. The circuit runs, returns results, and you have no obvious indication anything went wrong until you examine the output carefully.

Visualization is how you catch these mistakes. Qiskit ships with a rich set of tools for drawing circuits, plotting measurement distributions, and inspecting quantum states from multiple angles. This tutorial covers all of them.

Why Visualization Matters

Three use cases justify spending time on visualization tools:

Debugging: A circuit drawing shows you the gate sequence and qubit ordering at a glance. You can spot a CNOT pointing the wrong direction or a Hadamard applied to the wrong wire faster than you can parse the code.

Understanding gate effects: Watching how the Bloch sphere representation of a qubit changes as you add gates builds intuition about what each gate does geometrically.

Communicating results: Histograms, state city plots, and Q-spheres are standard figures in quantum computing papers and documentation. Knowing how to produce them cleanly saves time when writing up results.

Circuit Diagrams with qc.draw()

The most-used visualization in Qiskit is qc.draw(). It accepts three backends:

from qiskit import QuantumCircuit

qc = QuantumCircuit(3, 3)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
qc.barrier()
qc.measure([0, 1, 2], [0, 1, 2])

# Text (ASCII) -- works in any terminal or notebook
print(qc.draw('text'))

# Matplotlib -- returns a Figure object, displays inline in Jupyter
fig = qc.draw('mpl')
fig.savefig('circuit.png', dpi=150, bbox_inches='tight')

# LaTeX source -- returns a string you can compile with pdflatex
latex_str = qc.draw('latex_source')

The 'text' backend works anywhere and requires no extra dependencies. The 'mpl' backend produces publication-quality figures. The 'latex_source' backend generates quantikz LaTeX code for use in papers.

Customizing Circuit Diagrams

For long circuits, the diagram wraps to multiple rows when you set fold:

qc.draw('mpl', fold=40)
# Wraps the diagram at 40 gate columns

Other useful options:

qc.draw(
    'mpl',
    plot_barriers=True,    # Show barrier lines (default: True)
    initial_state=True,    # Label qubit wires with |0> at the start
    style={'backgroundcolor': '#EEEEEE'}  # Custom color scheme
)

To see gate parameter values on the diagram:

from qiskit.circuit import Parameter
theta = Parameter('θ')

qc2 = QuantumCircuit(1)
qc2.ry(theta, 0)
qc2.draw('mpl')   # Shows Ry(θ) on the wire with the parameter label

Measurement Histograms

After running a circuit, plot_histogram converts the counts dictionary into a bar chart:

from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
from qiskit.visualization import plot_histogram

qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])

simulator = AerSimulator()
job = simulator.run(qc, shots=1000)
counts = job.result().get_counts()

fig = plot_histogram(counts)
fig.savefig('histogram.png', dpi=150, bbox_inches='tight')
print(counts)
# Output: {'00': ~498, '11': ~502}

To compare two experiments side by side:

# Run a second circuit and compare distributions
counts2 = {'00': 300, '01': 200, '10': 250, '11': 250}
fig = plot_histogram([counts, counts2], legend=['Bell state', 'Comparison'])

The legend parameter labels each dataset in the combined chart.

Statevector and Bloch Sphere Visualization

To inspect the quantum state before measurement, use the Statevector class directly:

from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_bloch_vector, plot_bloch_multivector

# Single qubit: visualize on the Bloch sphere
qc_single = QuantumCircuit(1)
qc_single.h(0)
sv = Statevector(qc_single)
print(sv)
# Output: Statevector([0.70710678+0.j, 0.70710678+0.j],
#            dims=(2,))

# Plot on a Bloch sphere using the Bloch vector [x, y, z]
from qiskit.quantum_info import SparsePauliOp
bloch_vec = [float(sv.expectation_value(SparsePauliOp(op)).real)
             for op in ['X', 'Y', 'Z']]  # Use Pauli expectations
# Simpler: use plot_bloch_multivector for automatic extraction
fig = plot_bloch_multivector(sv)
fig.savefig('bloch_single.png', dpi=150)

For multi-qubit states, plot_bloch_multivector renders one Bloch sphere per qubit:

# Two-qubit Bell state
qc_bell = QuantumCircuit(2)
qc_bell.h(0)
qc_bell.cx(0, 1)
sv_bell = Statevector(qc_bell)

fig = plot_bloch_multivector(sv_bell)
fig.savefig('bloch_bell.png', dpi=150)
# Both qubits show maximally mixed single-qubit states (point at origin)
# because their reduced states are mixed -- entanglement hides on individual spheres

State City Plot

The state city plot shows the density matrix as a 3D bar chart. Real parts of the matrix elements form one set of bars, imaginary parts form another:

from qiskit.visualization import plot_state_city

qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
sv = Statevector(qc)

fig = plot_state_city(sv)
fig.savefig('state_city.png', dpi=150)

For the Bell state, the density matrix has four nonzero elements: the two diagonal entries rho[00,00] and rho[11,11], plus the two off-diagonal coherences rho[00,11] and rho[11,00]. The city plot makes this structure immediately visible as four tall bars in a flat landscape.

State Hinton Diagram

The Hinton diagram represents amplitude magnitudes as squares on a grid. Larger squares indicate larger amplitudes:

from qiskit.visualization import plot_state_hinton

fig = plot_state_hinton(sv)
fig.savefig('hinton.png', dpi=150)

The Hinton diagram is useful when you want a quick view of which basis states have significant amplitude without the 3D perspective of the city plot.

Q-Sphere

The Q-sphere maps all basis states onto the surface of a sphere, with state amplitudes shown as circles sized by probability and colored by phase. It is particularly good at showing multi-qubit superpositions:

from qiskit.visualization import plot_state_qsphere

# GHZ state: (|000> + |111>) / sqrt(2)
qc_ghz = QuantumCircuit(3)
qc_ghz.h(0)
qc_ghz.cx(0, 1)
qc_ghz.cx(0, 2)
sv_ghz = Statevector(qc_ghz)

fig = plot_state_qsphere(sv_ghz)
fig.savefig('qsphere_ghz.png', dpi=150)
# Shows two large circles: one at the north pole for |000>, one at the south for |111>

States near the north pole of the Q-sphere correspond to basis states with more 0s; states near the south pole have more 1s. The GHZ state shows the classic two-poles pattern.

Density Matrix Visualization

For mixed states (realistic noisy scenarios), the density matrix visualization is more informative than statevector plots:

from qiskit.quantum_info import DensityMatrix, partial_trace
from qiskit.visualization import plot_state_city

# Simulate a mixed state by tracing out one qubit of a Bell pair
qc_bell = QuantumCircuit(2)
qc_bell.h(0)
qc_bell.cx(0, 1)
sv_bell = Statevector(qc_bell)

# Reduced density matrix of qubit 0 (traced over qubit 1)
dm_reduced = partial_trace(sv_bell, [1])
print(dm_reduced)
# Output: DensityMatrix([[0.5+0.j, 0. +0.j],
#            [0. +0.j, 0.5+0.j]],
#           dims=(2,))
# This is the maximally mixed single-qubit state: I/2

fig = plot_state_city(dm_reduced)
fig.savefig('mixed_state_city.png', dpi=150)

Running plot_state_city or plot_state_hinton on a DensityMatrix object works the same as on a Statevector. Mixed states have real diagonal entries that sum to 1 and off-diagonal entries that indicate coherence.

Saving Figures

Every Qiskit visualization function returns a Matplotlib Figure object. Save it using the standard savefig method:

fig = plot_histogram(counts)
fig.savefig(
    'output.png',
    dpi=150,           # Dots per inch: 150 is good for screen, 300 for print
    bbox_inches='tight' # Crop whitespace from the edges
)

# Save as PDF for vector graphics (scalable without pixelation)
fig.savefig('output.pdf', bbox_inches='tight')

To prevent a plot from displaying interactively while still saving it, close the figure after saving:

import matplotlib.pyplot as plt
fig = plot_histogram(counts)
fig.savefig('histogram.png', dpi=150, bbox_inches='tight')
plt.close(fig)

Interactive Visualization in Jupyter

In a Jupyter notebook, all Qiskit visualization functions display inline automatically if you set up matplotlib for inline rendering:

In a Jupyter cell (these are IPython magic commands, not valid in plain .py scripts):

%matplotlib inline
# or for higher-resolution displays:
%config InlineBackend.figure_format = 'retina'

Circuit diagrams with the 'mpl' backend render inline without calling savefig:

qc.draw('mpl')   # Displays the figure directly in the cell output

For the text backend in a Jupyter notebook, wrap the output in print() to force monospace rendering:

print(qc.draw('text'))

Without print(), the raw TextDrawing object displays as a string representation that loses its alignment.

Summary

Qiskit’s visualization toolkit covers every stage of circuit inspection: qc.draw() for the gate sequence, plot_histogram() for measurement results, plot_bloch_multivector() for single-qubit state geometry, plot_state_city() and plot_state_hinton() for density matrix structure, and plot_state_qsphere() for multi-qubit amplitude patterns. All of them return Matplotlib figures you can save or display inline. Building the habit of visualizing your circuits before and after adding gates catches most common mistakes before they waste simulation time.

Further Reading

Was this tutorial helpful?