Public
Edited
May 17, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
printTable(nbaColors.slice(0, 8))
Insert cell
Insert cell
vl.markRect()
.data(nbaColors)
.encode(
vl.y().fieldN('Team'),
vl.color().fieldN('Team')
.scale({range: nbaColors.map(nbaTeam => nbaTeam.Color1) }) // set custom color scheme
).render()
Insert cell
Insert cell
vl.markRect()
.data(nbaColors)
.encode(
vl.x().fieldN('Team')
.axis({labelAngle: 320}) // angle the labels to improve readability
.title(null), // turn off team title
vl.color().fieldN('Color1')
.scale({range: vl.fieldN('Color1') }) // set Color1 field as source of color
.legend(null) // turn off legend
).width(Math.max(500, width - 200)).render() // add responsive width
Insert cell
Insert cell
vl.markRect()
.data(nbaColors)
.transform(
// Use Vega's hsl function to extract hue: https://vega.github.io/vega/docs/expressions/
vl.calculate("hsl(datum.Color1).h").as('hue1'), // store as hue1
)
.encode(
vl.x().fieldN('Team')
.sort({field: 'hue1', order: 'descending'})
.axis({labelAngle: 320})
.title(null),
vl.color().fieldN('Color1')
.scale({range: vl.fieldN('Color1') })
.legend(null)
).width(Math.max(500, width - 200)).render() // add responsive width
Insert cell
Insert cell
multiViewHuePlot.render();
Insert cell
multiViewHuePlot = {
const selectedPoint = vl.selectPoint('selected_point') // create our selection param
.on('mouseover') // mouse over for selection
.nearest(true) // select nearest point to the click (helpful because the points are small)
.clear('mouseout'); // move mouse out of vis to clear selection

// Extract hue, saturation, and lightness and set them as new calculated fields
const sharedTransform = [
vl.calculate("hsl(datum.Color1).h").as('hue1'),
vl.calculate("hsl(datum.Color1).s").as('sat1'),
vl.calculate("hsl(datum.Color1).l").as('light1'),
vl.calculate("hsl(datum.Color1).h / 360.0").as('hue1_scaled'),];

const scatterWidth = Math.max(150, (width / 3) - 120), scatterHeight = 250;

// Make the hue/saturation scatter plot
const hueSatScatter = vl.markCircle()
.transform(sharedTransform)
.params(selectedPoint)
.encode(
vl.x().fieldQ('hue1').scale({domain: [0, 360]}).title('Hue'),
vl.y().fieldQ('sat1').title('Saturation'),
vl.size().fieldQ('light1'),
vl.color().fieldN('Color1').scale({range: vl.fieldN('Color1') }).legend(null),
vl.opacity().if(selectedPoint, vl.value(1)).value(0.1)
).width(scatterWidth).height(scatterHeight);

// would be nice to make dx, dy dynamic so text moves based on point size
const hueSatText = vl.markText({align: 'right', dx: -12, dy: -4})
.transform(sharedTransform.concat(vl.filter(selectedPoint.empty(false))))
.encode(
vl.x().fieldQ('hue1').scale({domain: [0, 360]}),
vl.y().fieldQ('sat1'),
vl.text().fieldN('Team'),
);

const hueSatLayer = vl.layer(hueSatScatter, hueSatText);

const hueLightnessScatter = vl.markCircle()
.transform(sharedTransform)
.params(selectedPoint)
.encode(
vl.x().fieldQ('hue1').scale({domain: [0, 360]}).title('Hue'),
vl.y().fieldQ('light1').scale({domain: [0, 1]}).title('Lightness'),
vl.size().fieldQ('sat1'),
vl.color().fieldN('Color1').scale({range: vl.fieldN('Color1') }).legend(null),
vl.opacity().if(selectedPoint, vl.value(1)).value(0.1)
).width(scatterWidth).height(scatterHeight);

const hueLightnessText = vl.markText({align: 'right', dx: -8, dy: -2})
.transform(sharedTransform.concat(vl.filter(selectedPoint.empty(false))))
.encode(
vl.x().fieldQ('hue1').scale({domain: [0, 360]}),
vl.y().fieldQ('light1'),
vl.text().fieldN('Team'),
);
const hueLightnessLayer = vl.layer(hueLightnessScatter, hueLightnessText);

const satLightnessScatter = vl.markCircle()
.transform(sharedTransform)
.params(selectedPoint)
.encode(
vl.x().fieldQ('sat1').scale({domain: [0, 1]}).title('Saturation'),
vl.y().fieldQ('light1').scale({domain: [0, 1]}).title('Lightness'),
vl.size().fieldQ('hue1_scaled'),
vl.color().fieldN('Color1').scale({range: vl.fieldN('Color1') }).legend(null),
vl.opacity().if(selectedPoint, vl.value(1)).value(0.1)
).width(scatterWidth).height(scatterHeight);

const satLightnessText = vl.markText({align: 'right', dx: -8, dy: -2})
.transform(sharedTransform.concat(vl.filter(selectedPoint.empty(false))))
.encode(
vl.x().fieldQ('sat1').scale({domain: [0, 1]}),
vl.y().fieldQ('light1'),
vl.text().fieldN('Team'),
);

const satLightnessLayer = vl.layer(satLightnessScatter, satLightnessText);

const teamRectColors = vl.markRect()
.transform(sharedTransform)
.params(selectedPoint)
.encode(
vl.x().fieldN('Team')
.sort({field: 'hue1', order: 'descending'})
.axis({labelAngle: 320})
.title(null),
vl.color().fieldN('Color1')
.scale({range: vl.fieldN('Color1') })
.legend(null),
vl.opacity().if(selectedPoint, vl.value(1)).value(0.1)
).width(Math.max(600, scatterWidth * 3.5));
return vl.vconcat(vl.hconcat(hueSatLayer, hueLightnessLayer, satLightnessLayer), teamRectColors)
.data(nbaColors)
.resolve({scale: {color: 'independent'}});
}
Insert cell
Insert cell
{
const color1Plot = vl.markRect()
.encode(
vl.y().fieldN('Team'),
vl.color().fieldN('Color1')
.scale({range: vl.fieldN('Color1') })
.legend(null) // turn off legend
)

const color2Plot = vl.markRect()
.encode(
vl.y().fieldN('Team')
.axis(null), // turn off y axis here
vl.color().fieldN('Color2')
.scale({range: vl.fieldN('Color2') })
.legend(null)
)

const color3Plot = vl.markRect()
.encode(
vl.y().fieldN('Team')
.axis(null), // turn off y axis here
vl.color().fieldN('Color3')
.scale({range: vl.fieldN('Color3') })
.legend(null)
)

// example derived from https://stackoverflow.com/questions/68005751/individual-coloring-scale-of-each-column-in-vegalite-highlight-table
return vl.hconcat(color1Plot, color2Plot, color3Plot)
.data(nbaColors)
.resolve({scale: {color: 'independent'}})
.spacing(0)
.title("NBA Team Colors")
.render();
}
Insert cell
Insert cell
{
const columnWidth = 100;
const color1Plot = vl.layer(
vl.markRect().encode(
vl.y().fieldN('Team'),
vl.color().fieldN('Team')
.scale({range: nbaColors.map(nbaTeam => nbaTeam.Color1) })
.legend(null), // turn off legend
),
vl.markText().encode(
vl.y().fieldN('Team'),
vl.text().fieldN('Color1')
),
).width(columnWidth).title("Color 1");

const color2Plot = vl.layer(
vl.markRect().encode(
vl.y().fieldN('Team')
.axis(null), // turn off y axis here,
vl.color().fieldN('Team')
.scale({range: nbaColors.map(nbaTeam => nbaTeam.Color2) })
.legend(null), // turn off legend
),
vl.markText().encode(
vl.y().fieldN('Team'),
vl.text().fieldN('Color2')
),
).width(columnWidth).title("Color 2");

const color3Plot = vl.layer(
vl.markRect().encode(
vl.y().fieldN('Team')
.axis(null), // turn off y axis here,
vl.color().fieldN('Team')
.scale({range: nbaColors.map(nbaTeam => nbaTeam.Color3) })
.legend(null), // turn off legend
),
vl.markText().encode(
vl.y().fieldN('Team'),
vl.text().fieldN('Color3')
),
).width(columnWidth).title("Color 3");

return vl.hconcat(color1Plot, color2Plot, color3Plot)
.data(nbaColors)
.resolve({scale: {color: 'independent'}})
.spacing(3)
.title("NBA Team Colors")
.render();
}
Insert cell
Insert cell
{
const columnWidth = 100;
const color1Plot = vl.layer(
vl.markRect().encode(
vl.y().fieldN('Team'),
vl.color().fieldN('Color1')
.scale({range: vl.fieldN('Color1')})
.legend(null), // turn off legend
),
vl.markText().encode(
vl.y().fieldN('Team'),
vl.text().fieldN('Color1'),
vl.color().if("luminance(datum.Color1) > 0.3", vl.value("black")).value("white")
),
).width(columnWidth).title("Color 1");

const color2Plot = vl.layer(
vl.markRect().encode(
vl.y().fieldN('Team')
.axis(null), // turn off y axis here,
vl.color().fieldN('Color2')
.scale({range: vl.fieldN('Color2')})
.legend(null), // turn off legend
),
vl.markText().encode(
vl.y().fieldN('Team'),
vl.text().fieldN('Color2'),
vl.color().if("luminance(datum.Color2) > 0.3", vl.value("black")).value("white")
),
).width(columnWidth).title("Color 2");

const color3Plot = vl.layer(
vl.markRect().encode(
vl.y().fieldN('Team')
.axis(null), // turn off y axis here,
vl.color().fieldN('Color3')
.scale({range: vl.fieldN('Color3')})
.legend(null), // turn off legend
),
vl.markText().encode(
vl.y().fieldN('Team'),
vl.text().fieldN('Color3'),
vl.color().if("luminance(datum.Color3) > 0.3", vl.value("black")).value("white")
),
).width(columnWidth).title("Color 3");

return vl.hconcat(color1Plot, color2Plot, color3Plot)
.data(nbaColors)
.resolve({scale: {color: 'independent'}})
.spacing(3)
.title("NBA Team Colors")
.render();
}
Insert cell
Insert cell
Insert cell
Insert cell
vl.markImage({width: 40, height: 40})
.data(nbaColors)
.encode(
vl.x().fieldN('Team').axis({labelAngle: 320}),
vl.url().fieldN('LogoSvgUrl'),
).width(Math.max(600, width - 100)).render()
Insert cell
vl.data(nbaColors) // TODO add some jitter
.transform(
vl.calculate("rgb(datum.Color1).r").as('red1'),
vl.calculate("rgb(datum.Color1).g").as('green1'),
)
.layer(
vl.markCircle().encode(
vl.x().fieldQ('red1'),
vl.y().fieldQ('green1'),
),
vl.markText({align: 'left', baseline: 'middle', dx: 10, dy: 0}).encode(
vl.x().fieldQ('red1'),
vl.y().fieldQ('green1'),
vl.text().fieldN('Team')
),
vl.markImage({width: 20, height: 20, opacity: 0.9}).encode( // must set both width & height or image size doesn't change
vl.x().fieldQ('red1'),
vl.y().fieldQ('green1'),
vl.url().fieldN('LogoSvgUrl'),

// the mark image does not respond to size unfortunately
// see: https://github.com/vega/vega-lite/issues/5365
vl.size().fieldQ('red1')
//vl.size().value(100) // markImage does not appear to respond to this size
),
)
.width(400)
.render()
Insert cell
Insert cell
Insert cell
Insert cell
vegalite(
{
"data": {
"values": [
{"x": 0.5, "y": 0.5, "img": "https://vega.github.io/vega-lite/data/ffox.png"},
// doesn't seem to be working for some reason with nba url, so something is fundamentally broken about it
// see other logos here https://www.nba.com/teams
// Looks like this is due to CORS
// See: https://observablehq.com/@mbostock/cross-origin-images
// CORS Considerations: https://observablehq.com/@observablehq/working-with-apis-remote-files
{"x": 1.5, "y": 1.5, "img": "https://jonfroehlich.github.io/vis/data/nba/logos/san_antonio_spurs_logo.png"},
{"x": 2.5, "y": 2.5, "img": "https://jonfroehlich.github.io/vis/data/nba/logos/boston_celtics_logo.svg"}
]
},
"mark": {"type": "image", "width": 50, "height": 50},
"encoding": {
"x": {"field": "x", "type": "quantitative"},
"y": {"field": "y", "type": "quantitative"},
"url": {"field": "img", "type": "nominal"}
}
}
)
Insert cell
vl.data(
[{"x": 0.5, "y": 0.5, "img": "https://vega.github.io/vega-lite/data/ffox.png"},
{"x": 1.5, "y": 1.5, "img": "https://jonfroehlich.github.io/vis/data/nba/logos/san_antonio_spurs_logo.png"},
{"x": 2.5, "y": 2.5, "img": "https://jonfroehlich.github.io/vis/data/nba/logos/boston_celtics_logo.png"}])
.layer(
vl.markImage({width: 20, height: 20}).encode( // you must specify both width & height
vl.x().fieldQ('x'),
vl.y().fieldQ('y'),
vl.url().fieldN('img'),
),
vl.markCircle().encode(
vl.x().fieldQ('x'),
vl.y().fieldQ('y'),
),
vl.markText().encode(
vl.x().fieldQ('x'),
vl.y().fieldQ('y'),
//vl.text().fieldN('Team')
)
).render();
Insert cell
golden_state_img = fetchImage("https://jonfroehlich.github.io/vis/data/nba/logos/golden_state_warriors_logo.svg")
Insert cell
// From: https://observablehq.com/@mbostock/cross-origin-images
function fetchImage(src) {
return new Promise((resolve, reject) => {
const image = new Image;
image.crossOrigin = "anonymous";
image.src = src;
image.onload = () => resolve(image);
image.onerror = reject;
});
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import {vegalite} from "@jonfroehlich/vega-lite-utilities"
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