diff --git a/src/web/arr/trove/chart.arr b/src/web/arr/trove/chart.arr index e828dc7e..f01c0a72 100644 --- a/src/web/arr/trove/chart.arr +++ b/src/web/arr/trove/chart.arr @@ -1067,6 +1067,7 @@ default-bar-chart-series = { pointer-color: none, axisdata: none, horizontal: false, + dot-chart: false, default-interval-color: none } @@ -1917,6 +1918,57 @@ fun bar-chart-from-list(labels :: P.LoS, values :: P.LoN) -> DataSeries block: data-series.make-axis(max-positive-height, max-negative-height) end +fun num-dot-chart-from-list(labels :: P.LoN, values :: P.LoN) -> DataSeries block: + doc: ``` + Consume labels, a list of numbers, and values, a list of numbers + and construct a dot chart + ``` + labels.each(check-num) + num-dot-chart-args = map2(lam(x-elt, y-elt): [list: x-elt, y-elt] end, + labels, values) + .sort-by({(x-list, y-list): x-list.get(0) < y-list.get(0)}, + {(x-list, y-list): x-list.get(0) == y-list.get(0)}) + dot-chart-from-list(map(lam(x-list): num-to-string(x-list.get(0)) end, + num-dot-chart-args), + map(lam(x-list): x-list.get(1) end, num-dot-chart-args)) +end + +fun dot-chart-from-list(labels :: P.LoS, values :: P.LoN) -> DataSeries block: + doc: ``` + Consume labels, a list of string, and values, a list of numbers + and construct a dot chart + ``` + # Type Checking + values.each(check-num) + labels.each(check-string) + + # Constants + label-length = labels.length() + value-length = values.length() + rational-values = map(num-to-rational, values) + + # Edge Case Error Checking + when value-length == 0: + raise("dot-chart: can't have empty data") + end + when label-length <> value-length: + raise('dot-chart: labels and values should have the same length') + end + + {max-positive-height; max-negative-height} = prep-axis(rational-values) + + data-series = default-bar-chart-series.{ + tab: to-table2-n(labels, rational-values), + dot-chart: true, + axis-top: max-positive-height, + axis-bottom: max-negative-height, + annotations: values.map({(_): [list: none]}) ^ list-to-table2, + intervals: values.map({(_): [list: [raw-array: ]]}) ^ list-to-table2, + } ^ bar-chart-series + + data-series.make-axis(max-positive-height, max-negative-height) +end + fun grouped-bar-chart-from-list( labels :: P.LoS, value-lists :: P.LoLoN, @@ -2613,6 +2665,8 @@ from-list = { exploding-pie-chart: exploding-pie-chart-from-list, image-pie-chart: image-pie-chart-from-list, bar-chart: bar-chart-from-list, + dot-chart: dot-chart-from-list, + num-dot-chart: num-dot-chart-from-list, image-bar-chart: image-bar-chart-from-list, grouped-bar-chart: grouped-bar-chart-from-list, stacked-bar-chart: stacked-bar-chart-from-list, diff --git a/src/web/js/trove/chart-lib.js b/src/web/js/trove/chart-lib.js index 604bd8fc..6f1251bc 100644 --- a/src/web/js/trove/chart-lib.js +++ b/src/web/js/trove/chart-lib.js @@ -700,6 +700,7 @@ // ASSERT: if we're using custom images, there will be a 4th column const hasImage = table[0].length == 4; + const dotChartP = get(rawData, 'dot-chart'); // Adds each row of bar data and bar_color data table.forEach(function (row) { @@ -717,7 +718,7 @@ color : interval_color, }, series : { - 0 : { dataOpacity : hasImage? 0 : 1.0 } + 0 : { dataOpacity : (hasImage || dotChartP)? 0 : 1.0 } } }; @@ -761,10 +762,13 @@ onExit: defaultImageReturn, mutators: [backgroundMutator, axesNameMutator, yAxisRangeMutator], overlay: (overlay, restarter, chart, container) => { - if(!hasImage) return; + + if (!hasImage && !dotChartP) return; // if custom images are defined, use the image at that location // and overlay it atop each dot + + google.visualization.events.addListener(chart, 'ready', function () { // HACK(Emmanuel): // If Google changes the DOM for charts, these lines will likely break @@ -772,6 +776,7 @@ const rects = svgRoot.children[1].children[1].children[1].children; $('.__img_labels').each((idx, n) => $(n).remove()); + if (hasImage) { // Render each rect above the old ones, using the image as a pattern table.forEach(function (row, i) { const rect = rects[i]; @@ -790,7 +795,42 @@ Object.assign(imageElt, rects[i]); // we should probably not steal *everything*... svgRoot.appendChild(imageElt); }); + } + + if (dotChartP) { + table.forEach(function (row, i) { + // console.log('row', i, '=', row); + const rect = rects[i]; + // console.log('rect', i, '=', rect); + const num_elts = row[1]; + const rect_x = Number(rect.getAttribute('x')); + const rect_y = Number(rect.getAttribute('y')); + const rect_height = Number(rect.getAttribute('height')); + const unit_height = rect_height/num_elts; + const rect_width = Number(rect.getAttribute('width')); + const rect_fill = rect.getAttribute('fill'); + // const rect_fill_opacity = Number(rect.getAttribute('fill-opacity')); + // const rect_stroke = rect.getAttribute('stroke'); + // const rect_stroke_width = Number(rect.getAttribute('stroke-width')); + rect.setAttribute('stroke-width', 0); + for (let j = 0; j < num_elts; j++) { + const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + circle.classList.add('__img_labels'); + circle.setAttribute('r', rect_width/8); + circle.setAttribute('cx', rect_x + rect_width/2); + circle.setAttribute('cy', rect_y + (num_elts - j - 0.5)*unit_height); + circle.setAttribute('fill', rect_fill); + // circle.setAttribute('fill-opacity', rect_fill_opacity); + // circle.setAttribute('stroke', rect_stroke); + // circle.setAttribute('stroke-width', rect_stroke_width); + // console.log('adding circle elt', i, j, '=', circle); + svgRoot.appendChild(circle); + } + }); + } + }); + } }; } diff --git a/test-util/pyret-programs/charts/dot-chart-test.arr b/test-util/pyret-programs/charts/dot-chart-test.arr new file mode 100644 index 00000000..998b5ee2 --- /dev/null +++ b/test-util/pyret-programs/charts/dot-chart-test.arr @@ -0,0 +1,63 @@ +include chart +include image +import color as C +# include image-structs +include math + +labels = [list: "cats", "dogs", "ants", "elephants"] +count = [list: 3, 7, 4, 9] +nlabels = [list: 2, 4, 3, 1] + +zoo-series = from-list.dot-chart(labels, count) + +n-zoo-series = from-list.num-dot-chart(nlabels, count) + +just-red = [list: C.red] +rainbow-colors = [list: C.red, C.orange, C.yellow, C.green, C.blue, C.indigo, C.violet] +manual-colors = + [list: + C.color(51, 72, 252, 0.57), C.color(195, 180, 104, 0.87), + C.color(115, 23, 159, 0.24), C.color(144, 12, 138, 0.13), + C.color(31, 132, 224, 0.83), C.color(166, 16, 72, 0.59), + C.color(58, 193, 241, 0.98)] +fewer-colors = [list: C.red, C.green, C.blue, C.orange, C.purple] +more-colors = [list: C.red, C.green, C.blue, C.orange, C.purple, C.yellow, C.indigo, C.violet] + +fun render-image(series): + render-chart(series).get-image() +end + +zoo = render-image(zoo-series) +zoo-red = render-image(zoo-series.colors(just-red)) +zoo-rainbow = render-image(zoo-series.colors(rainbow-colors)) +zoo-manual = render-image(zoo-series.colors(manual-colors)) +zoo-fewer = render-image(zoo-series.colors(fewer-colors)) +zoo-more = render-image(zoo-series.colors(more-colors)) + +n-zoo = render-image(n-zoo-series) +n-zoo-red = render-image(n-zoo-series.colors(just-red)) +n-zoo-rainbow = render-image(n-zoo-series.colors(rainbow-colors)) +n-zoo-manual = render-image(n-zoo-series.colors(manual-colors)) +n-zoo-fewer = render-image(n-zoo-series.colors(fewer-colors)) +n-zoo-more = render-image(n-zoo-series.colors(more-colors)) + +check: + zoo satisfies is-image + zoo-red satisfies is-image + zoo-rainbow satisfies is-image + zoo-manual satisfies is-image + zoo-fewer satisfies is-image + zoo-more satisfies is-image + + n-zoo satisfies is-image + n-zoo-red satisfies is-image + n-zoo-rainbow satisfies is-image + n-zoo-manual satisfies is-image + n-zoo-fewer satisfies is-image + n-zoo-more satisfies is-image + + color-at-position(zoo-rainbow, 208, 340) is C.red + color-at-position(zoo-rainbow, 322, 340) is C.orange + color-at-position(zoo-rainbow, 439, 340) is C.yellow + color-at-position(zoo-rainbow, 558, 340) is C.green +end diff --git a/test-util/pyret-programs/charts/image-bar-chart-test.arr b/test-util/pyret-programs/charts/image-bar-chart-test.arr index 357e109e..b102e635 100644 --- a/test-util/pyret-programs/charts/image-bar-chart-test.arr +++ b/test-util/pyret-programs/charts/image-bar-chart-test.arr @@ -1,6 +1,7 @@ include chart include image -include image-structs +import color as C +# include image-structs # color tan clashes with starter2024's trig tan include math fun f(r): star(50, "solid", "red") end @@ -19,5 +20,5 @@ img = render-chart(series) check: img satisfies is-image - color-at-position(img, 504, 258) is red + color-at-position(img, 504, 258) is C.red end