Published
Edited
Nov 30, 2019
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
viewof ternary = slide({value: [0.5, .5, .5], labels: ['Temps', 'Topique', 'Auteur'], width:400, height:300}).g
Insert cell
viewof ternary
Insert cell
ternary2
Insert cell
ternary2 = slide({value: [0.5, .5, .5], labels: ['Temps', 'Topique', 'Auteur'], width:400, height:300}).g
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
vl2 = require("@observablehq/vega-lite@0.2")
Insert cell
Insert cell
import {slider as ternary_slider} from "@maliky/responsive-ternary-slider"
Insert cell
// import {select} from "@jashkenas/inputs"
Insert cell
mt =require('moment@2.24.0/moment.js')
Insert cell
Insert cell
md`### jashkenas function`
Insert cell
Insert cell
Insert cell
d3format = require("d3-format@1")
Insert cell
md`### Slider`
Insert cell
slide = (options) => {
// Observable makes a thumbnail from the first canvas or svg or image that is at least 320x200.
let opts = Object.assign({value: [1/3, 1/3, 1/3], width: 320, height: 320, labels: ['A', 'B', 'C'], fontsize:'.5em'}, options)

// Turn the initial value passed by the user into a composition if it isn't one already
opts.value = C(opts.value)

let node = DOM.svg(opts.width, opts.height)
let svg = d3.select(node).attr('viewBox', '-50 -50 100 100').style('overflow', 'visible')

// The y extent of the corners is [-1, 0.5], or [-50, 25] in view space, due to the rotation of the triangle.
// Offsetting downwards by 12.5 view points puts the triangle in the center of the svg.
let yOffset = 12.5
let g = svg.append('g').attr('transform', `translate(0, ${yOffset})`)
let {sin, cos, max, PI} = Math
let corners = d3.range(3).map(i => {
let angle = -PI/2 + i*(2*PI/3)
return [cos(angle), sin(angle)]
})

let R = 50

let line = d3.line()
.x(([x, y]) => R * x)
.y(([x, y]) => R * y)
.curve(d3.curveLinearClosed)

g.append('path')
.attr('d', line(corners))
.attr('fill', 'transparent')
.attr('stroke-width', 0.5)
.attr('stroke', '#ccc')

g.selectAll('text.label')
.data(corners)
.join('text')
.attr('class', 'label')
.text((d, i) => opts.labels[i])
.attr('x', ([x,y], i) => R * x)
.attr('y', ([x,y], i) => R * y + (i == 0 ? -7.5 : 7.5))
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('font-size', opts.fontsize)

let handle = g.append('circle')
.attr('cx', R * projectX(corners, opts.value))
.attr('cy', R * projectY(corners, opts.value))
.attr('r', 2)
.attr('fill', 'red')
// Define a transition to use when fading subcomposition marks
let t = d3.transition()
.ease(d3.easeCubicOut)
.duration(300)
let updateSubcompositions = (composition, doTransition) => {
g.selectAll('line.projection').data(
// One datum for each non-degenerate pair of (a, b, c).
// We show the projection of the value onto each of the three sides of
// the triangle, and fade the projection out if both endpoints of the
// sub-composition are zero.
[
{ composition: C([composition[0], composition[1], 0]), index: 0 },
{ composition: C([0, composition[1], composition[2]]), index: 1 },
{ composition: C([composition[0], 0, composition[2]]), index: 2 },
].filter(d => d3.sum(d.composition) > 0), // Invalid entries contain NaN.
d => d.index
)
.join(
enter => enter.append('line')
.attr('class', 'projection')
.attr('stroke-width', 0.5)
.attr('pointer-events', 'none')
.attr('stroke-linecap', 'round')
.attr('stroke', 'red')
.attr('opacity', doTransition ? 0 : 1)
.call(enter => doTransition && enter.transition(t).attr('opacity', 1)),
update => update,
exit => exit.transition(t)
.attr('opacity', 0)
.remove()
)
.attr('transform', d => `rotate(${60 + d.index * 120}, ${R * projectX(corners, d.composition)}, ${R * projectY(corners, d.composition)})`)
.attr('x1', d => R * projectX(corners, d.composition))
.attr('y1', d => R * projectY(corners, d.composition) + 0)
.attr('x2', d => R * projectX(corners, d.composition))
.attr('y2', d => R * projectY(corners, d.composition) - 1.5)
}

let dragged = () => {
let p = [d3.event.x / R, d3.event.y / R]
let [a, b, c] = corners
let total = area(a, b, c)
let composition = C([area(b, c, p), area(c, a, p), area(a, b, p)].map(a => max(0, a) / total))
updateSubcompositions(composition, true)
handle.attr('cx', R * projectX(corners, composition)).attr('cy', R * projectY(corners, composition))
node.value = composition
node.dispatchEvent(new CustomEvent("input", { bubbles: true }))
}

updateSubcompositions(opts.value, false)

// Allow dragging anywhere on the surface of the SVG. For d3.event.x and d3.event.y to determine
// the desired coordinates, put this rect inside the offset <g> and give it a transform to undo
// the parent transform.
g.append('rect')
.attr('width', 100).attr('height', 100)
.attr('x', -50).attr('y', -50)
.style('cursor', 'pointer')
.attr('transform', `translate(0, ${-yOffset})`)
.attr('fill', 'transparent')
.call(d3.drag().on('drag', dragged).on('start', dragged))

// Store the triangle "radius" and normalized corner coordinates on the SVG node
// for easy access for e.g. projecting additional data inside the slider.
node.R = R
node.corners = corners
node.labels = opts.labels
node.value = opts.value
return {node, g}
}
Insert cell
// Returns the (signed) area of the triangle with corners `a`, `b`, and `c`
area = ([xa, ya], [xb, yb], [xc, yc]) => 0.5 * (xa*yb + xb*yc + xc*ya - xa*yc - xc*yb - xb*ya)
Insert cell
projectY = ([a, b, c], weights) =>
d3.sum([a[1] * weights[0], b[1] * weights[1], c[1] * weights[2]])
Insert cell
// Project the composition given by `weights` onto the triangle defined by corners `a`, `b`, and `c`
projectX = ([a, b, c], weights) =>
d3.sum([a[0] * weights[0], b[0] * weights[1], c[0] * weights[2]])
Insert cell
// Composition closure operator: https://en.wikipedia.org/wiki/Compositional_data
// Returns a composition version of `arr` where the elements are normalized to sum to 1
C = arr => {
let total = d3.sum(arr)
return arr.map(d => d / total)
}
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