Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import { cars as newDataset } from "@observablehq/plot-test-data"
Insert cell
newDataset
Insert cell
newDatasetData = newDataset.data
Insert cell
Insert cell
newDatasetData
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
Insert cell
Insert cell
// Select a row from the table as a reference to extract attribute names.
referenceRow = tableData[0]
Insert cell
Insert cell
// Extract all attribute names from the reference row and filter by numeric attributes.
numericAttributes = Object.keys(referenceRow).filter(key => !isNaN(referenceRow[key]))
Insert cell
Insert cell
// Convert the table data into an array of rows, where each row is an array of values for all numeric attributes.
rowData = tableData.map(row => Object.keys(row).filter(key => numericAttributes.includes(key)).map(key => row[key]))
Insert cell
// Convert the table data into an array of columns, where each column is an array of values for all numeric attributes.
columnData = numericAttributes.map(key => tableData.map(row => row[key]))
Insert cell
Insert cell
// Set the width of the table visualization.
tableWidth = width
Insert cell
columnScale = d3.scaleBand() // Get a scale object for discrete bands (columns).
.domain(d3.range(columnData.length)) // Input from zero to maximum column index.
.range([0, tableWidth]) // Output from left to right edge of drawing area (image coordinates).
.padding(0.1) // Padding between columns.
Insert cell
// Set the height of the table visualization (here, at least one pixel per row).
tableHeight = Math.max(450, rowData.length);
Insert cell
rowScale = d3.scaleBand() // Get a scale object for discrete bands (rows).
.domain(d3.range(rowData.length)) // Input from zero to maximum row index.
.range([0, tableHeight]) // Output from top to bottom edge of drawing area (image coordinates).
.padding(0) // Padding between rows.
Insert cell
Insert cell
function blackAndWhiteColorScale(data, index, value) {
const scale = d3.scaleLinear() // Get a linear scale object.
.domain(d3.extent(data[index])) // Input between minimum and maximum of data values.
.range(['black','white']); // Output interpolated colors between black and white (🛠️ Try some other colors!).
return scale(value);
}
Insert cell
Insert cell
function createTable(width, height, columnScale, rowScale, colorScale, columnData, rowData) {
// Create the SVG image.
const svg = d3.select(DOM.svg(width, height));

// Select and create a SVG group for each row.
const rows = svg.selectAll('.row') // Select all groups with class name 'row'.
.data(rowData)
.join('g')
.attr('class', 'row'); // Give each row group a class name to distinguish it from other groups in the SVG image.

// Loop throught the row group and add the table cells.
rows.each(function(cellValues, rowIndex) {
// Add colored rectangles (table cells) for each row.
d3.select(this) // Select the current row group.
.selectAll('rect') // Select all rectangles.
.data(cellValues) // Attach the data for the current row the selection.
.join('rect') // Create / update / remove rectangles for the mapping.
.attr('x', (_, i) => columnScale(i)) // Set the rectangle x position.
.attr('y', rowScale(rowIndex)) // Set the rectangle y position.
.attr('width', columnScale.bandwidth()) // Set the rectangle width.
.attr('height', rowScale.bandwidth()) // Set the rectangle height.
.style('fill', (d, i) => colorScale(columnData, i, d)); // Set the rectangle fill color.
});

// Return the HTML element for the SVG image.
return svg.node();
}
Insert cell
Insert cell
createTable(tableWidth, tableHeight, columnScale, rowScale, blackAndWhiteColorScale, columnData, rowData)
Insert cell
Insert cell
Insert cell
Insert cell
tableHeaderHeight = 30 // Height of the column header in the image.
Insert cell
Insert cell
rowScaleWithColumnHeader = d3.scaleBand()
.domain(d3.range(rowData.length))
.range([tableHeaderHeight, tableHeight]) // ⬅️ Solution: Consider header height in row scale. ✔️
.padding(0);
Insert cell
Insert cell
tableWithHeaders = createTable(tableWidth, tableHeight, columnScale, rowScaleWithColumnHeader, blackAndWhiteColorScale, columnData, rowData)
Insert cell
Insert cell
{
// Select the table to be modified.
const svg = d3.select(tableWithHeaders);
// ⬇️ Solution: Create a group for each column. ✔️
const columns = svg.selectAll('.column') // Select the column groups.
.data(numericAttributes) // Assign the attribute names to the column groups.
.join('g') // Mapping between column groups and attribute name data.
.attr('class', 'column');

// Append a SVG text element to each column.
columns.append('text')
.attr('x', (_, i) => columnScale(i) + columnScale.bandwidth() * 0.5) // Set x position of text.
.attr('y', tableHeaderHeight * 0.5) // Set y position of text.
.attr('font-size', tableHeaderHeight * 0.5) // Set the font size.
.attr('text-anchor', 'middle') // Center text horizontally.
.attr('dominant-baseline', 'central') // Center text vertically.
.style('cursor', 'default') // Set default cursor icon.
.style('user-select', 'none') // Prevent text selection.
.text(d => d); // Set the text based on the assigned attribute name.
}
Insert cell
Insert cell
Insert cell
// An array of available color scheme names.
colorSchemeNames = ['Blues', 'Oranges', 'Greens', 'Reds', 'Purples', 'YlOrBr', 'YlGnBu', 'Greys', 'Inferno', 'Magma', 'Plasma', 'Warm', 'Cool', 'CubehelixDefault', 'BuGn', 'BuPu', 'GnBu', 'OrRd', 'PuBuGn', 'PuBu', 'PuRd', 'RdPu', 'YlGn', 'YlOrRd']
Insert cell
Insert cell
colorSchemes = columnData.map((_, i) => d3[`interpolate${colorSchemeNames[i]}`])
Insert cell
Insert cell
Insert cell
columnColorScales = columnData.map((values, i) => {
return d3.scaleSequential()
.domain(d3.extent(values))
.interpolator(colorSchemes[i % colorSchemes.length]);
});
Insert cell
Insert cell
function columnColorScale(data, index, value, scales = columnColorScales) {
return scales[index](value);
}
Insert cell
Insert cell
tableWithColors = createTable(tableWidth, tableHeight, columnScale, rowScale, columnColorScale, columnData, rowData)
Insert cell
Insert cell
Insert cell
viewof invalidColor = Inputs.color({label: "Invalid color", value: "#ff00ff"})
Insert cell
Insert cell
isValueInvalid = {
const testValue = Number.NaN; // Try a valid number, e.g., 1.
return 'The test value is ' + (isNaN(testValue) ? 'NaN' : 'not NaN');
}
Insert cell
Insert cell
Insert cell
function columnAndInvalidColorScale(data, index, value, scales = columnColorScales) {
return isNaN(value) ? invalidColor : scales[index](value);
}
Insert cell
Insert cell
tableWithInvalidColors = createTable(tableWidth, tableHeight, columnScale, rowScale, columnAndInvalidColorScale, columnData, rowData)
Insert cell
Insert cell
Insert cell
unsortExampleData = [ 5, 8, 1, 4, 9, 0, 3, 6, 4, 7 ]
Insert cell
mutable sortedExampleData = [...unsortExampleData]
Insert cell
Insert cell
{
const svg = d3.select(DOM.svg(28, 28));

svg.append('rect')
.attr('x', 2)
.attr('y', 2)
.attr('ry', 5)
.attr('width', 24)
.attr('height', 24)
.style('stroke', 'black')
.style('fill', 'steelblue')
.on('click', () => {
mutable sortedExampleData = [...unsortExampleData].sort((a, b) => a - b);
});

// 🛠️ Try adding additional event listeners to highlight the rectangle depending on whether the pointer enters or leaves the rectangle area.
// See: https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events
return svg.node();
}
Insert cell
Insert cell
tableWithSorting = createTable(tableWidth, tableHeight, columnScale, rowScale, columnAndInvalidColorScale, columnData, rowData)
Insert cell
Insert cell
{
// Select the table to be modified.
const svg = d3.select(tableWithSorting);

// Add a SVG group for each column.
const columns = svg.selectAll('.column')
.data(numericAttributes)
.join('g')
.attr('class', 'column');

// ⬇️ Solution: Add a rectangle to each column and sort the rows when the rectangles are clicked. ✔️
// The column rectangles cover the entire area of each column to be able to capture the click events.
columns.append('rect')
.attr('x', (_, i) => columnScale(i))
.attr('y', rowScale(0))
.attr('height', rowScale(rowData.length - 1) - rowScale(0))
.attr('width', columnScale.bandwidth())
.style('fill', '#0000') // Set the fill color to transparent.
.style('stroke', 'black') // Set the outline to a solid black.
.on('click', (_, d) => {
// Get the index of the clicked column.
const index = numericAttributes.indexOf(d);
//Sort the rows based on the values in the clicked column.
rowData.sort((a, b) => isNaN(a[index]) ? -1 : isNaN(b[index]) ? 1 : a[index] - b[index]);

// Select and update all rows in the image.
svg.selectAll('.row')
.data(rowData) // Assign the sorted data to the rows.
.each(function(cellData) {
// Update the rectangles in the rows with the new data.
d3.select(this)
.selectAll('rect')
.data(cellData)
.join('rect')
.style('fill', (d, i) => isNaN(d) ? invalidColor : columnColorScales[i](d)); // Recalculate the color of the rectangles.
});
})
.on('pointerenter', (e) => {
// Highlight the column rectangle outline when the pointer enters.
d3.select(e.target) // Select the column rectangle.
.style('stroke-width', 2); // Increase outline thickness.
})
.on('pointerleave', (e) => {
// Un-highlight the column rectangle outline when the pointer leaves.
d3.select(e.target) // Select the column rectangle.
.style('stroke-width', 1); // Reset the outline thickness.
});
}
Insert cell
Insert cell
Insert cell
function createColoredTable(width, height, headerHeight, attributes, data, colorSchemes) {
// Convert the data into a row / column format.
const columnData = attributes.map(key => data.map(row => row[key]));
const rowData = data.map(row => Object.keys(row).filter(key => attributes.includes(key)).map(key => row[key]))

// Create the SVG image.
const svg = d3.select(DOM.svg(width, height));

// Create the column scale (input: column index, output: image x position).
const columnScale = d3.scaleBand()
.domain(d3.range(columnData.length))
.range([0, width])
.padding(0.1); // Padding between columns.

// Create the row scale (input: row index, output: image y position).
const rowScale = d3.scaleBand()
.domain(d3.range(rowData.length))
.range([headerHeight, height])
.padding(0);

// Create a color scale for each column (input: data value, output: color).
const colorScales = attributes.map((_, i) => {
return d3.scaleSequential()
.domain(d3.extent(columnData[i]))
.interpolator(colorSchemes[i % colorSchemes.length]);
});

// Create a group for each row.
const rows = svg.selectAll('.row')
.data(rowData)
.join('g')
.attr('class', 'row');

// Loop through the rows and create the cells.
rows.each(function(rowData, rowIndex) {
// For each cell in the row, create a colored rectangle.
d3.select(this) // Select the row group.
.selectAll('rect')
.data(rowData)
.join('rect')
.attr('x', (_, i) => columnScale(i))
.attr('y', rowScale(rowIndex))
.attr('height', rowScale.bandwidth())
.attr('width', columnScale.bandwidth())
.style('fill', (d, i) => isNaN(d) ? '#f0f' : colorScales[i](d));
});

// Create a group for each column.
const columns = svg.selectAll('.column')
.data(attributes)
.join('g')
.attr('class', 'column');

// Append a rectangle to each column to make columns clickable.
// The rectangles also serve as an outline for the columns.
columns.append('rect')
.attr('x', (_, i) => columnScale(i))
.attr('y', rowScale(0))
.attr('height', rowScale(rowData.length - 1) - rowScale(0))
.attr('width', columnScale.bandwidth())
.style('fill', '#0000') // Set fill color to transparent.
.style('stroke', 'black')
.on('click', (_, d) => {
// If a column is clicked, sort the row data based on the values in the column.
const index = attributes.indexOf(d);
rowData.sort((a, b) => isNaN(a[index]) ? -1 : isNaN(b[index]) ? 1 : a[index] - b[index]);

// Update the rows with the sorted data.
rows.data(rowData).each(function(cellData) {
// Update the color of the rectangles in each row.
d3.select(this)
.selectAll('rect')
.data(cellData)
.join('rect')
.style('fill', (d, i) => isNaN(d) ? '#f0f' : colorScales[i](d));
});
})
.on('pointerenter', (e) => {
// Highlight the column outline on pointer enter.
d3.select(e.target) // Select the column rectangle.
.style('stroke-width', 2);
})
.on('pointerleave', (e) => {
// De-highlight the column outline on pointer enter.
d3.select(e.target) // Select the column rectangle.
.style('stroke-width', 1);
});

// Create the column header text.
columns.append('text')
.attr('x', (_, i) => columnScale(i) + columnScale.bandwidth() * 0.5)
.attr('y', headerHeight * 0.5)
.attr('font-size', headerHeight * 0.5)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central')
.style('cursor', 'default')
.style('user-select', 'none')
.text(d => d);

// Return the HTML element of the SVG image.
return svg.node();
}
Insert cell
Insert cell
coloredTableWithSorting = createColoredTable(width, Math.max(450, tableData.length), 30, numericAttributes, tableData, colorSchemes)
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more