Public
Edited
Mar 21
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
allCREATOR = ShotSpotterDataCSV.map(row => row.CREATOR)
Insert cell
uniqueCREATOR = new Set(allCREATOR.sort(d3.ascending))
Insert cell
SS_LoadedData_Drop3_CREATOR = SS_LoadedData_DropGIS_ID.map(({CREATOR, ...rest}) => rest);
Insert cell
allEDITOR = ShotSpotterDataCSV.map(row => row.EDITOR)
Insert cell
uniqueEDITOR = new Set(allEDITOR.sort(d3.ascending))
Insert cell
SS_CleanedData1_Metadata = SS_LoadedData_Drop3_CREATOR.map(({EDITOR, ...rest}) => rest);
Insert cell
Insert cell
allGLOBALID = ShotSpotterDataCSV.map(row => row.GLOBALID)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
uniqueSOURCEArray = [...uniqueSOURCE];
Insert cell
{
const brush = vl.selectInterval().encodings('x');
const click = vl.selectMulti().encodings('color');

const scale = {
domain: ["WashingtonDC4D", "WashingtonDC5D", "WashingtonDC6D", "WashingtonDC7D"],
range: ['#e7ba52', '#a7a7a7', '#aec7e8', '#1f77b4', '#9467bd']
};

const plot1 = vl.markLine()
.encode(
vl.color().value('lightgray')
.if(brush, vl.color().fieldN('SOURCE').title('Source')), // Different colors for each source
vl.x().fieldT('Year').axis({title: 'Year'}), // Extract Year from DATETIME
vl.y().fieldQ('avg_calls_per_year').axis({title: 'Avg Calls Per Year'}), // Aggregated calls per year
vl.tooltip([
vl.fieldN('SOURCE'),
vl.fieldT('Year'),
vl.fieldQ('avg_calls_per_year')
])
)
.width(800)
.height(300)
.select(brush)
.transform(
vl.timeUnit('year').field('DATETIME').as('Year'), // Extract year
vl.aggregate()
.groupby(['Year', 'SOURCE'])
// .count().as('calls_per_year'), // Count per year per source
vl.window()
.groupby(['SOURCE'])
.op('mean')
.field('calls_per_year')
.as('avg_calls_per_year'), // Compute yearly average per source
vl.filter(click) // Apply click selection
);

const plot2 = vl.markBar()
.encode(
vl.color().value('lightgray')
.if(click, vl.color().fieldN('SOURCE').scale(scale).title('Source')),
vl.x().count(),
vl.y().fieldN('SOURCE').scale({domain: scale.domain}).title('Source')
)
.width(width)
.select(click)
.transform(vl.filter(brush));

return vl.vconcat(plot1, plot2)
.data(TransformedData2_CleanTYPEAttribute)
.autosize({type: 'fit-x', contains: 'padding'})
.render();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
vl.markPoint()
.data(TransformedData1_FixedDataTypes)
.title("Unique lat points")
.encode(
vl.x().fieldT("DATETIME").timeUnit("yearmonthdate"), // fieldT is shorthand for temporal data type
vl.y().fieldN('TYPE').sort(["Gunshot_or_Firecracker", "Probable Gunfire", "Single_Gunshot", "Single Gunshot", "Multiple_Gunshots", "Multiple Gunshots"]) ,
vl.color().fieldN('TYPE')
)
.width(width - 100) // make the graph responsive to Observable canvas width
.render();




Insert cell
vl.markPoint()
.data(TransformedData1_FixedDataTypes)
.transform(
vl.aggregate([
vl.min("DATETIME").as("first_date"),
vl.max("DATETIME").as("last_date")
]).groupby(["TYPE"]),
vl.joinaggregate([
vl.min("DATETIME").as("first_date"),
vl.max("DATETIME").as("last_date")
]).groupby(["TYPE"]),
vl.filter("datum.DATETIME === datum.first_date || datum.DATETIME === datum.last_date")
)
.encode(
vl.x().fieldT("DATETIME"),
vl.y().fieldN("TYPE"),
vl.color().fieldN("TYPE")
)
.width(width - 100) // Responsive width
.render();
Insert cell
viewof strip = vl.markTick()
.data(TransformedData1_FixedDataTypes)
.encode(
vl.row().fieldN("TYPE"),
vl.x().fieldT("DATETIME").timeUnit('utcmonthdate'),
vl.y().fieldO("DATETIME").timeUnit("utcyear"),
vl.color().fieldN("TYPE"),
)
.render()
Insert cell
vl.markPoint({ filled: true, size: 80 }) // Make points more visible
.data(TransformedData2_CleanTYPEAttribute)
.title("Unique Latitude Points")
.transform(
vl.groupby("LATITUDE") // Group by LATITUDE to get unique points
)
.encode(
vl.x().fieldT("MinTime").title("Earliest Recorded Time"), // X-axis: First timestamp per unique latitude
vl.y().fieldQ("LATITUDE").title("LAT").scale({ domain: [38.74, 38.984] }), // Y-axis: Unique latitudes
vl.color().value("steelblue") // Fixed color for clarity
)
.width(width - 100)
.height(400)
.autosize({ type: "fit-x", contains: "padding" })
.render();

Insert cell
{
const chart = vl.markLine({ size: 10 })
.data(TransformedData2_CleanTYPEAttribute) // Use full dataset, not pre-filtered `minMaxArray`
.transform(
vl.aggregate()
.groupby(["TYPE"]) // Group by incident type
.ops(["min", "max"]) // Compute min and max dates
.fields(["DATETIME", "DATETIME"]) // Apply on DATETIME field
.as(["min_date", "max_date"]) // Store results
)
.encode(
vl.x().fieldT("min_date")
.title("Earliest Recorded Date")
.axis({
grid: false,
labelAngle: -45,
}),
vl.x2().fieldT("max_date")
.title("Latest Recorded Date"),
vl.y().fieldN("TYPE")
.title("Incident Type")
.sort(["Gunshot_or_Firecracker", "Probable Gunfire", "Single_Gunshot", "Single Gunshot", "Multiple_Gunshots", "Multiple Gunshots"])
.axis({
labelLimit: 200,
grid: true
}),
vl.color().fieldN("TYPE")
.sort("ascending")
.scale({
range: [
'#1b9e77', // A1 - Dark Green
'#66a18b', // A2 - Muted Green
'#d95f02', // B1 - Dark Orange
'#e69a61', // B2 - Muted Orange
'#7570b3', // C1 - Dark Purple
'#a99bc1' // C2 - Muted Purple
]
}),
vl.tooltip([
vl.fieldN("TYPE"),
vl.fieldT("min_date"),
vl.fieldT("max_date")
])
)
.width(800)
.height(200)
.title({
text: "Time Range of Gunfire Incidents by Type",
fontSize: 16,
anchor: 'middle'
})
.config({
axis: {
labelFontSize: 12,
titleFontSize: 13
},
legend: {
labelFontSize: 12
},
timeFormat: "%B %d, %Y"
})
.render();

return chart;}

Insert cell
Insert cell
vl.layer(
vl.markCircle()
.data(SS_CleanedData1_Metadata)
.encode(
vl.y().fieldN('TYPE'),
vl.x().fieldT('DATETIME').timeUnit('yearmonthdate').axis({ labelAngle: -45 }),
vl.size().count(),
vl.color().fieldN('TYPE'),
),

vl.markRule()
.data(SS_CleanedData1_Metadata)
.transform(
vl.filter("timeFormat(datum.DATETIME, '%m-%d') == '07-04'") // Filters dynamically for every July 4
)
.encode(
vl.x().fieldT('DATETIME'),
vl.color().value('red'),
vl.size().value(.2) // Thickness of the vertical line
)
)
.width(1000)
.height(300)
.render()


{
const markCircle = vl.markCircle()
.data(SS_CleanedData1_Metadata)
.encode(
vl.y().fieldN('TYPE'),
vl.x().fieldT('DATETIME').timeUnit('yearmonthdate').axis({ labelAngle: -45 }),
vl.size().count(),
vl.color().fieldN('TYPE'),
),
);

const line = base
.markLine()
.encode(vl.y().count(), vl.color().fieldN("TYPE"));

return vl
.layer(line)
.data(TransformedData1_FixedDataTypes)
.width(650)
.height(300)
.render();
}


Insert cell
Insert cell
Insert cell
{
const base = vl.mark().encode(
vl.x().fieldT("DATETIME")
);

const line = base
.markLine()
.encode(vl.y().count(), vl.color().fieldN("TYPE"));

return vl
.layer(line)
.data(TransformedData1_FixedDataTypes)
.width(650)
.height(300)
.render();
}
Insert cell
vl.markArea()
.width(1100)
.height(2000)
.data(TransformedData2_CleanTYPEAttribute)
.encode(
vl.y()
.fieldT("DATETIME")
.timeUnit("yearmonth")
.axis({ domain: false, format: "%m - %Y", tickSize: 10 }),
vl.x()
.count()
.axis(null)
.stack("center")
.axis({ title: 'Count' }), // Add this line to set the x-axis title,
vl.color()
.fieldN("SOURCE")
.scale({
scheme: "category20b",
domain: ["WashingtonDC5D", "WashingtonDC1D", "WashingtonDC7D", "WashingtonDC6D", "WashingtonDC3D", "WashingtonDC4D"]
}),
vl.tooltip([
{ field: 'SOURCE', type: 'nominal', title: 'Source' }
])
)
.title('Your Chart Title') // Add this line to set the title
.render()
Insert cell
vl.markLine()
.data(TransformedData2_CleanTYPEAttribute)
.transform(
vl.aggregate([{op: 'count', as: 'count1'}]).groupby(['DATETIME', 'TYPE', 'SOURCE'])
)
.encode(
vl.x().fieldT('DATETIME'), // Temporal field for x-axis
vl.y().fieldQ('count1').title('Number of Calls'),
vl.color().fieldN('TYPE').title('Type of Call:'),
vl.column().fieldN('SOURCE')
)
.width(300)
.height(200)
.render()
Insert cell
Insert cell
Insert cell
TransformedData3_UniqueCoords = TransformedData2_CleanTYPEAttribute.map(row => {
return {
...row,
LATITUDE: row.LATITUDE in uniqueLAT ? fixTypes[row.TYPE] : row.TYPE
}
})
Insert cell
Insert cell
Insert cell
Insert cell
{
const base = vl.mark().encode(
vl.x().fieldT("DATETIME")
);

const line = base
.markLine()
.encode(vl.y().count(), vl.color().fieldN("SOURCE"));

return vl
.layer(line)
.data(TransformedData2_CleanTYPEAttribute)
.width(650)
.height(300)
.render();
}
Insert cell
plot = spec.render()
Insert cell
spec = {
// select a point for which to provide details-on-demand
const hover = vl.selectPoint('hover')
.encodings('x') // limit selection to x-axis value
.on('mouseover') // select on mouseover events
.toggle(false) // disable toggle on shift-hover
.nearest(true); // select data point nearest the cursor

// predicate to test if a point is hover-selected
// return false if the selection is empty
const isHovered = hover.empty(false);
// define our base line chart of stock prices
const line = vl.markCircle().encode(
vl.x().fieldT('DATETIME'),
vl.y().count(),
vl.color().fieldN('SOURCE')
);
// shared base for new layers, filtered to hover selection
const base = line.transform(vl.filter(isHovered));

// mark properties for text label layers
const label = {align: 'left', dx: 5, dy: -5};
const white = {stroke: 'white', strokeWidth: 2};

return vl.data(TransformedData2_CleanTYPEAttribute)
.layer(
line,
// add a rule mark to serve as a guide line
vl.markRule({color: '#aaa'})
.transform(vl.filter(isHovered))
.encode(vl.x().fieldT('DATETIME')),
// add circle marks for selected time points, hide unselected points
line.markCircle()
.params(hover) // use as anchor points for selection
.encode(vl.opacity().if(isHovered, vl.value(1)).value(0)),
// add white stroked text to provide a legible background for labels
base.markText(label, white).encode(vl.text().fieldN('SOURCE')),
// add text labels for stock prices
base.markText(label).encode(vl.text().fieldN('SOURCE'))
)
.width(700)
.height(400);
}
Insert cell
{
const brush = vl.selectInterval().encodings('x');
const click = vl.selectMulti().encodings('color');

const scale = {
domain: ['sun', 'fog', 'drizzle', 'rain', 'snow'],
range: ['#e7ba52', '#a7a7a7', '#aec7e8', '#1f77b4', '#9467bd']
};

const plot1 = vl.markPoint({filled: true})
.encode(
vl.color().value('lightgray')
.if(brush, vl.color().fieldN('weather').scale(scale).title('Weather')),
vl.size().fieldQ('precipitation').scale({domain: [-1, 50], range: [10, 500]}).title('Precipitation'),
vl.order().fieldQ('precipitation').sort('descending'),
vl.x().timeMD('date').axis({title: 'Date', format: '%b'}),
vl.y().fieldQ('temp_max').scale({domain: [-5, 40]}).axis({title: 'Maximum Daily Temperature (°C)'})
)
.width(width)
.height(300)
.select(brush)
.transform(vl.filter(click));

const plot2 = vl.markBar()
.encode(
vl.color().value('lightgray')
.if(click, vl.color().fieldT('DATETIME').title('OVER TIME')),
vl.x().count(),
vl.y().fieldN('SOURCE').scale({domain: scale.domain}).title('MPD DISTRICT')
)
.width(width)
.select(click)
.transform(vl.filter(brush));

return vl.vconcat(plot1, plot2)
.data(TransformedData2_CleanTYPEAttribute)
.autosize({type: 'fit-x', contains: 'padding'})
.render();
}
Insert cell
Insert cell
Insert cell
Insert cell
minX = Math.min(...allX)
Insert cell
maxX = Math.max(...allX);
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
uniqueCoords = new Set(ShotSpotterDataCSV.map(d => `${d.X},${d.Y},${d.LATITUDE},${d.LONGITUDE}`));
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
allLAT = TransformedData1_FixedDataTypes.map(row => row.LATITUDE)
Insert cell
minLAT = Math.min(...allLAT)
Insert cell
Insert cell
Insert cell
Insert cell
coordinateCounts = {};
Insert cell
vl.markBar()
.data(TransformedData1_FixedDataTypes)
.aggregate()
.groupby("LATITUDE") // ✅ Group by Latitude
.count().as("count") // ✅ Count occurrences of each unique Latitude
.encode(
vl.x().fieldQ("LATITUDE").title("Latitude"),
vl.y().fieldQ("count").title("Occurrences"),
vl.color().fieldQ("count").scale({ scheme: "blues" }).title("Frequency"),
vl.tooltip([
vl.fieldQ("LATITUDE").title("Latitude"),
vl.fieldQ("count").title("Occurrences")
])
)
.width(800)
.height(600)
.title("Frequency of Unique Latitudes in Data")
.render();

Insert cell
Insert cell
Insert cell
vl.markCircle()
.data(TransformedData1_FixedDataTypes)
.encode(
vl.x().fieldT('DATETIME'),
vl.y().count(),
vl.color().fieldN('SOURCE')
).render()
Insert cell
vl.layer(
// Boxplot for X values
vl.markBoxplot()
.data(TransformedData2_CleanTYPEAttribute)
.encode(
vl.x().fieldQ("LATITUDE").title("LAT").scale({ domain: [38.74, 38.984] })
),

// Outliers: Points outside the usual range
vl.markCircle({ size: 50 })
.data(TransformedData2_CleanTYPEAttribute)
.transform(
vl.filter("datum.LATITUDE > 38.984") // Highlight instead of removing

)
.encode(
vl.x().fieldQ("X"),
vl.color().value("red"), // Outliers in red
vl.tooltip([
vl.fieldQ("X")
])
)
)
.width(1000)
.height(200)
.title("Box Plot with Outlier Highlighting")
.render();
Insert cell
vl.markLine()
.data(TransformedData1_FixedDataTypes)
.encode(
vl.x().fieldT("DATETIME").title("Checked DATETIME"),
vl.y().count()
)
.render();
Insert cell
Insert cell
Insert cell
Insert cell
allDates = fixedSS1_types.map(row => row.DATETIME)
Insert cell
distinctDates = new Set(allDates.sort(d3.ascending))
Insert cell
Insert cell
vl.markBar()
.data(cleaned2_SS_datetime)
.encode(
vl.y().count(),
vl.x().fieldN('SOURCE'),
)
.render()
Insert cell
vl.markPoint()
.data(cleaned2_SS_datetime)
.encode(
vl.y().count(),
vl.x().fieldT('DATETIME'),
)
.render()
Insert cell
cleaned2_SS_datetime[0].DATETIME instanceof Date;
Insert cell
cleaned2_SS_datetime[0].DATETIME.getFullYear();
Insert cell
Insert cell
Insert cell
vl.markCircle()
.data(cleaned2_SS_datetime)
.encode(
vl.y().fieldN("SOURCE"),
vl.x().fieldT("DATETIME").timeUnit("utchours"),
vl.size().sum("SOURCE") //.scale({ range: [0, 200] })
)
.render()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
archive = FileAttachment("Shot_Spotter_Gun_Shots.zip").arrayBuffer()
Insert cell
archive_boundary_shape = FileAttachment("Washington_DC_Boundary.zip").arrayBuffer()
Insert cell
view(ShotSpotterSHP, { width: 700, height: 350 })
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
geoShotSpotterCSV = geo.coords2geo(ShotSpotterDataCSV, { lat: "LATITUDE", lon: "LONGITUDE" })
Insert cell
view(geoShotSpotterCSV, { width: 700, height: 350 })
Insert cell
Insert cell
ShotSpotterDataGeoJSON = FileAttachment("Shot_Spotter_Gun_Shots.geojson").json()
Insert cell
Insert cell
Insert cell
MPDShape = shp(archive_boundary_shape)
Insert cell
Insert cell
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